スタイル・エッジ技術ブログ

士業集客支援/コンサルティングのスタイル・エッジのエンジニアによるブログです。

【モダン開発 #4】モダン開発で工夫した8個のこと

generated by DALL-E3

はじめに

モダン開発連載もいよいよ最終回。わたくしneueがお届けいたします。

番外編の冒頭でも触れていた内容にはなりますが、設計思想とフレームワークの良いとこ取りをしようとすると、必然的に両方に即したアプローチが求められます。中には思想同士が競合する部分もあり、折衷案を出すことすら困難で取捨選択を迫られることもあります。

そういったアーキテクチャの不和を、開発者が極力意識せずにフレームワーク利用の延長線ぐらいの感覚でプロダクト自体の設計・実装を進めることができ、量産されていくコードも自然に一貫性を保たれるような開発者インターフェイスを提供するには、水面下で多くの工夫を凝らす必要がありました。

まだ改善の余地がある状態ではありますが、本記事ではモダン開発の下地に取り入れた工夫をご紹介いたします。似た立場で開発基盤の設計と格闘している方にとって、何かしらの参考になれば幸いです。

※本記事で扱われている言語はPHP、フレームワークはLaravelです

具体例の紹介

各概念における抽象クラスの作成

量産対象となる、フレームワークであらかじめ用意されているクラス(Controller, FormRequestなど)および、独自に追加したクラス(Entity, ValueObjectなど)ほぼ全てに、独自の抽象クラスを作成しました。

一貫して抽象クラスを継承させておくことで、以下のようなメリットを得ることができます。

  • あらかじめ全体共通のふるまいを定義できる
  • 後から全体共通のふるまいが必要になった際に、具象クラスの修正が不要
  • 各具象クラス及び生成されたインスタンスが何者であるかを外部から判定しやすくする
<?php
// 最初の時点は何のふるまいが存在していなくても…
abstract class BaseEntity {}

// あらかじめ継承しておけば、具象クラスの継承先を後から全て書き換える必要がなくなる
class User extends BaseEntity {}
<?php
// ユーザークラスがエンティティなのかを調べることができる
if (is_a(User::class, BaseEntity::class, true)) {}

// ユーザーインスタンスがエンティティのインスタンスなのかを調べることができる
$user = User::create(~~~);
if (is_a($user, BaseEntity::class)) {}

継承先からは継承元のふるまいを一部変更することはできても、打ち消しをすることはできないため、全体ではないが一部で共有されるふるまいは別の形で実現します。

  • 下位概念が対象範囲のケースは、上位概念を継承した下位概念の抽象クラスを作成し、そちらに定義
  • 個別の判断が必要なケースは、トレイトに定義
<?php
abstract class BaseEntity {
    // エンティティとしての共通のふるまい
}

abstract class BaseRootEntity extends BaseEntity {
    // 集約ルートエンティティとしての共通のふるまい
}

abstract class BaseLocalEntity extends BaseEntity {
    // ローカルエンティティとしての共通のふるまい
}
<?php
class User extends BaseModel
{
    // ふるまいをトレイトとして抽出し、対象となる具象クラス側でのみ参照する
    use SoftDeletesTrait;
    ︙
}

量産対象となる具象クラスの記述量を減らす

レイヤーを分割したことで、情報の受け渡しにおいて同じような情報を複数回記述する必要が出てきました。

例えば、レイヤー間でDTOに詰めたり取り出したりする工程であったり、同クラスの静的メソッド間での持ち回し(例:エンティティのファクトリーメソッドに渡ってきた引数一式をバリデーションやコンストラクターに渡す)など、個数が多くなりがちな属性周りにはこの問題が付いて回ります。

情報の一部を選択して渡すケースではさておき、全て(または一定条件に一致するもの)を渡すケースについては、列挙することが人為的ミスの温床にもなりかねません。
もっと簡素な書き方で実現できないかと頭を捻り続け、結果的には黒魔術に手を染めることになりました。

LaravelDataの活用

レイヤー間については、番外編で紹介したLaravelDataを基盤としたDTOによって解決しています。

ユーザー入力からユースケースへの情報受け渡しに記載した通り、独自の入力用メソッドさえ定義してしまえば、あらゆる形式(リクエストオブジェクトやJSON文字列など)から、スキーマとして定めたデータ構造に当てはめることができ、受け渡し先が要求する形式で取り出すことが可能です。

