Study & Practice

北海道札幌市のプログラマによる技術とか雑記のブログ

読書感想文『そろそろはじめるテスト駆動開発』

はじめに

Software Design 2022年3月号の特集『そろそろはじめるテスト駆動開発』を読んだので覚えておきたいなと思ったことをまとめます。

今回は自動テスト編です。

特集のおおまかな内容

著者は日本におけるテスト駆動開発の第一人者、和田卓人さん。「テスト書いてないとかお前それ@t_wadaの前でも同じこと言えんの?」というライオンのアスキーアートでお馴染み方です。

前半では自動テスト、テストファーストテスト駆動開発がそれぞれどのようなもので、どのような違いがあるのかについて解説している。後半ではテスト駆動開発をするにはどのような手順踏めばよいのか、どのように考えると良いのかをJavaScriptの実際のコードをベース解説している。

今回はその中から自動テストについて書かれている部分について覚えておきたいと思ったことをまとめていきます。

自動テストとは

テストもコードで書いて、そのコードを実行することでテストの実施を自動で行っていく取り組みのこと

自動テストに必須の特性

自動テストで効果をあげるために必須となる性質

自己検証可能

テストの結果を人が見て判断しなくても成功か失敗かがわかること。つまりコードの実行結果が期待しているものかどうかの判断をコンピュータが行える状態。Pass/Failedをアサーションなどで判別して行うのが一般的。

繰り返し可能

テストがいつでもどの環境でも同じように動くこと。ローカル環境、開発環境、CI環境それぞれで同じように動き、テスト後の後処理なども人間が行う必要がない状態

必須ではないが強く推奨される特性

独立していること

テストを行う順番や並列実行による影響を受けないこと。どんな順番でどのタイミングに実行されても変わらない結果が得られる状態

高速であること

コードが自分の考えた通りに動いているかを判断するため、できる限り高速に動く必要がある。実行に時間がかかってしまうようだとテストが億劫になって実行頻度が下がり、コードが壊れたことに気付くのが遅れてしまう。

自動テストのメリット

根拠のある自信

開発しているシステムが想定通り動いていることで自信が生まれるようになります。これが根拠のある自信であり、著者が自動テストの最大の効果であると述べています。自動テストが整備されていれば既存のコードを壊してしまうかもしれないという心理的負担を感じずにリファクタリングを行うことができる。この安心感が継続的な変更と改善を支える。

デバッグを大幅に軽減できる

自動テストは既存のコードを壊してしまってもすぐに気づくことができる回帰テストリグレッションテスト)としての効果があります。バグの混入にいち早く気付けるようになることで、デバッグの時間を大幅に削減できます。

詳細なドキュメントになる

実際のソフトウェアと用意されたドキュメントに乖離しているという状態は誰しも経験があると思います。自動テストはもし乖離が起きてもテストが失敗することで乖離に気付くことができます。テストコードはよりコードに近い、詳細なドキュメントとしても活かすことができます。

自動テストの注意点

学習コストがかかる

過去のさまざまな事例からテストを書くコストはデバッグの時間の軽減などで相殺され、むしろ黒字になるのでテストを書き慣れているプログラマーはテストを書いたほうが早く開発を進められる。しかし、テストを描き慣れていないプログラマーはテストの書き方を学びながら開発を進める必要があるので、より時間がかかってしまう。

メンテナンスコストがかかる

自動テストは設計変更の影響を受けるため、テストコードの可読性や変更容易性も意識しないとメンテナンスコストが高くなってしまう。そのため、テストコードも定期的にリファクタリングを行なって適切な状態を保っていく必要がある。

品質保証としてはもの足りない

自動テストによって欠陥を減らし品質を高めることができるが、それによって品質が保証されているというわけではない。自動テストとは別に、コードレビューやペアプログラミングなど、品質を高める施策を行なっていく必要がある。

感想

