のこのこかずのこ

10年エンジニアやってるけどいまだになんもわからん

技術書読んだメモ : 良いコード/悪いコードで学ぶ設計入門

こちらの本を読みました!

もう何年も仕事で超絶レガシーコードを触っていて嫌になることが多々あるので、そして自分もレガシーコードを書いている気がして不安になるので、気になって読みました。

JavaC#のような静的型付けオブジェクト指向言語のクラス設計の話が中心だったので、普段自分が触っているJavaScriptPHPにドンピシャな本ではなかったですが、それでも学ぶことが多かったです。サンプルコードは馴染みのないJavaでしたが、特定の言語に限る話ではないので理解できないことはなく、NG例とOK例がセットで載っているので何が良くて何がダメなのかすらわからない初心者にはとてもわかりやすかったです。普段の仕事ではクラスを書くこともあまりないのですが(ロジックベタ書き)(絶対ダメ)、自分がクラス設計とかオブジェクト指向について完全に誤認していた部分があることが分かって、あ、読んどいてよかった……となりました。言葉だけ知ってて概念をイマイチ理解できていなかったものたちもだいぶクリアになりました。

以下、特に自分が特に誤解してた〜とか知らなかった〜とかやらかしそう〜と思った箇所の備忘録。

不変(イミュータブル)をちゃんと設定する

❌ NGコード これは副作用があるからだめ!

class AttackPower {
  static final int MIN = 0;
  int value;

  AttackPower(int value) {
    if(value < MIN) {
      throw new IllegalArgumentException();
    }
    this.value = value;
  }
  
  void reinForce(int increment) {
    value += increment;
  }
  void disable() {
    value = MIN;
  }
}

主作用というのは、関数(メソッド)が引数を受け取り、値を返すこと。副作用とは主作用以外に状態変更すること。参照透過性(同じ条件(引数)を与えたとき、実行結果が常に等しくなる)を意識すると副作用に気付きやすそう。

インスタンス変数にfinal演算子を付与して不変にする。でもそれだと変更できなくなるので、変更時には変更値を持った新しいインスタンスを生成する。 さらに、引数がintやstringのプリミティブ型ではミスや不正状態を防げないので、型を指定すると良い。

⭕️ Goodコード

class AttackPower {
  static final int MIN = 0;
  final int value;
  // コンストラクタは省略
  AttackPower reinForce(final AttackPower increment) {
    return new AttackPower(this.value + increment.value);
  }
  void disable() {
    return new AttackPower(MIN);
  }
}

出力引数は危険

❌ NGコード

class ActorManager {
  // ゲームキャラの位置を移動する
  void shift(Location location, int shiftX, int shiftY) {
    location.x += shiftX;
    location.y += shiftY;
  }
}

引数のLocationが変更されることが外部からわからない、低凝集になりやすい! データとデータを操作するロジックは同じクラス内に凝集する。

⭕️ Goodコード

class Location {
  final int x;
  final int y;
  Location(final int x, final int y) {
    this.x = x;
    this.y = y;
  }
  Location shift(final int shiftX, final int shiftY) {
    final int nextX = x + shiftX;
    final int nextY = y + shiftY;
    return new Location(nextX, nextY);
  }
}

継承より委譲

継承はよっぽど注意して扱わないと危険!スーパークラス依存を避けるため、下手に継承するより委譲したほうがいい。 委譲とはコンポジション構造にすること。継承するのではなく、privateなインスタンス変数として持ち、呼び出す。

❌ 闇雲な継承

class FighterPhysicalAttack extends PhysicalAttack {
    @Override
    int singleAttackDamage() {
        return super.singleAttackDamage() + 20;
    }
    @Override
    int doubleAttackDamage() {
        return super.doubleAttackDamage() + 10;
    }
}

⭕️ 委譲

class FighterPhysicalAttack {
    private final PhysicalAttack physicalAttack;
    // コンストラクタ省略
    int singleAttackDamage() {
        return physicalAttack.singleAttackDamage() + 20;
    }
    int doubleAttackDamage() {
        return physicalAttack.doubleAttackDamage() + 10;
    }
}