入出力ペアごとにDTOの抽象クラスを作り、入力と取り出し補助に関するロジックはそちらにカプセル化しました。

連想配列と引数のアンパックの活用

PHPではメソッドの呼び出し時に、配列やTraversableなオブジェクトを引数リストにアンパックできます。その際、添字配列の代わりに連想配列を渡すと、キー名と合致するパラメーターに対して引数が展開される仕様になっています。

<?php
function sum(int $foo, int $bar, int $baz): int {
    return $foo * $bar + $baz;
}

// 要素の順番の引数として渡されるので結果は`5`
sum(...[1, 2, 3]);

// 要素の順番は関係なく同じパラメータ名の引数として渡されるので結果は`7`
sum(...['baz' => 1, 'bar' => 2,'foo' => 3]);

// パラメータ名として存在しないものが含まれているので
// Error: Unknown named parameter $foobar
sum(...['foo' => 3, 'bar' => 2, 'baz' => 1, 'foobar' => 1]);

同クラス内でほぼ同じパラメーターを持つメソッドへの引数の受け渡しについては、わざわざデータオブジェクトを介在させる必要もないため、上記仕様の活用を起点に対応方法を考えました。

ちなみに、引数の単純な添字配列についてはfunc_get_args()で取得でき、呼び出し元と呼び出し先のパラメーターの数や順序が完全一致していれば、そのまま渡すだけで済んでしまう話ではあるのですが、多くのケースでは一部のみの受け渡しや、値を書き換えてから渡す必要がありました。

下記の課題を解決すれば採用が現実的であると判断し、PHPの機能を調査しました。

  • 自身に渡された引数とパラメーター名を使って連想配列を作れること
  • 自身に渡されなかった(省略された)引数は、デフォルト値に置き換わること
  • 呼び出し先メソッドのパラメーターに存在するキーのみに対象をフィルタリングできること(エラー防止)
  • 手動で一部の値の上書き・引数の追加ができること

リフレクションによる内部情報の利用(黒魔術)

リフレクションを使用することで、クラスや関数など多くの内部情報(型やプロパティ、コメントさえ)にアクセスできるようになるため、課題解決に必要だった以下の情報が入手できます。

  • 呼び出し元メソッドのパラメーター(名前、初期値)
  • 呼び出し先メソッドのパラメーター(指定した名前のものが存在するか)

以下の工程を踏むことで、目的のデータが作成可能になりました。

  1. 自身に渡された引数配列とパラメーター名配列をarray_combineして連想配列を作成
  2. 省略された引数はパラメーターの初期値で置き換え
  3. 呼び出し先メソッドのパラメーターに存在するキーの項目だけに絞り込み
  4. 上書き・項目追加用の連想配列をマージ

ただし、抽象クラス側に定義したこのメソッドを呼び出す際、引数と呼び出し元メソッドの情報を渡す必要が出てきてしまいました。

<?php
class User
{
    public static function create(〜〜〜): User
    {
        $arguments = func_get_args();

        // 引数配列と呼び出し元メソッド名を渡している(複数メソッドから呼び出したい場合)
        static::validateFromArguments($arguments, 'create');

        // 引数配列と値上書き用の連想配列を渡している(一部の値を上書きしたい場合)
        static::createInstanceFromArguments($arguments, [/* 上書き用連想配列 */]);
    }
}

バックトレースによる呼び出し元情報の参照(暗黒魔術)

PHPにはdebug_backtraceという、バックトレースを生成するエラー処理のための関数があります。
指定したスタックフレーム数まで、呼び出し元のメソッド・関数に関する情報を参照できます。

こちらを利用することで、以下の情報が入手できます。

  • 呼び出し元のクラス名・メソッド名
  • 呼び出し元のメソッドに渡された引数

これらの情報を利用することで、呼び出し元で引数配列を先に取得する必要もなくなり、呼び出し元のメソッドが複数(N:1)のケースにおいて、どのメソッドから呼び出したかという文字列情報も不要になりました。

<?php
static::validateFromArguments();

static::createInstanceFromArguments([/* 上書き用連想配列 */]);

使用にあたっては、スタックフレーム数の指定や、不必要な情報の参照を無効化するオプションを指定しないと、メモリの負荷が高くなってしまうため注意が必要です。

実際の使用にあたって

正直なところ、内部情報や実行時のトレース情報を利用することは、関数の純粋さが大きく失われてしまい抵抗感はあります。 ただし、Laravel内部でも使用されていることや、実装者視点で得られる恩恵があまりにも大きいことから、今回は用途限定で採用することにしました。

