Laravelで作ったsay Hello Worldをフルボッコにする話

前回作成したWebページをフルボッコにリファクタリングしてやります。

現在あるFeature試験は

  • IN : URL
  • OUT : Page

の結合後の試験です。

お店でたとえると、子供に100円掴ませて、指定商品を買ってくればOKと言う感じです。時間は計測していますが、店内の不具合が分かりづらいのですよ

するってぇ〜となんだい?そこで出るのがUnitテストです。
クラスのメソッド単位にI/O試験を書いていきます。
それこそ激しく!そう、フルボッコに!

フルボッコのイメージは馬乗りになって顔面殴打です。

つまりは、I/Oを完全に掌握して試験をしまくります。
完全に、完膚なきほど!

私の場合は以下の様なメソッドでも試験を書きます

public function get_two(){
   return 2;
}

はい、こんなのあったら無視するでしょ、試験します。

個人ではこういった部分は定義使うか分解しますが、わざわざ使用しているという事は今後変更の可能性があるわけです。
引数が増える?2が意味がある?とかあるわけです。

次は殴打です。

完全に馬乗りに乗った状態… 服までも針で固定した状態までした物を形を変えます

コントローラの場合は難しいですが、こんな感じの試験を書くとします

    public function get(){
        $word = 'word';
        $data  = ['word' => $word];
        return view('say',$data);
    }

これを書いているControllerはいじれないのですが、そう言った物は別で処理させます。
この処理を説明すると

  • 出力する文字列を取得する
  • Viewに渡す入れ物を作成する
  • viewに入れ物を入れて返す

… 解説の方が多いw

  • ここで考える部分は$wordで入れた文字は変わりますか?
  • Viewの入れ物は変わりますか? View内の変数名は変わりますか?
  • 入れ物の形は変わりますか?
  • なんか処理が増えますか?

Viewの内容が変わりますと言うならblade.phpを書き換えてね。と思いますので略します。

さて、まず、この処理の中で試験が抑えている部分は?function名(get)とview(say)の二つです。

それ以外、変えまくっても良いです。wordの文字列を1文字ずつ結合させても良いですし、ファイルから読み出してもDBを変えても良いです。

要するに、入り口と出口さえ同じならあとは知らん!と言う試験です。

やっつけ仕事ならこれでもOKですが、今後の対応を考えるともう少しややこしくします。

中で、return viewは返ることは不可能だと思います。
あとはすべてここに必ず必要というわけでもありません。

メソッドで切って… とか思いますが… classを切ります。
ControllerクラスはControllerクラスをexxtendsしているのでそのままではコンストラクタで怒られます;;

新規作成するクラスは…

ControllerService です!

app/Services/Controllers/HelloControllerService.php

はじめにコピペでも良いかもしてませんが、使い方を考えます。

要するに、このサービスは?

  • 表示する文字を取る
  • Viewに渡す入れ物を表示する文字から作る

シンプルに考えるとこんな感じ… 試験を作ります

tests/Unit/Services/Controllers/SayControllerServiceTest.php

class SayControllerServiceTest extends TestCase
{
    private $service = null;
    public function test_make_character(){
        $this->service = new SayControllerService();
        $this->assertEquals('hello',$this->service->makeCharacter());
    }
    public function test_make_view_package(){
        $this->service = new SayControllerService();
        $package = $this->service->makePackage('hello');
        $this->assertEquals('hello',$package->word);
    }
}

文字を返す、makeCharacterと文字列とviewのパッケージを作る感じです

実装すると…

文字列を返すのは… こちら

    public function makeCharacter(){
        return 'hello';
    }

で、test_make_characterはOKですが、makePackageは… classが良いですね。

app/Classes/Views/Packages/SayViewPackage.php に作りました
wordプロパティに文字列が入っているだけなので

<?php
namespace App\Classes\Views\Packages;
class SayViewPackage{
    public $word;
}

はい…これを返します。

    public function makeViewPackage(string $word){
        $package = new SayViewPackage();
        $package->word = $word;
        return $package;
    }

Unit試験です….

Time: 00:00.087, Memory: 18.00 MB

OK (2 tests, 2 assertions)
Process finished with exit code 0

OK.

それを使うようにControllerを変更します

<?php
namespace App\Http\Controllers;
use App\Services\Controllers\SayControllerService;
use Illuminate\Http\Request;
class SayController extends Controller{
    public $controllerService = null;
    public function __construct(SayControllerService $controllerService){
        $this->controllerService = $controllerService;
    }