その他もろもろ

  • クラス設計とは、インスタンス変数を不正状態に陥らせないための仕組みづくり。 ≒ コンストラクタにバリデーションを定義すること。
  • ifやswitchの分岐処理を描きそうになったら、まずinterfaceを使うことを検討する!
  • DRY原則において、DRYにすべきはそれぞれの概念単位。同じようなロジック、似ているロジックであっても、概念が違えばDRYにすべきではない。 ex. 通常割引価格と夏季割引価格はロジックは似ていても流用すべきでない。
  • getter/setter メソッドは開発生産性が良くないコードでよく見られる!よそのクラスを気にしたりいじったりするメソッド構造に陥りやすいので闇雲に実装すべきではない。
  • メソッドはコマンド(=状態の変更)とクエリ(=問い合わせ、状態を返す)のどちらか一方だけを行うよう設計する。
// コマンド
void gainPoint() {
    point += 10;
}
// クエリ
int getPoint() {
    return point;
}

以上のようなポイントを、普段使っているJavaScriptに応用しようと思うと、機能自体がなくて難しいものもある…。けど、調べた感じ、TypeScriptではかなりカバーできそう。(TypeScriptには不変にするためのreadonly プロパティがあったり、interfaceがあったり。) TypeScriptもちゃんと勉強したい!

技術書読んだメモ : フロントエンド開発のためのテスト入門

こちらの本を読みました。とっても勉強になりました!

読もうと思ったわけと感想

テストを導入の必要性はとても感じているけれど、仕事でなかなかテストを書く機会に恵まれず…。 そもそも自分がフロントエンドのテストって何すればいいのかよくわかっていなかったので、提案もままならず…なので、まずは知識をつけたいと思ってこちらの本を読みました。フロントエンドのテストに関して体系的にまとめられている本はほとんどお見かけしないので、本当にありがたいです。

結果、読む前と後ではテストに対する解像度がガラッと変わりました。世の中にはこんなに便利なツールが溢れているのかと!(笑) ツールの情報が与えられて、使い所が理解できれば、テストを書くこと自体はそれほど難しくなさそう、と感じました。(導入のハードルはあるけど)

普段の仕事ではVueを使っているのですが、こちらの本のサンプルコードはReactとNext.jsで書かれています。慣れないReactのコードは読むのに時間がかかりましたが、要所で説明が入るので理解できないことはなかったです。React + Next.jsのプロジェクトってこんな感じなのか〜、と触れるいい機会にもなりました。(やっぱりReactは勉強しなきゃな…。) サンプルコードはGitHubからcloneできるので手元で色々試しながら読めたのも楽しかったです。

以下は、復習も兼ねた備忘録。

Jest

Jest · 🃏 Delightful JavaScript Testing

こちらの本の体感7割くらいが Jestによるテストの解説だったし、後々出てくるStorybookやPlaywrightでもJestを使ったりJestライクなコードだったりするので、まずはJestでコンポーネント単位の単体テストを書けるようになるといいっぽい。

単体テストはロジックのテストしかイメージできていなかったが、jsdom と Testing Library をインストールすることでUIコンポーネントのテストもできることを知った。カバレッジレポートの読み方も章を使って説明が書いてあって、ありがたかった。

テストしやすい単位にコンポーネントを設計することが大事だな〜と感じた。

UIテストで使ったライブラリ
  • jest-environment-jsdom : JestでDOM APIを使用するための環境。jest.config.ts に設定する。
  • @testing-library/jest-dom : UIコンポーネント用カスタムマッチャーの拡張。
  • @testing-library/react : renderでコンポーネントレンダリングしたり、screenで要素を取得したり、fireEventでDOMイベントを発火させたり。
  • @testing-library/user-event : キーボード入力など、実際のユーザー操作により近いシミュレートができる。
モック化