また、渡していないデータが暗黙的に参照されること自体も本来アンチパターンであり、コードを読んだだけでは挙動が想像できないため、ささやかな情報としてメソッド名にFromArgumentsといった接尾辞を含めることにしました。

ビジネスルール検証におけるValidatorの活用

DDDにおいて、ドメインオブジェクトが持つビジネスルールは、原則クラス内にカプセル化します。
インスタンスの作成や更新前に各属性に関するルールチェック、後に複数属性にまたがる整合性チェックを遅延バリデーションの形で行い、ルール違反が見つかった場合には例外をスローするといったアプローチが一般的かと思います。

従来パターンにおける課題

属性数が多くなったりビジネスルールが複雑になるにつれて、検証ロジックのコード量・分岐数の増加を招きます。
中でも、属性のルールチェックに関してはルール自体が汎用性が高いものであることが多いにも拘らず、通常の分岐を用いると行数が増加していってしまい、ルールが把握しづらくなってしまいます。

<?php
class User
{
    // 各属性のセッターでバリデートするとしたら…
    private function setAge(int $age): void
    {
        $errors = [];
        if (strlen($age) > 3) {
            $errors[] = 'age.maxDigits';
        }
        if ($age < 0) {
          $errors[] = 'age.positiveInteger';       
        }
        if ($age > 120) {
            $errors[] = 'age.max';
        }
        if (count($errors)) {
            throw new DomainValidateError($errors);
        }
        $this->age = $age;
    }
}

Validatorを用いる前提で宣言的に記述する方式

各属性に関するルールチェックはLaravelのValidatorに依存させる形で実装して、ルールはFormRequestのように宣言的に記述する方式を採用しました。
ルールを、配列を返す静的メソッドに切り出すことで、各属性の一般的なビジネスルールが一目瞭然になりました。

<?php
class BaseEntity
{
    protected static function validate(): bool
    {
        // create, updateXxxなどの呼び出し元に応じて、
        // 全ての属性または一部属性をValidatorを使ってバリデート
    }
}

class User extends BaseEntity
{
    // 型や必須判定は、ファクトリーメソッドのパラメーターの型宣言で担保されているため、こちらには含まない
    private static function rules(): array
    {
        return [
            'firstName' => ['max:80'],
            'lastName'  => ['max:80'],
            'age'       => ['positive_integer', 'max_digits:3', 'max:120'],
        ];
    }
}

オブジェクトが入れ子になっている場合の責務の所在

各オブジェクトで直接検証を行うのはプリミティブ型の属性に限定し、オブジェクト型が指定された属性の検証は、当該オブジェクト側の責務として、再帰的に検証される作りにしています。

例えば、年齢がAgeといった値オブジェクトとして実装されているのであれば、ルールの定義・検証はいずれもUserでなくAge側で行います。

<?php
class User extends BaseEntity
{
    // クラスのプロパティ(コンストラクターのパラメーターから昇格)で、インスタンスが持つ各属性の型を定義
    private function __construct(
        protected string $firstName,
        protected Age $age,
    ) {}

    // 一方ファクトリーメソッドのパラメーターは、ユースケースから渡されるプリミティブ値を受け入れる
    public static function create(
      string $firstName,
      int $age,
    ): User
    {
        // クラスのプロパティがプリミティブ型の属性のみチェック
        static::validate();
        // クラスのプロパティが固有オブジェクト型の属性は変換
        // ※内部的には当該クラスのファクトリーメソッドが呼び出され、ここと同様の判定・生成処理が行われる
        $convertedValues = static::convertObjects(['age' => $age]);

        // 引数の値とオブジェクトに変換された値を用いてインスタンスを作成
        $user = static::createInstance($convertedValues);
        // 作成されたインスタンスの整合性をチェック
        $user->validateIntegrity();

        // ルール違反がなければインスタンスが返却される
        return $user;
    }

    private static function rules(): array
    {
        return [
            // ユーザーの属性ルールとして定義するのはプリミティブ型である名前のみ
            'firstName' => ['max:80'],
        ];
    }
}

class Age extends BaseValueObject {
    public readonly int $value;

    private function __construct(int $age)
    {
        $this->value = $age;
    }

    public static function create(int $age): Age
    {
        // オブジェクト型の属性や整合性チェックを持たない末端のオブジェクトだとシンプルに
        static::validate();
        return static::createInstance();
    }