自動テストのメリットは品質や開発効率という面にあると思っていたので、1番の利点が心理的なものであるというのは意外でした。言われてみると確かに自動テストがあることでリファクタリングを行うときにも、既存の機能にバグを生み出してしまうかもしれないといった恐怖感を低減できる。それによって品質や開発効率の向上という効果が生まれる、というのは納得ができます。メリットを活かすためにも、十分なカバー範囲を持ったテストを実装する必要がありますし、個人でなくチームレベルで自動テストの文化を作っていくためにも、今後は自動テストの手法について学んでいこうとおもいます。

 

CSS: font-sizeで使うem/remについて調べてみた

今までWeb開発であまりCSSを本格的に学んだことがなく、なんとなくでCSSを書いてきましたが、フロントエンド業務を行うプロジュエクトにアサインされてCSSを学ぶ必要性を感じてきました。ひとまず、HTML/CSSの写経を行なっていて気になったfont-sizeのem/remが何者か気になったので、調べてみました。

em/remとは一体なんなのか

em/remはCSSで指定できる相対的な長さを表す単位で、CSSで定義されているlengthというデータ型に属しています。

em

emは親要素で指定されているfont-sizeとの相対的なサイズを指定します。例えば親要素が16pxの場合、1emでは16px、2emでは32pxになります。emでは複合(compounding)と呼ばれる事象が発生します。これは親要素との相対的なサイズで指定されるためです。例えば以下のようなHTML・CSSが設定されている場合に、タグのOuterは32pxになりますが、タグのInnerは64pxになります。

<div>
  <span>Outer <span>Inner</span> Outer</span>
</div>
div {
  font-size: 16px;
}
span {
  font-size: 2em;
}
rem

remは親要素ではなく、ルート要素(タグ)との相対的なサイズを指定します。親要素の影響受けないため、emとは違い複合発生しないことがメリットになります。つまり、以下のようなhtmlとCSSが設定している場合、全て32pxになります。

<div>
  <span>Outer <span>Inner</span> Outer</span>
</div>
html {
  font-size: 16px;
}
span {
  font-size: 2rem;
}

まとめ

アクセシビリティの観点から、font-sizeの設定値はem/remを設定することが推奨されています。これはユーザーが設定したフォントサイズからの相対値で文字の大きさを指定できるからです。逆にいうと、ユーザーが設定したフォントサイズの影響を受けたくない場合についてはem/remの使用を避ける方が良いということでしょう。

短いエントリになりましたが、なんとなくスルーしていた要素について調べることでスッキリしました。今後のHTML/CSSについて学びを深めてきたいと思います。

Vue.jsのライフサイクルフックを学ぶ

7月からVue.jsを扱うプロジェクトに入ったのでVue.jsをおさらいしています。今回はライフサイクルフックについて学んでいきます。参考は公式ドキュメントです。

jp.vuejs.org

ライフサイクルフックとは

インスタンスの状態が変化したタイミングで自動的に呼ばれる関数です。ライフサイクルフックを理解することはVue.jsで効果的な開発をすることにとって、とても重要です。

beforeCreate

Vueインスタンスが作られる前、インスタンスが初期化されるときに呼ばれる。インスタンスプロパティなんかにアクセスすることができないのであんまり使うことはない。あるとしたらVueインスタンスを作る前に、Vue外部と連携を取る必要がある時かな。経験はないです。

created

インスタンスが作られた後に呼ばれます。このタイミングでdata、computed、methodsなどが利用可能になります。dataやcomputedにアクセスできるので、使い所はありそうですね。

beforeMount

Vueインスタンスが作成された後、DOMが初めて構築される直前に呼ばれます。DOMが構築される「前」なのであまり使い所はないですね。

mounted

DOMが構築されたタイミングで呼ばれます。HTMLとして描画されているわけではないこと、子コンポーネントのDOMも構築されていることが保証されるわけでない点に注意が必要です。

beforeUpdate

dataオプションで保持している値が変更され、DOMが更新される前に呼ばれます。更新される前のDOMにアクセスしたいときに有効です。

updated

dataオプションで保持している値が変更され、DOMが更新された後に呼ばれます。update内でDOMの更新を行うと無限ループに陥ってしまうので注意が必要です。また、子コンポーネントが更新されていることは保証されません。呼ばれる回数も多いことから描画コストもかかる上に、ロジックが複雑になりやすいのでできるだけ使うのを避けた方がいいフックです。