Jestのモックはいろんな手法があってちょっと混乱しがち。適切に使えるようになりたい。

  • jest.fn() : 無からモック関数を生み出せる。適当なコールバック関数を指定したい時とかに便利。
  • jest.spyOn() : 既存の関数(メソッド)を監視して実行情報を記録する。モックとして動作を上書きすることも可能。
  • jest.mock() : importした外部モジュールをまるっとモック化する。
  • MSW (Mock Service Worker) : ネットワークレベルのモックを実現するライブラリ。特定のWeb APIリクエストをインターセプトし、レスポンスを任意の値に書き換えることができる。これ使いこなせたらローカルのテストが捗りそう。
  • next-router-mock : Next.jsのRouterに関連するテストを書くためのライブラリ。 setCurrentUrl で現在のURLを仮定できたりする。

Storybook

Storybook: Frontend workshop for UI development

これめちゃくちゃ便利! コンポーネントの状態毎に登録できるのでせっかくコンポーネントを作ったのに誰にも使われない…という事態が減りそうだし、開発途中にデザイナーさんとかと認識合わせるのにもとても便利そう。コンポーネントの一覧画面かと思いきや、テストもできる。

ディレクトリで npx storybook init コマンド一発打つだけで始められて、サンプルコードもあるので、後述の GitHub Actionsとか試してみる時のプレイグラウンドとしても使いやすかった。

使ってみたアドオン
  • @storybook/addon-essentials : 標準インストール時に追加される便利アドオン。Controls、Actions、Viewportなどのデバッガーが使える。
  • msw-storybook-addon : StorybookでMSWを使う時のアドオン。
  • storybook-addon-next-router : StorybookでNext.jsのRouterの設定をしたい時のアドオン。
  • @storybook/addon-a11y : アクセシビリティの検証をしてくれる。
  • @storybook/test-runner : CLIとかCIでStoryのテストができる。Storybookでついでにテストしたい時に便利。
  • @storybook/testing-react : StoryをJestのテストに読み込んで、コンポーネントを再利用できる。

GitHub ActionsでVRT(ビジュアルリグレッションテスト)を自動化

GitHubリポジトリにpushした時に、Storybookのキャプチャ画像を比較して差分のレポートを受け取るVRTを自動化した。比較するのは、AWS S3に保存した過去のキャプチャ画像。 ハンズオンでできて楽しかった!おお、動いた!という感じ。これがCIか…!

やったことをざっとメモ
  1. Storycapをインストール。Storybookに登録したStoryの画像キャプチャを撮るツール。
  2. GitHubリポジトリ作成&初期push。
  3. AWS S3で新規バケットを作成。アクセス設定が必要。
  4. AWS IAMでS3にアクセスできるユーザーを作成。
  5. reg-suit をインストール。インストール時にプラグイン選択。reg-keygen-git-hash-pluginreg-publish-s3-plugin はコミットハッシュ値命名されたスナップショットと検証結果レポートをAWS S3に転送する。reg-notify-github-plugin は検証結果をプルリクエストに通知する。reg-suit GitHub Appをリポジトリにインストールする作業もある。
  6. GitHubリポジトリのActions Secretsにクレデンシャル情報を設定。
  7. GitHub Actionsのworkflowを書いてpushする。Storybookをビルドして、Storycapを実行して、reg-suitを実行する感じ。

ちなみに、ローカルで簡易的にキャプチャ比較とかしたい場合は、reg-suitよりもreg-cliが向いてるようだ。

Playwright

Fast and reliable end-to-end testing for modern web apps | Playwright

ブラウザオートメーションでE2Eテストできる。Dockerとかで動かせる環境があればいいので、導入しやすそう。DBのseederとかはちゃんと用意する必要があるけど。 npx playwright show-report で見れるレポートがわかりやすいし、--debugで実行した時のデバッグが楽しい。テストじゃなくても単にブラウザオートメーションとして使っても良さそう。

いつか仕事で使えたらいいな〜!

jest.fn() と jest.spyOn() の用法の違い

Jest で書かれた複雑なテストコードを読んでいると、 モック関数である jest.fn()jest.spyOn() の用法で混乱してしまうので、超シンプルなコードで要点を押さえておく。自分用メモ。

jest.fn()

test("jest.fn", () => {
    const mockFn = jest.fn();
    mockFn(1,2);  
    expect(mockFn).toHaveBeenCalledWith(1,2);
  });
  • 無からモック関数を生み出せる。
  • 適当なコールバック関数を指定したい時とかに便利。
  • .mockReturnValue() .mockImplementation() などで挙動のカスタマイズ可能。