    private static function rules(): array
    {
        return [
            // 年齢として扱う数値に関するルールは、年齢オブジェクトのルールとして定義
            'age' => ['positive_integer', 'max_digits:3', 'max:120'],
        ];
    }
}

その他ポイント

Validatorを用いると、起点となるユーザーインターフェイス層で違反を扱う際に下記のようなメリットもあります。

  • 特別な工夫なしに各属性のルール違反をひとまとめにできる(ErrorBag
  • メッセージのフォーマットとして、Laravelのメッセージファイルを利用できる

また、サンプルコードにしれっと紛れ込んでいますが、Laravel標準のバリデーションルールには存在しないもので、ビジネスルールとして頻出するものについては拡張しています。(例:正の整数、カタカナのみなど)

今回触れなかった整合性チェックについては、クロージャ形式で混在させることも不可能ではないものの、視認性が高くはなく再利用性も低いため、従来のValidatorを使用しない記述方式で遅延バリデート*1することにしました。

フロントエンドバリデーションとの数値ルール共有

ユーザー体験向上のためには、即座に応答できるフロントエンド時点でも入力バリデーションを行う必要があります。

ビジネスルールとユーザー入力における制限が必ずしも一致するとは限りませんが、多くは連動しています。
ドメイン層とユーザーインターフェイス層で、同一の根拠を持つバリデーションルールを別々に保持することは、差異発生のリスクがあるため、ドメイン層にのみ情報を持ち、そちらに依存させることが望ましいでしょう。

ルールセットとしてフロントエンドと同期するには、TypeScriptの型やZodなどのスキーマの形式で取得可能にするのが1つの理想ではあります。
現時点では、個別で取り出したいケースがある点も考慮し、多重管理リスクの大きい数値系ルールのみを扱いやすい形式で切り出し、Controllerから取得可能にしました。

<?php
class User
{
    // 属性/内容別に数値形式で参照できるようにする
    public static function numericLimits(): array
    {
        return [
            'firstName' => [
                'max' => 80,
            ],
            'lastName' => [
                'max' => 80,
            ],
        ];
    }

    // 数値系ルールはバリデート時に自動変換する仕組みを作り、rulesには記述しない
    private static function rules(): array
    {
        return [
            'age' => ['positive_integer'],
        ];
    }
}

数値系ルールを構造化したオブジェクトをViewに渡すことで、属性値などとして個別参照が可能です。

<x-form-input
  type="tel"

  :maxlength="$numericLimits->user->phoneNumber->max"
/>

Eloquent Modelとの共存

Laravelのコアといっても過言ではないEloquent Modelは、包括的な概念でありつつ、Laravelが提供する多くの機能との関連性を持っています。 レイヤーを越境しやすい性質を持っていることから、DDDやレイヤードアーキテクチャといった設計思想における責務分離の考え方と、最も競合しやすい概念ではないでしょうか。

ORMとして同品質の概念を再開発する自信と時間はさすがにないため、使用自体は許容しつつRepositoryQueryServiceから情報を返す際に、ドメインオブジェクトに変換してから返却するルールにすることで、インフラストラクチャ層から外へはModelを原則露出させない方式を選択しています。

<?php
class UserRepository extends BaseRepository
{
    // クエリメソッドからModelをそのまま返却しない
    public function find(int $id): ?User
    {
        return UserModel::find($id)?->convertEntity();
    }
}

class UserModel extends BaseModel
{
    public function convertEntity(): User
    {
        // Modelのインスタンスを元にEntityのインスタンスを再構築
        return User::reconstruct(
            id:   $this->id,
            name: $this->name,
            ︙
        );
    }
}

Modelの責務としては

  • データベース操作の抽象化
  • データベース上とシステム上で扱うデータ形式の相互変換
  • リレーション先のデータ取得

に留めて、各層からは直接依存させないことで、あくまでORMとしての役割を担ってもらっています。

ユーティリティクラスなどのレイヤー外概念

プログラムで取り扱う概念の中には、問題領域・解決領域いずれも直接の関係はなく、開発そのものや非機能要件にまつわる課題を解決するための概念が存在します。

これらはいずれかのレイヤーに所属させても矛盾が発生することがあるため、切り離して考えることにしました。
(正確には、この領域から各レイヤーへの依存は許可せず、この領域に対する依存については制約を設けない)

情報・関数

システムによって変動することのない、再利用性の高い補助的な手続き(例:文字列加工、年号変換)については、ユーティリティクラスとして外部化し、Laravelのヘルパー同様、エイリアスとして登録することでどこからでも参照できるようにしました。

具体的な手続きに名前を付けて外部化することで、メインロジックをクリーンに保てるのと同時に、本来求められているふるまいが際立つことで、処理のアウトラインを追う目的でのコードリーディング負荷は大きく下げられます。

より良い手段が見つけられた際に仕組みの交換が容易になることもメリットに感じました。

<?php
// 処理全てをその場で書いてしまうと本筋が読みにくい
$originalAttributes = $request->attributes();
$replacedKeys = array_map('\Str::camel', array_keys($originalAttributes));
$attributes = array_combine($replacedKeys, $originalAttributes);

// keyをキャメルケースに変換した属性の配列が欲しいということが分かる
$attributes = \ArrayHelper::replaceKeys($request->attributes(), '\Str::camel');

内製パッケージ

今回のシステムに合わせて開発したものの、システムを選ばずに導入ができそうな機能(例:操作ログ)や、外部サービスとの連携の仕組み(例:Amazon TimestreamのORM)などについては、関連ファイルをあえて一纏めにした状態で管理しています。

異なるシステムが持つ要求の差異を吸収できるほどの汎用化を終えたら、社内向けのパッケージレジストリ上に管理を移行することを前提として、あらかじめ関連コードが分散しないよう関心事にフォーカスしたディレクトリ構成となっています。

また、動作に必要な実装をシステム側で管理する場合は、必ずインターフェイスを提供することで、規格を分かりやすく実行前から基準を満たしているかを確認できるようにしています。

App\Support\Packages\OperationalLogger
├── OperationalLogger.php
├── OperationalLoggerRepositoryInterface.php
├── Http
│   └── Middleware
│       └── RecordOperationalLogs.php
└── Providers
    └── OperationalLoggerServiceProvider.php

文字列フォーマット・設定値

前述の2つとは性質が異なりますが、同じくレイヤー外で扱う情報はあります。

画面に表示する各種メッセージの文字列フォーマット、データベースで管理するほどではない挙動制御のための設定値については、Laravelの設計に則ってlang, config配下に独自の定義を追加することで、一元管理による一貫性の維持とハードコーディング防止に努めています。

<?php
// 引数に使用する値を設定から参照
formatDate($date, config('date.formate.datetime'));

class ItemNotFoundException extends Exception
{
    public function __construct($itemName, $code = 0, $previous = null)
    {
        // 例外のメッセージを動的に生成
        parent::__construct(trans('exception.item_not_found', ['item' => $itemName]), $code, $previous);
    }
}

課題として、config()で返却される値について型の保証が標準では難しいため、エイリアス関数を用意してそちらで返却時の型情報を補完するか、各設定にあるPHPDocの情報を参照してヒントを与えるような、PHPStanの拡張機能を自作する必要性を感じています。

Make系コンソールコマンドの充実

DDD・レイヤードアーキテクチャに則って開発していることもあり、従来のLaravelに比べて開発時に扱う概念(クラス)の種類は多くなっています。

独自クラス(エンティティ、目的別DTOなど)は、Web上に情報が掲載されていないため、

  • 必要プロパティの示唆
  • 定義するメソッドのサンプル
  • 取り扱いにあたっての注意点

といった情報を目に留まる場所に記述して、正しく扱えるようにする必要があるのと、
既存クラスについても、

  • 名前空間の変更
  • 継承元の抽象クラスの独自化
  • コメントの日本語化・PHPDocにおける型定義の詳細化

といった調整箇所が存在します。

Laravelには、各種クラスのファイルを生成するためのArtisanコマンドが整備されており、その中でファイルの雛形(スタブという)も管理されているため、仕組みに乗る形でパターン別の対応を行いました。

  • 既存クラス
    • 静的文字列の変更のみ
    • 引数やオプションの拡張、名前空間の変更、動的文字列の変更が必要
      • スタブファイルの編集
      • 対象のMakeCommandを継承したコマンドを作成・処理の上書き
  • 新規クラス

また、他コマンドからのコマンド呼び出しを活用することで、特定のクラスと実質セットで作成することになるインターフェイスやユニットテストを同時作成するようなオプションを拡張し、作業工程の短縮を図っています。

例:

# User集約内にRelatedPersonというローカルエンティティとそれに紐づく
# エンティティコレクション(エンティティのリストをラップしたクラス)を生成
php artisan make:entity User/RelatedPerson -lc

# Actionとそれに紐づく入力用DTO、ユニットテストを生成
php artisan make:action User/Get -dt

今回は作成しませんでしたが、フローチャート次第で使用スタブが変わるようなケースでは、オプションの組み合わせで判断するのではなく、対話式のインターフェイスにしてみると無効な組み合わせパターンを防ぐことができそうです。

仕組みが整っていない場合にはよくある、類似ファイルを複製して調整を掛ける運用では、記述の過不足が発生するリスクがあるため、スタブファイルをバージョン管理し、基本ルールに変更が生じた場合はそちらにも修正を加える運用が安全です。

静的解析・リントツールの整備

コード品質の維持には、一貫性を守るためのルール整備が必要です。
しかし、ルールを人力だけで徹底しようとすると、かえって作業効率が低下したりレビュー工数が肥大化したりすることが予想されるため、可能な限りルール違反は静的解析で検出できるようにしています。

PHPStanの拡張機能は、Laravel組み込みクラスへの対応としてlarastanを、レイヤードアーキテクチャにおける正しくない方向への依存発生、所定の抽象クラスを継承していない具象クラスの検出に、phpatを活用しています。

また、独自クラスにおける必須指定などの開発時に遵守すべきルール・制約についてもこの層でカバーします。
専用のPHPStan拡張機能を作成することで、実行時に検証を挟まずとも早期検出ができるため、ロジック側に防御的なコードが氾濫することを防止できます。

エディター・CI上でチェックを通過してからレビューに進むフローにすることで、レビュアーの負荷も低下します。

今後の計画

記述量への対策

記述量が多い部分については、まだ全てが解消には至っておらずボトルネックとなっています。
しかし、レイヤーを分割し然るべき粒度で概念を分けるとなると、元の状態に比べてある程度増えることは当然とも感じます。

記述量を減らすといったアプローチだけではなく、1つのクラスの内容を元に関連クラスの内容を生成するジェネレーター・コンバーターの開発であったり、PHPDocの型情報とパラメーターなどの重複部分についてはAIの力を借りて自動補完できるようにするなど、まだ試せていないアプローチもこれからトライしていこうと思います。

自動テスト関連の強化・ハードル低下

自動テストに関しては、前回の記事にもある通り、依存の中心に関するユニットテストに留まっており万全とは言えない状況です。

ユニットテストだけではカバーができていない箇所について、統合テストやE2Eテストを導入できるよう、テスト実装の共通処理を抽象化するなどして実装ハードルの低下を目指しつつ、カバレッジについても基準等を検討していければと考えています。

DevContainerの導入

環境構築の時間を短縮しつつ、設定が近い状態でチーム開発ができるようにするため、ローカル環境の動作に必要なDocker Composeの定義や、VS Codeの設定ファイル雛形・推奨拡張機能の定義をバージョン管理対象に含めて運用しています。
しかし、実際に0から環境構築を行い開発に着手できるようになるまで、一部工程については手順書が必要な状況です。

DevContainer化することで、コマンド一発でエディターのセットアップも含めた開発環境を立ち上げられるようにし、最終的にはGitHub Codespacesを活用して、レビュアーは自身のローカルを汚さずともレビュイーの作業環境をチェックできるような状態を作れるようにしたいと思います。

おわりに

断片的な紹介ではあるものの、気付けばなかなかのボリュームになっていました。
まだ改善の余地が多い熟成度なので、今後も開発現場からのフィードバックに耳をしっかりと向けながら、思考と選択を繰り返していこうと思います。

また、冒頭でもお伝えしましたが、モダン開発連載は今回で終了となります。
振り返ってみると想像以上にチームのキャパシティを超えた挑戦にはなり、一部は方針転換をすることにはなりましたが、プロジェクト開始前に比べて設計に対する捉え方・考え方が大きく成長したメンバーが複数名いたことは、弊社にとっても大きな実りになったと感じています。

仕組みの構築という支援的なポジションではありましたが、その一助となれたことは、個人的に良い経験となりました。
限られたリソースの中でも、こういった投資的な取り組みが持続できる組織でありたいと切に思います。

一緒に探求してみたい方、もっと良い方法がある…と左手が疼いている方、ぜひ一度お話だけでも聞きにきていただけたら幸いです。 recruit.styleedge.co.jp

*1:CHECKS パターン・ランゲージにおけるバリデート手法の一つ。複雑性が高い検証は極力後回しにするという考え方。