Upgrade to Pro — share decks privately, control downloads, hide ads and more …

readonly class で作る堅牢なアプリケーション

shogogg
October 10, 2023

readonly class で作る堅牢なアプリケーション

2023年10月8日に東京で行われた PHP Conference 2023 の登壇資料です。

shogogg

October 10, 2023
Tweet

Transcript

  1. readonly class で作る
    堅牢なアプリケーション
    Oct. 8 2023
    PHP Conference Japan 2023
    河瀨 翔吾 / @shogogg

    View Slide

  2. 河瀨 翔吾
    ユースタイルラボラトリー(株)ソフトウェア開発部テックリード
    シンアジャイルコミュニティ運営
    好きな⾔葉
    型安全 / アジャイル
    好きなゲーム
    ボドゲ / マリオカート / フロムゲー(最近だとAC6)
    好きなアイドル
    ももいろクローバーZ
    ⾃⼰紹介
    shogogg
    shogogg

    View Slide

  3. ユースタイルラボラトリー株式会社
    PHP Conference Japan 2023 Bronze Sponser
    主な事業内容
    訪問介護事業
    MISSION
    すべての必要な⼈に、 必要なケアを届ける。
    エンジニアのお仕事
    社内向け業務システムの開発‧保守
    福祉業界向けソフトウェアの開発‧保守
    会社紹介

    View Slide

  4. #phpcon #track4

    View Slide

  5. 今⽇お話すること
    ● readonly class とは?
    ● readonly class をなぜ使うのか
    ● readonly class を使う場合のつらさ

    View Slide

  6. readonly class とは?

    View Slide

  7. readonly class?
    ● PHP 8.2 で登場した読み取り専⽤のクラス
    ● すべてのプロパティが readonly property となる

    View Slide

  8. readonly property?
    ● PHP 8.1 で登場した読み取り専⽤のプロパティ
    ● コンストラクタでのみ値が設定できる
    ● ⼀度コンストラクタで設定した値から変更ができない

    View Slide

  9. final class Momoclo
    {
    public readonly string $name;
    public readonly string $color;
    public readonly int $age;
    public function __construct(string $name, string $color, int $age)
    {
    $this->name = $name;
    $this->color = $color;
    $this->age = $age;
    }
    }
    readonly property

    View Slide

  10. final class Momoclo
    {
    public function __construct(
    public readonly string $name,
    public readonly string $color,
    public readonly int $age,
    ) {
    }
    }
    readonly property

    View Slide

  11. final class Momoclo
    {
    public function __construct(
    public readonly string $name,
    public readonly string $color,
    public readonly int $age,
    ) {
    }
    }
    $reni = new Momoclo('高城れに', '紫', 30);
    $reni->color = '赤';
    // => PHP Fatal Error: Uncaught Error: Cannot modify readonly property Momoclo::$color
    readonly property

    View Slide

  12. プロパティが増えると……
    final class User
    {
    public function __construct(
    public readonly int $id,
    public readonly string $familyName,
    public readonly string $givenName,
    public readonly EmailAddress $email,
    public readonly DateTime $birthday,
    public readonly string $postcode,
    public readonly Prefecture $prefecture,
    public readonly string $city,
    public readonly string $street,
    public readonly string $room,
    public readonly PhoneNumber $tel,
    public readonly bool $isActive,
    ...

    View Slide

  13. readonly class
    final readonly class User
    {
    public function __construct(
    public int $id,
    public string $familyName,
    public string $givenName,
    public EmailAddress $email,
    public DateTime $birthday,
    public string $postcode,
    public Prefecture $prefecture,
    public string $city,
    public string $street,
    public string $room,
    public PhoneNumber $tel,
    public bool $isActive,
    ...

    View Slide

  14. readonly class をなぜ使うのか

    View Slide

  15. A. Immutable だから

    View Slide

  16. Immutable?
    ● ⼀度⽣成されたら、その状態を変更(mutate)できない
    特性のこと。不変。
    ● 逆に⽣成後に状態を変更できることを Mutable(可変)と
    表現する。

    View Slide

  17. // readonly ではない Momoclo クラスのインスタンス
    $shiori = new Momoclo(
    name: '玉井詩織',
    color: '黄色', // <- 黄色を指定したはずなのに……
    age: 28,
    );
    // Momoclo 型のオブジェクトを受け取って何かをするメソッドを呼び出すと……
    MomocloService::doSomething($shiori);
    // なぜか引数に渡したオブジェクトの値が書き換わっている!
    echo $shiori->color; // => ピンク
    Mutable なオブジェクトで困ってしまう例

    View Slide

  18. use Carbon\Carbon;
    $today = Carbon::create(2023, 10, 8);
    $tomorrow = $today->addDays(1);
    echo $today->toDateString(); // => 2023-10-09
    Carbon を使った例

    View Slide

  19. $x = 8;
    $y = $x + 1;
    echo $x; // => 8
    // こうはならない
    echo $x; // => 9
    整数 を使った例

    View Slide

  20. use Carbon\CarbonImmutable;
    $today = CarbonImmutable::create(2023, 10, 8);
    $tomorrow = $today->addDays(1);
    echo $today->toDateString(); // => 2023-10-08
    echo $tomorrow->toDateString(); // => 2023-10-09
    CarbonImmutable を使えば解決!

    View Slide

  21. class CarbonImmutable extends DateTimeImmutable implements CarbonInterface
    {
    // (略)
    }
    CarbonImmutable の定義
    PHP 標準の DateTimeImmutable を継承している

    View Slide

  22. DateTime と DateTimeImmutable
    引用:https://www.php.net/manual/ja/class.datetime.php

    View Slide

  23. Mutable だと……
    ● オブジェクトの状態(値)が変えられる、ということは、いつどこで
    書き換えられてもおかしくない、ということ。
    ● いつの間にか書き換えられた結果として不具合が⽣じると、どこで書
    き換えられているのかを探したり、その不具合を解消するのも⼀苦
    労。
    ● 安全なコードを書くためには、どの処理に副作⽤があるのか把握した
    り、確認しながらコードを書いていく必要がある。

    View Slide

  24. Immutable なら……
    ● オブジェクトの状態(値)が変えられることがないため、⼀度⽣成し
    たオブジェクトは必ず同じ値を持ち続ける。
    ● ⽬の前のコードを読むだけで、オブジェクトの状態(値)がどうなっ
    ているかを把握できるため、物事がシンプルになる。
    ● 考えることが減るため、安全にコードの読み書きに集中することがで
    きる。

    View Slide

  25. Q. readonly class をなぜ使うのか

    View Slide

  26. A. Immutable だから
    ➡ 複雑さを減らし、シンプルにすることで
      ⽬の前の課題に安⼼して集中するため

    View Slide

  27. readonly class をいつ使うのか
    ● 中〜⼤規模アプリケーションの開発であり、ビジネスロジック
    が複雑になる(なりそうな)場合にデータを表現するクラスと
    して⽤いると効果は⼤きい。
    ● 逆に⼩規模であったり、リクエストに応じた単純な CRUD 操作
    が主体のアプリケーションなどにおいて、(無理をして)導⼊
    する必要性は低いと感じる。

    View Slide

  28. 例えば介護報酬の請求
    ● 医療同様、費⽤は⼀部のみを利⽤者さんに請求し、残りは保険
    者‧⾃治体に請求する必要がある。
    ● サービスに応じて点数を計算し、保険者や⾃治体に請求する仕
    組みも医療とほぼ同じ。
    ● ⾼齢者向けの介護と障がい者向けの介護で制度が全く異なる。
    ● 制度の内容や点数の計算ルールが提供サービスによって⼤きく
    異なる。

    View Slide

  29. 介護サービスの種類(⾼齢者向け)
    1. 訪問介護
    2. 訪問⼊浴介護
    3. 訪問看護
    4. 訪問リハビリテーション
    5. 通所介護(デイサービス)
    6. 通所リハビリテーション
    7. 福祉⽤具貸与
    8. 短期⼊所⽣活介護(ショートステイ)
    9. 短期⼊所療養介護(介護⽼⼈保健施設)
    10. 短期⼊所療養介護(介護療養型医療施設等)
    11. 居宅療養管理指導
    12. 夜間対応型訪問介護
    13. 認知症対応型通所介護
    14. 定期巡回‧随時対応型訪問介護看護
    15. 複合型サービス(看護⼩規模多機能型居宅介護)
    16. 特定施設入居者生活介護
    17. 特定施設入居者生活介護(短期利用型)
    18. 地域密着型特定施設入居者生活介護
    19. 地域密着型特定施設入居者生活介護(短期利用型)
    20. 認知症対応型共同生活介護(グループホーム)
    21. 認知症対応型共同生活介護(短期利用型)
    22. 特定福祉用具販売
    23. 住宅改修
    24. 介護福祉施設(特別養護老人ホーム)
    25. 介護保険施設
    26. 介護療養施設
    27. 地域密着型介護福祉施設入所者生活介護
    28. 特定入所者介護サービス等
    29. 地域密着型通所介護
    30. 居宅介護支援
    ※読み飛ばし推奨

    View Slide

  30. 介護サービスの種類(障がい者向け)
    1. 居宅介護
    2. 重度訪問介護
    3. 同⾏援護
    4. ⾏動援護
    5. 療養介護
    6. ⽣活介護
    7. 経過的⽣活介護
    8. 短期⼊所(ショートステイ)
    9. 重度障害者等包括⽀援
    10. 施設⼊所⽀援
    11. 経過的施設⼊所⽀援
    12. 機能訓練
    13. ⽣活訓練
    14. 宿泊型⾃⽴訓練
    15. 就労移⾏⽀援
    16. 就労移⾏⽀援(養成)
    17. 就労継続支援A型
    18. 就労継続支援B型
    19. 就労定着支援
    20. 自立生活援助
    21. 共同生活援助(グループホーム)
    22. 計画相談支援
    23. 障害児相談支援
    24. 地域相談支援(地域移行支援)
    25. 地域相談支援(地域定着支援)
    26. 福祉型障害児入所施設
    27. 医療型障害児入所施設
    28. 児童発達支援
    29. 医療型児童発達支援
    30. 放課後等デイサービス
    31. 居宅訪問児童支援
    32. 保育所等訪問支援
    ※読み飛ばし推奨

    View Slide

  31. 例えば介護報酬の請求
    ● 請求に関するオブジェクトを⽣成するためのビジネスロジック
    が⾮常に複雑。そのコードが数千⾏に及ぶ場合もある。
    ● 制度の内容や点数の計算ルールが提供サービスによって⼤きく
    異なり、それぞれのサービスごとに必要。
    ● ビジネスロジックが⾮常に複雑なのに、さらに Mutable なオブ
    ジェクトの複雑性を持ち込みたくない。
    ➡ Immutable なオブジェクト = readonly class が有効!

    View Slide

  32. readonly class を使う場合のつらさ

    View Slide

  33. readonly class を使う場合のつらさ
    ● ORM からの変換が⾯倒くさい
    ● 配列を使ったシンプルなキャッシュと相性が悪い
    ● ⼀部の値を変更したインスタンスを得るのが⾯倒くさい

    View Slide

  34. use Illuminate\Database\Eloquent\Model;
    final readonly class Item
    {
    public function __construct(
    public string $name,
    public int $price,
    ) {
    }
    }
    final class ItemRecord extends Model
    {
    // 略
    }
    $record = ItemRecord::find(id: 17);
    $item = new Item(
    id: $record->id,
    price: $record->price,
    );
    ORM からの変換

    View Slide

  35. use Illuminate\Database\Eloquent\Model;
    final class ItemRecord extends Model
    {
    // 略
    // ドメインモデルに変換するメソッド
    public function toDomain(): Item
    {
    return new Item(
    id: $record->id,
    price: $record->price,
    );
    }
    }
    $item = ItemRecord::find(id: 17)->toDomain();
    解決策①:ドメインモデルへの変換メソッド

    View Slide

  36. use Illuminate\Database\Eloquent\Model;
    final class ItemRecord extends Model
    {
    // 略
    }
    final class ItemRepository
    {
    public function lookup(int $id): Item
    {
    return ItemRecord::find(id: 17)->toDomain();
    }
    }
    解決策②:リポジトリパターン

    View Slide

  37. // 注:このコードは動作しない
    final readonly class Foo
    {
    private array $cache = [];
    // 略
    public function getSomething(int $arg): int
    {
    if (!isset($this->cache[$arg])) {
    $this->cache[$arg] = $this->computeSomething($arg);
    }
    return $this->cache[$arg];
    }
    // 引数に対応して何かを計算して返すメソッド
    private function computeSomething(int $arg): int
    {
    return $arg ** 2;
    }
    }
    配列を使ったシンプルなキャッシュと相性が悪い

    View Slide

  38. use \Illuminate\Contracts\Cache\Repository as CacheRepository;
    final readonly class Foo
    {
    private CacheRepository $cache;
    public function _construct()
    {
    $this->cache = Cache::store('array');
    }
    public function getSomething(int $arg): int
    {
    return $this->cache->remenber($arg, 86400, function (int $arg): int {
    // 引数に対応して何かを計算して返す処理
    return $arg ** 2;
    });
    }
    }
    解決策①:素直にフレームワークに頼る

    View Slide

  39. final readonly class Foo
    {
    // キャッシュを保存・読み出ししてくれるクラスを独自に用意して利用する
    private CacheManager $cache;
    // 略
    public function getSomething(int $arg): int
    {
    return $this->cache->getOrElse($arg, function (int $arg): int {
    // 引数に対応して何かを計算して返す処理
    return $arg ** 2;
    });
    }
    }
    解決策②:キャッシュ専⽤のクラスを⽤意する

    View Slide

  40. final readonly class Point
    {
    public function __construct(public int $x, public int $y)
    {
    }
    }
    $a = new Point(x: 3, y: 5);
    // $a と X 座標は同じで Y 座標が異なる Point オブジェクトを作成したい
    $b = new Point(x: $a->x, y: 7);
    ⼀部の値を変更したインスタンスを得る

    View Slide

  41. final readonly class Person
    {
    public function __construct(
    public int $id,
    public string $familyName,
    public string $givenName,
    public int $age,
    // ...
    ) {
    }
    function aging(Person $person): Person
    {
    return new Person(
    id: $person->id,
    familyName: $person->familyName,
    givenName: $person->givenName,
    age: $person->age + 1,
    // ...
    );
    }
    }
    プロパティが多いと⾯倒だし読みにくい

    View Slide

  42. // Scala: case class
    case class Point(x: Int, y: Int)
    val a = Point(x = 3, y = 5)
    val b = a.copy(y = 7)
    // Kotlin: data class
    data class Point(val x: Int, val y: Int)
    val a = Point(x = 3, y = 5)
    val b = a.copy(y = 7)
    // Java 16 以降: Record
    public record Point(int x, int y) {}
    他の⾔語の場合

    View Slide

  43. abstract readonly class Model
    {
    public function copy(...$values): self
    {
    return call_user_func_array([new ReflectionClass(self::class), 'newInstance'], [
    ...get_object_vars($this),
    ...$values,
    ]);
    }
    }
    final readonly class Point extends Model
    {
    // 略
    }
    $a = new Point(x: 3, y: 5);
    $b = $a->copy(y: 7);
    解決策①:⾃家製 copy メソッドを実装する

    View Slide

  44. use Spatie\Cloneable\Cloneable;
    final readonly class Point
    {
    use Cloneable;
    public function __construct(public int $x, public int $y)
    {
    }
    }
    $a = new Point(x: 3, y: 5);
    $b = $a->with(y: 7);
    解決策②:spatie/php-cloneable を使う

    View Slide

  45. 解決策③:withHoge メソッドを実装する
    use Spatie\Cloneable\Cloneable;
    final readonly class Item
    {
    use Cloneable;
    public function __construct(
    public string $name,
    public int $price,
    public Carbon $createdAt,
    public Carbon $updatedAt,
    ) {
    }
    public function withPrice(int $price): self
    {
    return $this->with(price: $price, updatedAt: Carbon::now());
    }
    }

    View Slide

  46. まとめ

    View Slide

  47. まとめ
    ● readonly class の登場によって、PHP でもお⼿軽、かつ安全に
    に不変(Immutable)オブジェクトを扱うことができるように
    なった。
    ● 複雑なビジネスロジックを扱うアプリケーションにおいては、
    Immutable なオブジェクトを⽤いることでコードの保守性の
    向上が期待できる。
    ● かゆいところに⼿が届くとまでは⾔えないが、⼗分に実⽤的。

    View Slide

  48. Thank you!

    View Slide