jest.spyOn()

const obj = { add: (a: number, b: number): number => a + b };

test("spyOn", () => {
    const spy = jest.spyOn(obj, "add");
    obj.add(1,2);
    expect(spy).toHaveBeenCalledWith(1,2);
  });
  • 既存の関数(メソッド)を置き換える。ので、あくまでも元となる関数(メソッド)が必要。
  • スパイされた元の関数(メソッド)が呼び出されるたびに、その情報が記録される。基本的には監視目的として使う。
  • モックとして使いたい場合は、.mockImplementation()などで挙動のカスタマイズも可能。

ちなみに、importしたモジュールを spyOn したい場合は、 jest.mock()でモジュールをまるっとモック化する必要があるみたい。

import * as util from ".";

 jest.mock(".");   // これがないと TypeError: Cannot redefine property エラーになる

test("spyOn", () => {
    const spy = jest.spyOn(util, "add");
    util.add(1,2);
    expect(spy).toHaveBeenCalledWith(1,2);
  });

Jest実行時の fetch is not defined エラーに対処する

問題のコード

Jestでテストしたかったのは、TypeScriptでfetch()を使った簡単な関数。

export function getData(): Promise<Profile> {
  return fetch("https://jsonplaceholder.typicode.com/users").then(handleResponse);
}

事象に直接は関係ないけど、テストコードはこんな感じ。

  test("データ取得成功", async () => {
    await expect(getData()).resolves.toHaveLength(10);
  });

npm test でテスト実行すると、 ReferenceError: fetch is not defined エラーとなった。

関数の方をts-nodeで実行すれば、エラーにはならずにfetchは問題なく実行される。どうやらJestを通すとfetchが見つからなくなってしまうようだ。

解決方法

結論から言うと、 cross-fetch をimportすることで解決した。

www.npmjs.com

参考にさせていただきました : Jest × React Testing Library × msw で fetch を使ってるコンポーネントのテストでエラーになった - かもメモ

npmでインストール。

npm i -D cross-fetch

テストファイルにimportする。

この時に、cross-fetch/polyfillをimportすることで、グローバルにインポートできるみたい。なので、実際にfetch()を実行するファイルでなくても、テストファイルでimportすればOK。もしくは、すべてのテストに適用されるように jest.setup.ts でimportしといてもいいかも。

import 'cross-fetch/polyfill';

でもなんで?

Node.jsはv20.11.1、Jestは v29.4 を使っているので、fetchがデフォルトで使えてもいいと思うのだが…。 と思って調べたら、こちらの記事が大変参考になりました。

node v18 で導入された global fetch を jsdom 環境下の jest で利用する方法 #Node.js - Qiita

jest.config.ts を確認したら、まさに jest-environment-jsdom を使っていたので、こいつが悪さをしてるみたい。

とりあえず今回は、polyfillを使うってことで問題ないけど、これは罠だな〜…

Jest で非同期な関数のテストを書く

最近は、こちらの本でフロントエンドのテストについて勉強しています。テストって書いた方がいいんだろうな〜と思いながらもとっつきにくいというか何をしたらいいか分からなかったので、体系的に学べるのは大変助かるしとても勉強になります!

その中から、Jestで非同期のテストをするときの書き方パターンを、本の内容+自分で調べたことのまとめ。 Jestはドキュメントも丁寧。

非同期コードのテスト · Jest

テストしたい関数

// 必ず成功するPromiseを返す関数
export function alwaysResolve(duration: number) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(duration);
    }, duration);
  });
}

// 必ず失敗するPromiseを返す関数
export function alwaysReject(duration: number) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(duration);
    }, duration);
  });
}

// 普通に使う場合
alwaysResolve(3000).then((n)=>{console.log(n)});
alwaysReject(4000).catch((n)=>{console.log(n)});

非同期処理のテストコード

パターン1: 関数を実行した時に生成されたPromiseインスタンスを、テスト関数の戻り値としてreturnし、Promiseが解決するまでテストの判定を待つ。