activated

コンポーネントが活性化するときに呼ばれます。このフックはタグを使用している時のみ呼ばれます。

deactivated

コンポーネントが非活性化するときに呼ばれます。このフックはタグを使用している時のみ呼ばれます。

beforeDestroy

destroy()メソッドを呼んだ時など、Vueインスタンスが破棄される直前に呼ばれます。このフック内では、Vueの各オプションはまだ機能しているため、インスタンス内の情報を利用したい場合に有効になるフックです。

destroyed

Vueインスタンスが破棄された後に呼ばれます。このフックが呼ばれるとき、既にVueの各オプションは機能していないので、Vueインスタンスが持っていた情報にはアクセスできないため使い所は少ないフックです。

errorCaputured

コンポーネントからエラーが捕捉されたときに呼ばれます。このフックは返り値としてbooleanが定義されています。falseを返すことで親コンポーネントへのエラーの伝播を止めることができます。

まとめ

さらっとVueもライフサイクルフックを全て紹介してみました。activated、deactivatedなんてフックがあるのは知らなかったし、destroyedなんて使うことあんのかな〜って改めて思いました。使うのはほとんどcreated、mountedあたりなんじゃないでしょうか?ひとまず、頭の片隅ででも覚えておけば、開発中に何かあったときにドキュメントを見返して使ってみることができると思うので、是非覚えてみてください。

Vue.js + TypeScriptの始め方

7月から新しいチームに参画しました。Nuxt.js+TypeScriptを使ったプロジェクトで、技術的な難易度は高くないとのことなんですが、Nuxt.jsは会社が推しているフロントエンド技術でもあるようなので、これから学習していこうと思っています。まずは何事も基本から、ということで今回はNuxt.jsの前にVue.s+TypeScriptのコードを書いていくための下準備を行なっていきます。

手順などは公式ドキュメントに従います。
v3.vuejs.org

Vue CLIのインストール

まずはVue CLIをインストールしていきます。業務ではyarnを使っているみたいなので今回はyarnを使います。

yarn global add @vue/cli

以下のコマンドを実行してバージョンが表示されればOKです

vue --version

プロジェクト作成

次にプロジェクトを作ります。下記のコマンドはhello-world-appという名前のプロジェクトを作るコマンドです。

vue create hello-world-app

コマンドを実行するとターミナルが以下のような表示になります。

? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 2] babel, eslint)
  Default (Vue 3) ([Vue 3] babel, eslint)
  Manually select features

vue.jsをそのまま使うのであればVue2かVue3で問題ありませんが、今回はTypeScriptを使うので「Manually select features」を選択します。

次は以下のような表示になります。

? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to t
oggle all, <i> to invert selection)
❯◉ Choose Vue version
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

TypeScriptを使用するので、TypeScriptを選択します。選択するには矢印キーで移動してスペースキーを押します。

下記のようにTypeScript横の丸が塗りつぶされた状態になっていればOKです。

? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Choose Vue version
 ◉ Babel
❯◉ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

エンターキーを押して次に進みます。

あとはお好みで問題ありませんが、参考に僕が選んだ結果を以下に載せておきます。

? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Linter
? Choose a version of Vue.js that you want to start the project with 2.x
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N)No

正常にプロジェクトが作成されたら以下のような出力がされているはずです。

🎉  Successfully created project hello-world-app.
👉  Get started with the following commands:

 $ cd hello-world-app
 $ yarn serve

次は指示に従ってアプリケーションを動かしてみます。

アプリケーションの実行

以下のコマンドを実行します。

cd hello-world-app
yarn serve

ブラウザからhttp://localhost:8081/にアクセスするとVue.js + TypeScriptのWelcomeページが表示されます。

Vue.js + TypeScriptのWelcomeページ

Hello Worldする

僕はどんな言語や技術でもHello Worldと出力することから始めることにしているので、今回もやっていきます。