    public function get(){
        $word = $this->controllerService->makeCharacter();
        $data  = $this->controllerService->makeViewPackage($word);
        return view('say',$data);
    }

Controllerのコンストラクタは引っ張ってくれるので、取得して返します!

んで、Feature試験… NG?

ぎゃぁ〜途中からミスっていたget()ではない!get(Request でwordを取るんだった;; orz…
書き直し…

    public function get(Request $request){
        $word = $request->input('word');
//        $word = $this->controllerService->makeCharacter();
        $data  = $this->controllerService->makeViewPackage($word);
        return view('say',(array)$data);
    }

これで無事…

    return view('say',(array)$data);

もエラーポイントでした。

ここで、終了とか言いません。

ここからが重要です、先に作ったget / post / get_paramを統合させようと考えます。

すべての処理内容を考えると

  • 表示文字列を取得する
  • Viewパッケージを作成する
  • view(‘pay’)を返す

今後を考えると。payは変わる?

入力条件で翻訳とか考えると、変えた方が良いかな?

Controllerは何もせず、ControllerServiceに処理を与える。
wordに取得方法

  • get : request
  • post : request
  • get_param : stringと

普通の人なら、requestとstring引数でとか考えるでしょうが、そんなことはさせません!ってかRequestクラスは試験しづらいのです!

だったらどうするか?もちろん、変換します。ControllerServiceのRequestをw

Classes/Services/Controllers/SayControllerService/SayControllerServiceRequest.php

出来ちゃったw

この中にはwordだけが入ります。

<?php
namespace App\Classes\Services\Controllers\SayControllerService;
class SayControllerServiceRequest{
    public $word;
}

次は、コンバータを作ります。

この試験は… 出来ればハブります

<?php
namespace App\Classes\Services\Controllers\SayControllerService\converter;
use App\Classes\Services\Controllers\SayControllerService\SayControllerServiceRequest;
use Illuminate\Http\Request;

class RequestConverter
{
    public static function convertRequest(Request $request){
        $serviceRequest = new SayControllerServiceRequest();
        $serviceRequest->word = $request->input('word');
        return $serviceRequest;
    }
    public static function convertValue(string $word){
        $serviceRequest = new SayControllerServiceRequest();
        $serviceRequest->word = $word;
        return $serviceRequest;
    }
}

極力単純明快に出来るように考えます… 単語長過ぎ!;;

これを… Controllerに埋め込みます
Controllerでの処理を書くと

  • ServiceRequestを作成する
  • viewを返す

シンプルですね。
実際に書くと

    public function get(Request $request){
        $serviceRequest = RequestConverter::convertRequest($request);
        $response = $this->controllerService->route($serviceRequest);
        return $response->page;
    }

routeにて結果を返しますが、Responseクラスに入れてます。

<?php
namespace App\Classes\Services\Controllers\SayControllerService;
class SayControllerServiceResponse
{
    public $page = null;
}
    public function makeViewPackage(SayControllerServiceRequest $request){
        return $this->makeWordViewPackage($request->word);
    }
    public function makeWordViewPackage(string $word){
        $package = new SayViewPackage();
        $package->word = $word;
        return $package;
    }
    public function route(SayControllerServiceRequest $request){
        $response = new SayControllerServiceResponse();
        $viewPage = $this->makeViewPackage($request);
        $response->page = view('say',(array)$viewPage);
        return $response;
    }

うむ、結構リファクタリングしちゃいましたがこな感じです実際にControllerを見ると…

    public function get(Request $request){
        $serviceRequest = RequestConverter::convertRequest($request);
        $response = $this->controllerService->route($serviceRequest);
        return view($response->view_name,$response->data);
    }
    public function post(Request $request){
        $serviceRequest = RequestConverter::convertRequest($request);
        $response = $this->controllerService->route($serviceRequest);
        return view($response->view_name,$response->data);
    }
    public function param($word){
        $serviceRequest = RequestConverter::convertValue($word);
        $response = $this->controllerService->route($serviceRequest);
        return view($response->view_name,$response->data);
    }

うん、試験はすべて通って、makeCharacterは使っていませんw

このリファクタリングにて、ふぅ〜〜疲れたw

ポイントを書くと

Controllerにはロジカルな物は書きません!
試験しづらい部分は作り込まないように注意です。
サービスを利用してリクエスト/レスポンスを処理します。
リクエスト/レスポンスにした方がデバグが楽です。
サービス提供の有無をログに出力すのに、この部分を出力すればソースを追うのが楽になります
このサンプルでは、view名と埋め込み値が返ってくるので、次のページと値は引けます。
それをAPIに置き換えても同じですね

    public function get(Request $request){
        $serviceRequest = RequestConverter::convertRequest($request);
        $response = $this->controllerService->route($serviceRequest);
        return view($response->view_name,$response->data);
    }

Http RequestをServiceRequestへと変換するRequestConverterの部分ですが、これは泥臭く作ります。置き換えだけです。ロジカルは別にまかせましょう!
もし1の場合は、priceを3の場合はkakakuの…
とか言われる前に両方出しましょう!後で成形してください

    public static function convertRequest(Request $request){
        $serviceRequest = new SayControllerServiceRequest();
        $serviceRequest->word = $request->input('word');
        return $serviceRequest;
    }

最後はControllerServiceは、複雑になったら別のサービスを使いましょう。
このサービスはあくまでもControllerでの処理です、ページです、料金計算は料金計算Serviceにまかせましょう

    public function route(SayControllerServiceRequest $request){
        $response = new SayControllerServiceResponse();
        $viewPage = $this->makeViewPackage($request);

        $response->view_name = 'say';
        $response->data = (array)$viewPage;

        return $response;
    }

書きながら作っていたので前後不覚になっている部分もありますが、こんな感じです!

次回はcsv , YAMLなどのファイル読込かな