then / catchを使うので感覚的にわかりやすい。が、ちょっと冗長なコード。

describe("Promiseをreturnして解決を待つ", () => {
  test("alwaysResolveの実行結果", () => {
    return alwaysResolve(50).then((duration) => {
      expect(duration).toBe(50);
    });
  });
  test("alwaysRejectの実行結果", () => {
    return alwaysReject(50).catch((duration) => {
      expect(duration).toBe(50);
    });
  });
});

パターン2 : resolves rejects マッチャーを使用したアサーションをreturnする。

ちょっとシンプルに書ける。が、うっかりreturnを書き忘れちゃうと、失敗すべきテストも常にパスするバグが生まれるので注意が必要。

describe("マッチャーを使う", () => {
  test("alwaysResolveの実行結果", () => {
    return expect(alwaysResolve(50)).resolves.toBe(50);
  });
  test("alwaysRejectの実行結果", () => {
    return expect(alwaysReject(50)).rejects.toBe(50);
  });
});

※ 上記2つのパターンでは、JestがPromiseの完了を待つために return が必要ってところが注意ポイント。

パターン3 : async/await と、resolves rejects マッチャーを使用する。

async/await を使えば、自動的にPromiseを返すため、returnを書き忘れてバグる心配はなし。まぁ、同じようにawaitを書き忘れた場合も常にテストがパスしちゃいますが…(書き忘れるな)

describe("async/awaitとマッチャーを使う", () => {
  test("alwaysResolveの実行結果", async () => {
    await expect(alwaysResolve(50)).resolves.toBe(50);
  });
  test("alwaysRejectの実行結果", async () => {
    await expect(alwaysReject(50)).rejects.toBe(50);
  });
});

パターン4 : async/await でPromiseが解決するのを待ってから、アサーションに展開する。

resolveのテストは一番シンプルに書けるけど、rejectのテストはtry...catchを使う必要がある。さらに、テスト対象の関数が意図せず成功してしまった場合に、catch節を通らなくて、テストをパスしてしまう危険がある!そのため、想定した数のアサーションが呼ばれたことを確認するために expect.assertions を必ず記述する必要がある。

describe("async/awaitで解決を待つ", () => {
  test("alwaysResolveの実行結果", async () => {
    expect(await alwaysResolve(50)).toBe(50);
  });
  test("alwaysRejectの実行結果", async () => {
    expect.assertions(1); // これが大事!!!
    try {
      await alwaysReject(50);
    } catch (err) {
      expect(err).toBe(50);
    }
  });
});

どの書き方も、一長一短な感じがあるので、私はこれで行くぜ!と決めてその書き方をマスターするのがいいのかもしれないな〜と思いました。

Laravel10 bladeでめっちゃ表示崩れると思ったらTailwindをインストールしてなかった

ことの発端

Laravelでページネーションを実装するためにbladeに以下のコードを書いたところ、表示がめっちゃ崩れました。

{{$items->links()}}

矢印でかっ

{{$items->links()}} で自動的に生成されるHTMLを見てみたら、 flex items-center ...などのclassが設定されていますが、これらが全く効いていない様子。

あ、Tailwindを手動で入れなきゃいけないのね、と気づいたのでインストールします。

LaravelにTailwindCSSをインストールする

Tailwindのドキュメントの通りにやるだけでした。

Install Tailwind CSS with Laravel - Tailwind CSS

まずはプロジェクト配下でnpmでインストール。

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.config.js が生成されるので、contentを設定する。(bladeでしか使う予定がないのでとりあえずbladeだけ)

  content: [
    "./resources/**/*.blade.php",
  ],

./resources/css/app.css にてtailwindを読み込む。

@tailwind base;
@tailwind components;
@tailwind utilities;

ローカルで npm run devコマンドを実行する。(LaravelはDockerで動かしていても、ローカルで実行すればよしなにやってくれるので問題なし。変更もリアルタイムで反映してくれる。)

bladeの <head> 内でapp.cssを読み込めばOK。

@vite('resources/css/app.css')

ページネーションがそれなりに表示されるようになりました。