./src/App.vueを以下のように編集します。

<template>
  <div id="app">
    <h1>Hello World!</h1>
  </div>
</template>

これでhttp://localhost:8081/にアクセスしたら「Hello World!」と出力されているはずです。

まとめ

おつかれさまでした。とはいっても今回は本当に初歩的なことをしているだけですけどね。次からはVue.jsやTypeScriptの構文についてのポストをしていきたいと思います。

LaravelのValidationを使ってみる

5月から参画したチームがLaravelを使っていまして、ちょいちょい不具合修正のチケットなんかを担当し始めた。しかし、Laravelの機能がまだあんまりわかってないもので既存のコードを読むのに時間がかかってしまっています。ということで公式ドキュメントを読んでいろんな機能を使ってみようと思います。今回はその第一弾、Validation機能を使っていきます。

laravel.com

下準備

まずは下準備です。

以下のコマンドでコントローラーを追加します。

php artisan make:controller ValidationController

コントローラーにview表示用のメソッドを追加します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ValidationController extends Controller
{
    // 追加
    public function index()
    {
        return view('validation');
    }

    // 追加
    public function validation(Request $request)
    {
        echo "name: $request->name<br>";
        echo "email: $request->email<br>";
    }
}

以下のコマンドでviewを追加

touch resources/views/validation.blade.php

resources/views/validation.blade.phpの内容は以下のようにします。

<form method="POST" action="/validation">
    @csrf
    <label>Name</label>
    <input id="name" name="name" type="text">
    <br>
    <label>Email</label>
    <input id="email-address" name="email" type="email">
    <button type="submit">Submit</button>
</form>

最後にweb.phpにValidationController::index()とValidationController::validation()へのルーティング設定を追加します。

<?php

use App\Http\Controllers\ValidationController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

// 追加
Route::get('/validation', [ValidationController::class, 'index']);
Route::post('/validation', [ValidationController::class, 'validation']);

これでhttp://localhost:8000/validationにブラウザからアクセスすると

f:id:carametal:20210623203734p:plain
こんな感じのシンプルなフォームが出来上がります。

そしてNameとEmailに適当な文字列を入れてSubmitを押すと以下のような出力がされるはずです。

name: test name
email: test@example.com

これで下準備が完了です。

基本的なValidationの使い方

それではValidationの機能を使っていきます。

まずはValidationControllerのvalidationメソッドを以下のように更新します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ValidationController extends Controller
{
    public function index()
    {
        return view('validation');
    }

    // 更新
    public function validation(Request $request)
    {
        $request->validate([
            'name' => 'required|max:10',
            'email' => 'required|max:20'
        ]);
        echo "name: $request->name<br>";
        echo "email: $request->email<br>";
    }
}

Requestのvalidateメソッドが公式ドキュメントで一番最初に紹介されるValidation機能です。今回のコードはリクエストのnameとemail両方を必須項目として、nameを最大10文字、emailを最大20文字の制限をつけています。

ただ、実行していただければわかるかと思いますが、validationでエラーになるデータをPOSTしてもFormの画面に戻されるだけでエラーなどが表示されませんので、エラーメッセージの表示機能を追加してみます。

<form method="POST" action="/validation">
    @csrf
    <label>Name</label>
    <input id="name" name="name" type="text">
    <br>
    <label>Email</label>
    <input id="email-address" name="email" type="email">
    <button type="submit">Submit</button>
</form>

{{-- 追加 --}}
@if ($errors->any())
    <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
    </ul>
@endif

validationがエラーになると自動的に元の画面にリダイレクトされます。$errorsという変数にエラーメッセージが格納されているため、上記のように取り出して画面上に表示することができます。

ためしにNameとEmailをどちらも入力せずにSubmitを押してみると以下のようなエラーメッセージが表示されます。

The name field is required.
The email field is required.

どちらの項目も必須項目に設定してあるためfield is requiredのメッセージが出ます。

次はNameを11文字以上、Emailを21文字以上にしてみましょう。

The name must not be greater than 10 characters.
The email must not be greater than 20 characters