表示が大人しくなった

ついでに、ページネーションのカスタマイズのためのビューを導入すると、何もしなくてもけっこういい感じの表示になりました!

このコマンドを打つだけ。resources/views/vendor/pagination/tailwind.blade.php が適用されるようになります。

php artisan vendor:publish --tag=laravel-pagination

いい感じのページネーション!

ちなみに…。

npm run build をすると publicフォルダにビルドファイルが生成されるので、ボリュームを共有してるDockerに反映されるのはすんなり理解できるのですが、 npm run dev で動かしているときはどういう仕組みでDockerと連携しているのかまだあまり理解できてないです🤔 npm run devのときは物理的なファイルではなくメモリ上に保持されるみたいですが…。今度Viteについて詳しく調べてみたいです!

Laravel 肥大化しがちなコントローラから分離できる処理のまとめ

現在、こちらの本で、よくわからないままなんとなく使っていたLaravelを学び直しています。

今までなんでもかんでもコントローラに書いて肥大化しがちだったので、この処理は切り離せるのか〜という学びのメモ。とりあえず作成方法と登録方法の概要だけ。

バリデーション関連

10.x バリデーション Laravel

フォームリクエス

バリデーション処理を分離する。

php artisan make:request HelloRequest

app/Http/Requests/HelloRequest.php が作成され、その中で検証ルールの設定やエラーメッセージのカスタマイズができる。 コントローラ側では、アクションに渡すRequestクラスの引数を変更するだけでよい。

    public function post(HelloRequest $request)
    {
        // 処理
    }
ルールオブジェクト

バリデーションのルールをカスタマイズする。

php artisan make:rule Myrule

app/Rules/Myrule.php が生成され、バリデーションルールを設定できる。 コントローラやフォームリクエストのバリデーションルール設定時に呼び出せる。

$rules = [
            'age' => ['numeric', new Myrule]
        ];

データベース操作

10.x Eloquentの準備 Laravel

モデルクラス

EloquentというORMでデータベースを操作する際のクラス。基本的にはこれを使ってDB操作した方がいい。

例: usersというテーブルを操作するためのモデルクラスを作成する

php artisan make:model User

User::all();などのようにモデルクラスのインスタンスを通してDBを操作する。モデルを使うことの利点の1つはスコープが使えること。

スコープクラス

モデルへのグローバルスコープの追加はクロージャでもできるが、汎用性の高い処理などではスコープクラスを作っておくと便利。

php artisan make:scope UserScope

app/Models/Scopes/UserScope.php 内の apply() にスコープを設定する。 モデルクラスでは以下のようにグローバルスコープが設定できる。

    protected static function boot()
    {
        parent::boot();
        static::addGlobalScope(new UserScope);
    }

本来はユースケースに分離とかもした方が良さそうだけど、未履修なので後ほど追記するかも。

コントローラー外の処理

ミドルウェア

10.x ミドルウェア Laravel

認証など、すべてのアクセス時に行いたい処理はミドルウェアに分離しておく。ミドルウェアはリクエストがコントローラのアクションに届く前or後に実行されるプログラム。

php artisan make:middleware HelloMiddleware

app/Http/Middleware/HelloMiddleware.php が生成される。 ルーティングの際にミドルウェアを指定するのが一般的。

Route::get('hello', 'App\Http\Controllers\HelloController@index')->middleware(HelloMiddleware::class);
ビューコンポーザ

10.x ビュー Laravel

ビューをレンダリングする際に自動的に実行される処理を設定する。複数のページで常に特定のデータが必要なときなどに効果的。コントローラから完全に見えないので処理の流れがわかりにくくなるかも。

サービスプロバイダを用意してその中に設定する。

php artisan make:provider HelloServiceProvider

app/Providers/HelloServiceProvider.php が生成されるので、boot()内にビューコンポーザを設定する。config/app.phpにサービスプロバイダを登録するのを忘れずに。

    public function boot(): void
    {
        View::composer('hello', 'App\Http\Composers\HelloComposer');  // クラスでもクロージャでも設定できる
    }

とりあえず今知ってるのはこのくらいかな…?また追記するかもです。