今度はmust not be greater thanのメッセージが出ました。意図した通りにValidationが効いていますね。


ちなみに今回のような規定のvalidationを使用している場合、resources/lang/en/validation.phpでエラーメッセージを編集することができます。定義されている連想配列の中からrequired、maxとそれぞれのキーを探して値として設定されているエラーメッセージを編集してください。

※上記のファイルは英語用のファイルとして定義されています。日本語用のファイルを設定したい方は以下を参考にしてください。
laravel.com

Form Requestを使ったValidation

次はForm Request機能を使ったValidationを試してみます。

Form Requestはartisanコマンドで作ることができます。

php artisan make:request ValidationRequest

app/Http/Requests/ValidationRequest.phpを以下のように書き換えます。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ValidationRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        // false -> true
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        // 更新
        return [
            'name' => 'required|max:15',
            'email' => 'required|max:25'
        ];
    }
}

Validationの内容が一緒だと変化したかわかりずらいのでそれぞれmaxを15と25に設定してみました。これでコントローラーのValidationが不要になったので削除します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ValidationController extends Controller
{
    public function index()
    {
        return view('validation');
    }

    // 更新
    public function validation(ValidationRequest $request)
    {
        $request->validated();
        echo "name: $request->name<br>";
        echo "email: $request->email<br>";
    }
}

$request->validated();のでForm Requestで設定したValidationを呼び出しています。早速使ってみましょう。

The name must not be greater than 15 characters.
The email must not be greater than 25 characters.

それぞれ15文字と25文字の制限になりましたね。これでForm RequestのValidationを実装できました。

ちなみにForm Requestではエラーメッセージのカスタムもできます。やってみましょう

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ValidationRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:15',
            'email' => 'required|max:25'
        ];
    }

    // 追加
    public function messages()
    {
        return [
            'name.required' => 'Name is required.',
            'name.max' => 'Name must be less than 15 characters.',
            'email.required' => 'Email is required.',
            'email.max' => 'Email must be less than 25 characters.',
        }
    }
}

messages()メソッドはFormRequestクラスに定義されており、それをオーバーライドする形で定義します。requestのキーとValidationのルール名をドットで繋ぐ記法を使っています。上記のコードでそれぞれを未入力のままSubmitすると

Name is required.
Email is required.

ちゃんと反映されてますね。

Nameを16文字以上、Emailを26文字以上入力すると

Name must be less than 15 characters.
Email must be less than 25 characters.

こちらもちゃんと反映されてますね。

それとattributes()メソッドをオーバーライドすることで汎用的なエラーメッセージを作ることもできます。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ValidationRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:15',
            'email' => 'required|max:25'
        ];
    }

    // 追加
    public function attributes()
    {
        return [
            'name' => 'Name',
            'email' => 'Email'
        ];
    }

    public function messages()
    {
        return [

            '*.required' => ':attribute is required.',
            'name.max' => 'Name must be less than 15 characters.',
            'email.max' => 'Email must be less than 25 characters.',
        ];
    }
}

このようにすることで:attributeの部分がnameのエラーではName、EmailのエラーではEmailという表示をしてくれるようになります。

Validationのリダイレクト先の指定

Validationでエラーになった場合に元の画面ではなく、特定の指定した画面にリダイレクトさせたい場合があると思います。その場合はValidator::make()メソッドを使うことができます。

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ValidationRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;

class ValidationController extends Controller
{
    public function index()
    {
        return view('validation');
    }

    public function validation(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|max:15',
            'email' => 'required|max:25'
        ]);

        if($validator->fails()) {
            return redirect('/error');
        }
        echo "name: $request->name<br>";
        echo "email: $request->email<br>";
    }
}

fails()メソッドはValiationでエラーが発生した際に、trueを返します。なのでエラーが発生したら/errorにリダイレクトするという処理になります。

リダイレクト先が必要になるので以下のようにルーティングを追加してください。

<?php

use App\Http\Controllers\ValidationController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/validation', [ValidationController::class, 'index']);
Route::post('/validation', [ValidationController::class, 'validation']);

// 追加
Route::get('/error', function() {
    return 'ERROR.';
});

これでValidationでエラーが出るデータを入力してsubmitすると

ERROR.

と表示されるはずです。

まとめ

ということでValidation機能を使ってみました。今回はrequiredとmaxしかしようしていませんが、組み込みのValidationルールは数多くあるので是非公式ドキュメントを見て使ってみてください。

laravel.com

サービスコンテナ入門(Laravel)

5月から配属されたチームがLaravelを使ったチームなので、ちゃんと勉強しとこうと思ってLaravelのドキュメントを読み始めました。Laravelを使う上で避けて通れないのが「サービスコンテナ」ということで、本ポストを学んだことの事項とします。

サービスコンテナとは

公式ドキュメントでは「サービスコンテナはクラスの依存関係を管理し依存性の注入(Dependency Injection)を実行するための強力なツールです。」と記載があります。

laravel.com

依存性の注入(Dependency Injection)ってなに?なんのためにあるのっていう方にはこちらの記事がおすすめです。

qiita.com

それでは早速Laravelのコードを書いて学んでいきましょう。

準備

まずはLaravelプロジェクトを作ります。

composer create-project laravel/laravel service-container-example

※最新の公式ドキュメントではLaravel Sailの利用が推奨されているんですが、今開発用に使っているM1 MacBook Airだとエラーが解決できなかったのでcompoerを使ってます。

プロジェクトディレクトリに入ってPHP用のテストサーバを起動します。

cd service-container-example
php artisan serve

ブラウザからhttp://localhost:8000にアクセスしてLaravelのWelcomeページが表示されれば準備完了です。

サービスコンテナの使い方

サービスコンテナを使うためのAPIを作っていきます。まずはコントローラーからです。プロジェクトルートディレクトリで以下のコマンドを実行します。

php artisan make:controller HelloController

./app/Http/Controllers/HelloController.phpができるので、indexメソッドを作成します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class HelloController extends Controller
{
    // 追加
    public function index()
    {
        return "Hello.";
    }
}

./routes/web.phpにHelloController@index用のルーティングを設定します。

<?php

use App\Http\Controllers\HelloController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

// 追加
Route::get('/hello', [HelloController::class, "index"]);

これでhttp://localhost:8000/helloにアクセスしたら「Hello.」と表示されます。

それではこの機能を単純なサービスコンテナを使って実装していきます。

./app/Models/HelloMaker.phpを以下の内容で作成します。

<?php

namespace App\Models;

class HelloMaker
{
    public function text()
    {
        return "Hello from Hello Maker.";
    }
}

以下のコマンドを実行してHelloMakerProvider.phpを作成します。

php artisan make:provider HelloMakerProvider

./app/Providers/HelloMakerProvider.phpが作成されるのでregisterメソッドにサービスコンテナを利用するための処理を追加します。

<?php
namespace App\Providers;

use App\Models\HelloMaker;
use Illuminate\Support\ServiceProvider;

class HelloMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // 追加
        $this->app->bind(HelloMaker::class, function() {
            return new HelloMaker();
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

そして./config/app.phpで定義されている連想配列のprovidersの項目に「App\Providers\HelloMakerProvider::class」を追加します。
ファイルは結構大きいので全ては載せませんが追加後のprovidersは私の環境では以下のようになっています。

    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

        // 追加
        App\Providers\HelloMakerProvider::class,
    ],


最後にHelloControllerの内容を以下のように編集します。

<?php

namespace App\Http\Controllers;

use App\Models\HelloMaker;
use Illuminate\Http\Request;

class HelloController extends Controller
{
    private $hello_maker;

    // 追加
    public function __construct(HelloMaker $hello_maker)
    {
        $this->hello_maker = $hello_maker;
    }

    public function index()
    {
        return $this->hello_maker->text();
    }
}

HelloMakerProvider.phpのregisterメソッドの処理を記載することによって、HelloControllerのコンストラクタでHelloMakerが呼ばれたときに自動的に新しいインスタンスが生成されるようになります。

これでhttp://localhost:8000/helloにアクセスしたときに「Hello from Hello Maker.」と表示されます。

ただこれはサービスコンテナを使ってはいますがHelloMakerがそのまま出てきているので依存性の注入の利点が活かせていません。ということで依存性の注入を活かすためにインターフェースを利用したバージョンにしてみましょう。

インターフェースを使ったバージョン

まずは./app/Models/TextMaker.phpをinterfaceとして定義します。

<?php

namespace App\Models;

interface TextMaker
{
    public function text();
}

HelloMakerをTextMakerの実装にします。

<?php

namespace App\Models;

// implements TextMakerを追加
class HelloMaker implements TextMaker
{
    public function text()
    {
        return "Hello from Hello Maker.";
    }
}

./app/Providers/HelloMakerProvider.phpを以下のように編集します。

<?php

namespace App\Providers;

use App\Models\HelloMaker;
use App\Models\TextMaker;
use Illuminate\Support\ServiceProvider;

class HelloMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // 変更
        $this->app->bind(TextMaker::class, HelloMaker::class);
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

./app/Http/Controllers/HelloController.phpの内容を以下に編集します。

<?php

namespace App\Http\Controllers;

use App\Models\TextMaker;

class HelloController extends Controller
{
    // $hello_maker から$text_makerに変更
    private $text_maker;

    public function __construct(TextMaker $text_maker)
    {
        $this->text_maker = $text_maker;
    }

    public function index()
    {
        return $this->text_maker->text();
    }
}

これでインターフェースを使ったバージョンの実装ができました。動作は変わっていませんが、HelloControllerではTextMakerしか参照していないのにhttp://localhost:8000/helloにアクセスすると「Hello from Hello Maker.」と表示されるはずです。

ついでにインターフェースの実装を呼び分けるパターンも試してみましょう

実装を呼び分けるパターン

以下のコマンドで./app/Http/Controllers/GoodbyeController.phpを作成します。

php artisan make:controller GoodbyeController

内容は以下のようにします。

<?php

namespace App\Http\Controllers;

use App\Models\TextMaker;
use Illuminate\Http\Request;

class GoodbyeController extends Controller
{
    private $text_maker;

    public function __construct(TextMaker $text_maker)
    {
        $this->text_maker = $text_maker;
    }

    public function index()
    {
        return $this->text_maker->text();
    }
}

./routes/web.phpにGoodbyeController用のルーティング設定を追加します。

use App\Http\Controllers\GoodbyeController;
use App\Http\Controllers\HelloController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::get('/hello', [HelloController::class, "index"]);

// 追加
Route::get('/goodbye', [GoodbyeController::class, "index"]);

./app/Models/GoodbyeMaker.phpを作成し、内容を以下のようにします。

<?php

namespace App\Models;

class GoodbyeMaker implements TextMaker
{
    public function text()
    {
        return "Goodbye from Goodbye Maker.";
    }
}

以下のコマンドで./app/Providers/TextMakerProvider.phpを作成します。

php artisan make:provider TextMakerProvider

内容を以下のようにします。

<?php

namespace App\Providers;

use App\Http\Controllers\GoodbyeController;
use App\Http\Controllers\HelloController;
use App\Models\GoodbyeMaker;
use App\Models\HelloMaker;
use App\Models\TextMaker;
use Illuminate\Support\ServiceProvider;

class TextMakerProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->when(HelloController::class)
            ->needs(TextMaker::class)
            ->give(HelloMaker::class);
        
        $this->app->when(GoodbyeController::class)
            ->needs(TextMaker::class)
            ->give(GoodbyeMaker::class);
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

./app/Providers/HelloMakerProvider.phpは不要になるので削除します。

最後に./config/app.phpのprovidersからApp\Providers\HelloMakerProvider::classをApp\Providers\TextMakerProvider::classを変更します。

これでhttp://localhost:8000/helloにアクセスすると「Hello from Hello Maker.」と表示され、http://localhost:8000/goodbyeにアクセスすると「Goodbye from Goodbye Maker.」と表示されます。

まとめ

以上がLaravelのサービスコンテナの使い方になります。今回のようなテキストを返すだけのAPIに対してサービスコンテナを使ってもあまりメリットは感じられません。しかし、コントローラごとにDBの向き先を変えたい場合や、テストでモックを使いたい場合などではインターフェースを利用した依存性の注入は非常に強力です。疎結合な設計をする際にも重要な技術になってくるので使い方を覚えておいて損はないはずです。みなさんも是非使ってみてください。

リーダブルコードから学んだプログラミングにおける命名原則

転職をして5月から新しい職場で働き始めました。オフィスの本棚には色々な本が置いてるわけですが、実はいままで読んだことなかったリーダブルコードを借りて読んでみました。Clean CodeとCode Complete(上巻のみ)を読んだことがあるので目新しい情報はなかったですが、そういえばこんなことも書いてあったなぁ。みたいなすっかり忘れていた項目も多かったので自分用のリファレンスマニュアルも兼ねて、久々のブログポストにしようと思います。

リーダブルコードは15章に分けて構成されていますが、本ポストではいわゆる「命名」について書かれている2章と3章で、もっとも重要だと感じた内容を3つにまとめました。

明確な単語を選ぶ

1つめは明確な単語を選ぶ、違う言い方をすると役割が伝わりやすい名前をつけることです。本書では「2.1 明確な単語を選ぶ」のほか「2.2 tmpやretvalなどの汎用的な名前を避ける」、「2.3 抽象的な名前よりも具体的な名前を使う」など複数のチャプターに分けられ、詳細な具体例が提示されています。それらに書かれているのは関数の動作や変数の役割を、より簡潔で正確に表すことができる言葉選びをする必要があるということです。

本書ではgetが例に挙げられています。getはプログラミングをしていれば頻繁に目にしますが、できる限り使うべきではありません。なぜなら、getは「値を取得する」こと以上の情報が得られないからです。getの代わりにdownloadを使用することでどこかのサーバーからダウンロードしてくるのだということが明確にわかるようになりますし、selectを使用することで複数の対象の中から取得するということが伝わりやすくなります。

このような単語のボキャブラリーを探す際に便利なThesaurusというサイトが紹介されていました。名前を考える際にチェックしてみてください。

名前に情報を追加する

2つ目は明確な名前をつけた上でより詳細な情報を追加することです。本書では単位を名前に含めることが例として挙げられています。たとえば、描画する図形の高さを保持する変数にheightとだけ名付けるのではなく、heightPixelのように単位を含めた変数名にすることで何を入れるべきなのかがひと目でわかるようになります。

他にも単位ではなく属性を追加するという方法もあります。パスワードを保持してる変数に対して、十分に安全な複雑さを持っている場合はsecurePasswordとしたり、逆に単純で短い場合にはinsecurePasswordと名前をつけることでどんな意図で使われる変数なのかがより詳細に伝わるようになります。ただこの例でいえば、セキュリティの担保するための重要な処理の周辺など、追加した情報が重要な役割を果たす場合でなければ不要な情報になってしまい、余計にわかりにくくしてしまうので注意してください。

不要な単語は投げ捨てる

3つ目は不要な単語は投げ捨てる、つまり意図を伝えるために不要な単語を取り除くことです。前項と対立するようにも感じますが、役割をわかりやすくするという目的は同じです。本書ではconvertToStringをtoStringに変えるという例が挙げられていました。toStringだけでも文字列に変換するという動作が十分に伝わるためconvertは不要だという考え方です。

他に例を挙げるとするならuserDataやuserInfoと名付けられている変数はほとんどの場合でuserで十分です。オブジェクト指向を意識したプログラミングが行われていれば、オブジェクトが何らかのデータを持っていることはあきらかです。そのため単にuserという変数であってもユーザのデータであることはひと目でわかります。

まとめ

非常に短い内容になりましたが、本当に重要なことはシンプルなものです。本ポストにわかりやすい具体的なプラクティスは含まれていませんが、日々この3つを意識してコードを書くことで命名の能力が養われていくはずです。是非、みなさんも試してみてください。