Laravel大規模開発入門!MVC分離のFatModel問題に対する責任分離と依存管理、その設計と考え方について
ナイル株式会社メディアテクノロジー事業本部の工藤さんにMVC分離のFatModel問題に対する責任分離と依存管理、その設計と考え方について解説いただきました。
こんにちは、ナイル株式会社メディアテクノロジー事業本部で開発マネージャをしています工藤@ta99toです。
今回は大規模で複雑度の高い開発をMVCフレームワークベースで構築する際に僕が課題と捉えているポイントやその具体的な解決手法について解説させていただきたいと思います。
- 「MVC以上の責任分離イメージがつかないよ!」
- 「DDDとかクリーンとかオニオンとかあのへんの設計パターンの導入モチベーションが不明」
- 「どうやっても最終的には複雑になって追加開発や修正開発が怖い状態になっちゃう」
↑このような悩みを持った方に対して本質的な課題の構造に対する理解が深まったり、その解決手法を提案するようなエントリーとなれば良いなと考えています。 上記に少しでも共感のあったwebエンジニアの方はぜひ最後まで目を通してみてくださいね。
また、エンジニアの方でなくてもなんとなくわかったような気になれる表現を目指しますので業務上よくエンジニアと関わるよという方もぜひ読んでみてください!
概要
システム開発はその規模が大きくなればなるほど検知しにくい技術的な課題を内部に累積しやすくなってしまいます。
そのようないわゆる「技術的な負債」を開発プロジェクトが抱えてしまう現象の主な要因の1つとして、自分たちの実装したプログラムの1つ1つに対して適切に責任や役割を与えたり分担させたりするような責任分離、依存管理のコントロールができていないということが挙げられます。
規模によって適切な責任分担、役割分担の粒度は変わるのにそれに対応する設計手段、実装手段を持たずにいると要件とスキルのミスマッチが発生し、そのギャップが様々な課題を引き起こす要因となってしまいます。
こうしたシステムの要件、規模の増大が引き起こす複雑さの解決に挑戦してきた先人たちの歩み、アウトプットが各種設計アーキテクチャであり、オブジェクト指向です。
近年のweb開発ではMVCフレームワークを利用した開発が一般的になっていて小さなアプリケーションであれば特に難しいことを考えなくても動く物が実装可能になっているのですが、中規模以上のものを流動的な要件に対応しやすい状態を保ち運用開発していくことを実現しようとすると先述したエンジニアリングスキルが欠けた状態では対応が難しい場合が往々にしてあります。
本稿ではそのようなシステム開発の複雑さを引き起こす課題の真因について考察し、解決の具体的な手法や設計アーキテクチャのさわりの部分を簡単に紹介したいと思います。
LaravelなどMVCフレームワーク開発の抱える課題
課題の話をする上で理解が必要なフレームワークやORM、MVC分離など用語の説明をはじめにざっくりとしてから内容に入っていきたいと思います。
フレームワーク、ORM概要
めぼしいweb開発のフレームワークには大体このORM(Object Relational Mapping)と呼ばれる仕組みが用意されています。
名前の通りデータベースの1テーブル1テーブルと対になるようクラス(Object)を用意し、フレームワークで実装済みの処理を継承すればすぐにデータベースに対して読み書きを行う処理を実装可能になるというものです。
// BlogPostというクラス名を内部でblog_postsという文字列に変換してテーブルを探しに行きます
class BlogPost extends Model
{
}
// ブログ投稿を全て取得します
$allBlogPosts = BlogPostModel::all();
上記のようにRDBでサポートされているデータベースであればSQLを一切記述せずにデータベースとのやりとりを処理できます。
こうしたフレームワークが用意してくれるアセットを活用することで開発工数を短縮できるというのがフレームワーク導入の1つの大きなモチベーションです。
データベースとのやり取りを行う処理が抽象化されており、プログラマはallという関数の奥で実際どんな処理が実行されているのか知らずとも実現したい処理を実装することができるようになりました。
こうした「開発コストを下げるような仕組み」と、「MVC分離のルール」を与えてくれるのがフレームワークです。
MVC分離とは
webサービスやモバイルアプリなどのUIが存在するアプリケーション開発において、
- 見た目(View)
- ユーザ入力の解釈、処理の実行(Controller)
- ビジネスロジック(Model)
の3つに分けて開発することで保守性、運用性を担保しようとする設計方針の1種です。
ビジネスロジックというのは例えばTwitterのようなSNSのシステムで言うなら
- 「ツイートはリツイートできる」
- 「ツイート内容にメンション付いてたら該当ユーザに通知飛ばす」
などのシステムの中心に存在するデータ構造とその振る舞いに関する実装のことです。
MVCフレームワークの功罪
という訳で、肝心なのはモジュール間の責任分離や依存管理をうまくやりくりする事であって、MVCとはそのための手段、概念の1つです。
ところがMVCフレームワーク繁栄の結果としてこの辺の勘所を抑えないままとりあえずMVCで分けるということだけが広く浸透しました。
結果としてFatControllerやFatModelと呼ばれる、ControllerやModelの1クラスに数百行もの処理を書いてしまう実装が量産され一時期話題になったこともあります。
- データ構造や処理フローをオブジェクトで表現するというオブジェクト指向プログラミングの基礎
- ソースコードの品質を維持して実装を拡張していくにはどうすればいいか
このような基礎が無いままMVCフレームワークというツールだけが広まっていったことで、Modelにはデータベースとのやり取りに関する情報が溢れて本質的なデータ構造やビジネスロジックが霞み、Controlerには漏れ出したビジネスロジックや分岐がだらだらと書かれてシェルスクリプトかな状態。
こうした残念な品質を負債として抱えたプロダクトがすごく増えました。
一方でこうした「フレームワーク」と言う形で提供された開発ワークフローはweb開発のハードルを大幅に引き下げ、実に多くのプロダクトの誕生に世界中で貢献しています。
そういう意味では数多あるOSSプロダクトの中でも「フレームワーク」と言うカテゴリが実世界に及ぼした益は非常に大きなものがあります。
課題の正体
MVCフレームワークを扱えるwebエンジニアはたくさんいるんですが、モジュール間の適切な責任分離、依存管理、情報設計を行い事業のスケールに合わせてシステムを安心、安全に拡張、拡大していける仕組みづくり、旗振りをできるエンジニアがあまりいないのです。
そんな状態で進める開発プロジェクトは以下のような課題を抱えます。
- 再利用性が低く開発が進んでも進んでも楽にならない。コードの増加は複雑度の増加に比例しどんどん開発スピードが鈍化する。
- 依存管理の質が低くあっちを直せばこっちが壊れる。逆に1箇所直せば済むような修正のはずがあちこちいじる必要がある。
- 情報設計の質が低くデータベースの正規化がうまくいってない。不自然なUI、不自然なデータ構造。
- 理想形のイメージがないのでコードレビューや設計レビューで突っ込むことなくてレビュー体制が形骸化してる。
- ソースコードの見通しが悪くキャッチアップするのに時間がかかる。
理想は以下のような状態をつくることです。
- 再利用性が高く開発が進むほど安全で便利なモジュールが増えて新規開発や修正が楽になり開発スピードが上がる。
- 依存管理の質が高く改修の影響範囲が明確で1箇所直せば必要な箇所は全て直る。
- 情報設計の質が高くデータベースは適切に正規化、必要なところは冗長化されている。自然なUI、自然なデータ構造。
- 理想形のイメージがありそれに沿わない設計や実装を拒否、改善する仕組みとしてレビューがワークしている。
- ソースコードの見通しが良くキャッチアップに時間がかからない。
こうした理想状態の実現を妨げる課題は何でしょうか。
それはどちらかと言えば
「プログラミングの難しさ」や「開発プロジェクトマネジメントの難しさ」
と言うよりは
「自分ではない何かの集合(人ないしプログラム)に適切に責任や役割を与えて代わりに仕事をしてもらうことの難しさ」
だと僕は考えています。
システムを組織として見た時、従業員としてのソースコードを制御する仕組み
会社組織における未熟と成熟
会社組織というのは良くできた仕組みで、適切に責任分離、目標設定、役割分担された組織というのは細かい指示や管理が無くても目標に対して個人個人が有機的に動き協調しあって成果をあげます。
反対にそうでない組織、責任範囲が曖昧だったり役割が重複していたりなど設計に問題のある組織においては途端に成果をあげるどころかトラブルばかり、組織や人に起因する問題解決に終始してしまい事業成長どころではありません。
更にはこうした人や組織構造に起因する課題は見えないところでゆっくりと進むため、目に見えるほど不具合を蓄積してそれを検知した時点で課題のサイズは既に大きく複雑に膨らんでいて解決するのに時間を要します。
システム開発における未熟と成熟
システム開発においても同様にうまく責任分離、依存管理がされていない実装同士がうまく協調することは難しく、複数人で開発しているなら尚更、そのような状況でプログラマAとプログラマBの実装がうまく協調して成果をあげると言うのは難しいわけです。
こうした不協和音を検知できず放置した結果が故障率や開発スピードの低下といったビジネスマネージャらの目にも見えるほどの影響を及ぼし出す頃にはもう既に手遅れ、システムという名の組織は既に壊死しており部分最適でなんとかなる状況ではなくなっています。
反対に、綺麗に設計されたシステムの上でなら実装と実装は開発者も驚くほど美しく繋がり、連携し、成果をあげるものです。
結果の差異を生むもの
あなたが綺麗に設計された組織にいる場合、あなたに任された責任や権限、目標は明確であり、ある日何かの業務に対応しようとした時にその業務が依存する部署、連携の必要なメンバーは明らかであり、迷って動けないと言うことはないでしょう。
例えば「新しいツールを導入したい」と思った時、それは
- 稟議の作成
- 上司の承認
- 法務のリーガルチェック
の3つに依存することが明確で、その通りにすればやりたいことが達成できると言うことが明らかです。
- あなたはプロジェクト内で任された責任を全うするために稟議を作成、提案する権限を持っている。
- あなたの上司は予算管理とプロジェクト目標達成の責任を全うするために提案された内容を承認したり差し戻す権限を持っている。
- 法務は組織にリスクのあるツールを導入させないという責任を果たすためリーガルチェックを実行する権限を持っている。
こうした会社組織内におけるチームとチーム、人と人の自立的で安全な連携を可能にしているのが
- チームやロールの責任範囲と権限が明確であること
- 業務と業務の依存関係が明確であること
なのです。
一方こうした設計の破綻した組織ではどうでしょう。以下のようなことが起きてしまう可能性があります。
- 稟議は作成されたりされなかったり、誰にチェックしてもらえばいいかわからない
- 管理職のあずかり知らないところで受発注が行われている
- リリースしたサービスが実は法律に違反していた
どうでしょうか。こんなことは例に挙げたほど極端ではないにしてもあちこちで実際に起きていることです。
こうした責任範囲と権限の暴走、依存関係の不明確さが不都合を起こすのはシステムにおいてもまた同じで、そうしたシステムでは
- メインルーチンを読んでも何がしたいのかわからない(ビジネスロジックの漏洩、宣言的でない手続き的な実装)
- 入力と出力が不明確で再利用が怖い(依存関係の不明確)
- どこからどれくらい呼ばれてるんだか分からないグローバルな変数や関数(責任範囲の不明確、依存関係管理の放棄)
↑こういった状況が発生しています。
こうした現象を避けるために、
- 矛盾や無駄のない整合性の取れた情報設計
- 1つのまとまりが1つの目的に集中できるような責任分離
- オブジェクト間の関係を明確に説明する依存管理
が必要なのです。
課題考察まとめ
とりあえず何度も書いたこと、
- 責任分離
- 依存管理
- 情報設計
がシステム開発には(一般的な会社組織における組織設計にて重要とされるのと同程度かそれ以上には)重要なんだ、と言う主旨についてはなんとなくご理解いただけたんじゃないでしょうか。
それでは具体的にどう解決するのかと言うのを次のセクションからコードも添えながら見ていきたいと思います。
Laravelの例に見るFatModel/FatController問題の解決
冒頭で
- FatModel(太ったモデル)
- FatController(太ったコントローラ)
の話をしましたが、なんで太るかと言うとこの2つが担う責務が多いからです。
逆にここがFatにならずに済む規模ならこの2つが器用に責任を兼務して効率的に要件を満たしている状態と言えるでしょう。
プロトタイプ開発や開発者の人数が1人~2人ほどの規模であれば特に問題にはなりにくいはずです。
それ以上の規模になる時にはやはり兼務させていた責任を剥がしてそれぞれ最適化していく必要が出てきます。
Modelとは
来ましたこれの説明、超むずい。
何故なら様々な設計パターン、各種プログラミング言語、色々な言語パラダイム、においてそれぞれの文脈で微妙に違う意味、でもすごい似てる感じでこのModelと言う同じ言葉が使われ語られるので一般的にこうです、というのが非常に言いにくいのです。
なのでまずここではweb開発で最もポピュラーなMVCフレームワークであるLaravelのModelはこんな感じですというのを示し、そこから課題設定、解決手法の話に入っていきたいと思います。
LaravelのModel
// postsテーブルに対応する投稿モデル(ブログCMSみたいなものをイメージ)
class Post extends Model {
// getter。メンバをミューテタの実装を利用し加工して返す
public function getContentAttribute($value)
{
return escape($value);
}
public function slug()
{
return hash($this->id);
}
// authorsテーブルに対応する著者モデルの1つとリレーションを持つ
public function author()
{
return $this->belongsTo('authors');
}
// commentsテーブルに対応するコメントモデルの複数とリレーションを持つ
public function comments()
{
return $this->hasMany('comments');
}
public static function publishAll()
{
self::query()->update(['publish_flg' => 1]);
}
}
// データの取得ができる
$firstPost = Post::first();
// 取得したデータは「投稿」オブジェクトとして動作、Postクラスに実装されたメンバや関数が参照できる
echo $firstPost->title;
echo $firstPost->slug();
// データの更新ができる
$firstPost->content = 'hoge';
$firstPost->save();
// 記事の全公開ができる
Post::publishAll();
// データの削除ができる
$firstPost->delete();
このLaravelにおけるModelで実装したPostModelは
- データベースから投稿データを取り出す
- データベースへ投稿データを保存する
- 取り出した投稿データで投稿オブジェクトを作成する
という機能を持っているのがわかると思います。
ここで注意したいのが、規模の大きな開発ではデータベースに読み書きしにいく処理というのは非常にコストの高いデリケートな処理である、ということです。
このデータベースへのIOというデリケートな権限を、実装単純化のために投稿オブジェクトが持ってしまっています。
これがなぜ課題になるんでしょうか。
例えばこの投稿オブジェクトを使って記事一覧を生成する処理を考えてみましょう。
post/index.blade.php
@foreach($posts as $post) <a href="{{ $post->url }}">{{ $post->title }} @endforeach
投稿オブジェクトが複数セットされたiterableなCollectionクラスのインスタンスを受け取ってforeachにかけてリンクを投稿の数だけ生成しています。
上記はMVCでいうとViewにあたる見た目を生成する実装です。
先述した投稿オブジェクトにDBアクセスの実装が入っているというのは、このループされた$post1つ1つがDBにアクセスする機能を持っているということなのです。
post/index.blade.php
@foreach($posts as $post) <a href="{{ $post->url }}">{{ $post->title }} @php($post->delete()) // こんなんとか @php($post::publishAll()) // こんなんとかできちゃうってこと @endforeach
実際にこのようなコードが上がってくることはないと思いますが、これを気にすること、つまり自分の実装が返したオブジェクトを他のエンジニアがどう扱うかをしっかりコントロールすること、は複数人のジュニアエンジニアを率いて開発を行うことを任されるなら必要な考慮です。
この投稿一覧は
- 投稿オブジェクトが記事のURLとタイトルを正しく返してくれること
- 投稿オブジェクトのコレクションがiterable(ループ可能)であること
を期待しているだけで、それ以外のことに関心がありません。
それ以外のことには依存していないのです。
これは、以下のような状態があるということです。
- 本当はもっとシンプルな値渡しだけで成立可能な処理なのに必要以上に大きな情報を渡してしまっている
- 見た目に関することに責任を持ったViewがDBアクセス権という不要かつ重大な権限を(投稿オブジェクトを通して)持ってしまっている
これを許してしまうと意図しないところからDBアクセスを行うような実装が混入するリスクがあります。
またDBアクセスに関する変更の影響をviewファイルが受けてしまうというような不自然な依存関係をつくってしまうことになるので、どちらをいじるにも副作用の有無を気にせざるを得ません。
コード レビューで気を遣う箇所が増え、自分の仕事を増やしてしまうことになります。
なので
- 「投稿というデータ構造のオブジェクト表現」
- 「投稿データを取得して投稿オブジェクトを生成する処理」
上記2点を分離することでこれを解決しましょう。
Repositoryパターン --ModelからデータベースIOの責務を剥がす--
まず機能を削った投稿オブジェクトから見てみましょう。
Model/Post.php
// シンプルな投稿オブジェクトを表現するPostModel class PostModel { private $id; private $title; private $content; // 投稿オブジェクトは投稿ID,タイトル,文章の3つに依存して生成される public function __construct ($id, $title, $content) { $this->id = $id; $this->title = $title; $this->content = $content; } // getter public function id() { return $this->id; } public function title() { return $this->title; } public function url() { return 'posts/' . $this->id;; } ... }
何も継承していないとてもシンプルなクラスです。
「投稿オブジェクト」に依存した処理、例えば先述した投稿一覧のviewも上記のPostModelオブジェクトだけ渡してあげれば事足ります。
何かの拍子に意図せずDBアクセスするような機能が実行されるようなリスクもないです。
拡張する際の副作用も限定的になります。
次に分離したDBとのやりとりを行うPostRepositoryです。
Repository/IPostRepository.php
// PostRepositoryを実装する時は以下のような内容が実装してあれば動作します、という関数と値の出入りの定義 interface IPostRepository { public function all():PostCollection; public function byId(PostId $id):PostModel; public function store(PostModel $post):PostModel; }
上記はPostRepositoryのinterfaceの定義です。
この条件を満たすように、以下のようにPostRepositoryを実装します。
Repository/MySqlPostRepository.php
// 投稿データの取得や更新に責任を持つPostRepositoryのMySQL+Eloquent実装
class MySqlPostRepository implements IPostRepository
{
private $postEloquent;
public function __construct(PostEloquent $postEloquent)
{
$this->postEloquent = $postEloquent;
}
public function all():PostCollection
{
return $this->postEloquent::all()
->map(function($postElo){
return new PostModel(
$postElo->id,
$postElo->title,
$postElo->content
);
});
}
public function byId(PostId $postId):PostModel
{
$postElo = $this->postEloquent->find($postId);
return new PostModel(
$postElo->id,
$postElo->title,
$postElo->content
);
}
public function store(PostModel $postModel):PostModel
{
$postModel = $this->postElo::updateOrCreate(
['id' => $postModel->id()],
[
'title' => $postModel->title(),
'content' => $postModel->content(),
]
);
return new PostModel(
$postElo->id,
$postElo->title,
$postElo->content
);
}
}
DBとのやりとりのためにEloquentを活用しています。
ここではEloquentを活用してPostModelを正しく生成し、呼び出し元へ返してあげることだけに集中します。
PostEloquentやPostRepositoryに対して何か変更を加えても型宣言した通りPostModelを返すことができればそこから先の処理に副作用は少なさそうです。
そうして搬出したPostModelには先程と変わってDBアクセスできるような機能はつけられていないので、このリポジトリを呼び出す各種モジュール内で安全に利用してもらうことができます。
Repositoryは本来はストレージエンジンと実装の疎結合を実現する手段
このRepositoryパターンが本来対応しようとする課題は、例えば急に会社都合や何かの都合でデータベースをMySQLからPostgresSQLに変更しないといけなくなった、といったようなケースでストレージエンジンと実装を疎結合にしておかないとアプリケーションコードの修正コストが高くなってしまう、というようなものです。
システムやサービスのユーザがそのサービスの実装がPHPなのかRubyなのかPythonなのかということを気にする必要無く、そのサービスの提供価値を享受できるように、システムの中身もまた自身が依存するストレージエンジンからのその価値の享受をRepositoryという1つの領域が主に担う状態にしておくことで抽象化し、依存度を制御、影響範囲を限定的にしようとする努力なのです。
こうした内容を具体的には言語仕様のinterfaceという仕組みを用いて行います。
オブジェクト指向などの文脈で「オブジェクトではなくインターフェースに依存せよ」と言うのはこのように後で実装の内容を容易に交換できたり、テストがしやすかったりと、実装の内容を容易に交換可能な状態そのものに価値があるからです。
ただ実際にストレージエンジンの交換を迫られるシーンというのは滅多に無いため実際の開発現場では
- 先述したようなデータアクセスをMVCフレームワークModelから分離したい
- ユニットテスト書く時にモック差し込みやすいようにしておきたい
といったニーズが主だと思います。
こうした背景やニーズからinterfaceとrepositoryの実装によりデータストレージ周辺の事情を抽象化し、アプリケーション本体をMySQLやRedisなど特定のミドルウェアに依存させないようにする仕組みをリポジトリパターンと言います。
レイヤードアーキテクチャなど色々な設計パターンに登場する基本的なデザインパターンの1つであり、オブジェクト指向プログラミング全般的に通ずる依存管理、抽象化についての価値観とモチベーションを明確に表しているパターンと言えるのでしっかりインストールしておきたいところです。
ValueObject --Modelから値仕様の実装を巻き取る--
Modelからデータアクセスの分離をRepositoryによって実現する手法を紹介しましたが、さらにModelを整理する方法としてValueObjectを紹介します。
先述したPostModelに実装されていた処理に以下のようなURLを生成して返す実装がありました。
Model/PostModel.php
... public function url() { return 'posts/' . $this->id;; } ...
この「投稿URLはposts/{$id}
というフォーマットである」という仕様は投稿モデルのものというよりは、投稿モデルがメンバとして所有するURLという値の仕様です。
こうした「値の仕様」をオブジェクトとして切り出すことでさらにModelの責任を明確にできます。
以下のように実現します。
Model/Post/Url.php
class PostUrl { // 投稿のURLという値の仕様が以下のようなフォーマットであることと、投稿IDに依存していることを表現できる private const FORMAT = '/posts/%s'; private $postId; public function __construct(PostId $postId) { $this->postId = $postId; } public function __toString() { return sprintf(self::FORMAT, $this->postId->val()); } }
Model/Post.php
class PostModel { private $id; private $title; private $content; private $url; public function __construct (PostId $id, string $title, string $content, PostUrl $postUrl) { $this->id = $id; $this->title = $title; $this->content = $content; $this->url = $postUrl; } ... $postId = new PostId(1); $post = new Post($postId,'hoge', 'hogefuga', new PostUrl($postId)); echo $post->url(); // post/1
このように値の仕様は値の仕様としてさらにModelから追い出すことで、PostModelはPostModelにしかできない仕事や仕様の表現、データ構造の実装、により集中できるようになります。
サンプルコードでは少しピンと来にくいかもしれませんが、大きなシステムで複雑な仕様を抱えるデータ構造はここまでやってようやく見通しの良いものとなり、それが「Model」として、オブジェクト指向的オブジェクトとして、マシンとプログラマを繋ぐプロトコルとして、機能します。
こうしてMVCフレームワークModelからデータベースとのやり取りを追い出し、値の仕様を追い出し、残ったもの。
これがデータ構造のオブジェクト表現として一番正解に近いModelだと考えています。
(これはDDDやクリーンアーキテクチャという設計パターンにおける「Model」の考え方です。
簡素化するために色々な要素を省いているので、詳しく学びたい方は以下を参照ください。)
Controller
Controllerの責務は
- 入力(HTTPリクエスト)を受け取り、チェックする
- 適切な処理系へ値を渡す
- レスポンスを返す
の3つです。
投稿データに関するCRUDを扱う良くないController.php
BadPostController extends Controller { public function store(Request $request) { // バリデーションが書いてあったり $request->validate([ 'title' => 'required|unique:posts|max:255', 'body' => 'required', 'category' => 'required', 'tags' => 'required', ]); // insert用にオブジェクトをつくったり $post = new PostEloquent(); $post->fill([ 'title' => $request->input('title'), 'body' => $request->input('body'), 'category' => $request->input('category'), 'tags' => $request->input('tags'), ]); $ret = $post->save(); // 分岐が書いてあったり if ($post->needNotify()) { $post->notify(); } if ($ret) { return redirect('posts/index')->with(['success' => '成功しました']); } else { return redirect('posts/index')->with(['error' => '失敗しました']); } } }
投稿データに関するCRUDを扱う良さそうなController.php
PostController extends Controller { public function store(PostRequest $request, UserPostContent $userPostContent) // バリデーションルールはFormRequestに定義 { $newPostModle = $request->makePostByUserInput(); // ユーザの入力から投稿モデルを生成 $ret = $userPostContent($newPostModel); // 実行したい処理系を表現したクラスのインスタンス。どんな風にデータを永続化したり通知したりしなかったりするなどはここの責任 if ($ret) { return redirect('posts/index')->with(['success' => '成功しました']); } else { return redirect('posts/index')->with(['error' => '失敗しました']); } } }
そのルーティングで実行したい処理系をクラスで表現し、しっかりと名前を与えて
- 処理系の依存する値
- 返却される値
- やりたいこと
を明確にします。
こうすることでControllerは入力の解釈とレスポンスの返却に集中でき、見通しも良いです。
「投稿の新規作成」についてユニットテストを書くケースを想定しても、後者ならUserPostContent
クラスのインスタンスとPostModel
のインスタンスのみ解決すればテスト可能ですが、前者の場合HTTPリクエストとControllerというそこそこ大きなインスタンスを生成する必要があります。
この例ほどの規模であれば例えばバリデーションなどはそのままベタ書きされていたほうがメンテナンスしやすいと思います。
ただ割れ窓理論じゃないですが、こういうところからきちんと整えてあると実装メンバーには「自分の手で汚したくない」という心理が働いて全体が綺麗に維持されやすい、ということもあるかなと思います。
具体的な事例
最後に具体的な自社事例をいくつか紹介したいと思います。
サービス内通貨の複雑な要件
ある自社サービスにて、クレジットカード等で決済可能なサービス内通貨を実装する開発プロジェクトがありました。
企画当初は円と同価値の「コイン」という概念のみだったのですが、運用が進むにつれどんどん複雑化し最終的には以下のようなサービス内通貨のパターンが発生しました。
- 円と同価値の「コイン」
- コイン購入時に付与される「ボーナスコイン」
- 毎日一定量無料で付与される「ポイント」
- 毎日一枚無料で付与される「チケット」
良い感じに複雑ですね。データ構造としては面白い題材です。
当初は「コイン」のみだったのでシンプルに単一のデータ構造を表現できれば事足りましたが、上記のようになったのなら適切なクラス構造の設計を行い、継承関係やサービス内通貨としてのinterfaceを整えないと複雑さに起因する不具合を生んでしまいます。
- サービス内通貨共通の仕様
- 一つのサービス内通貨のみが持つユニークな仕様
- 複数のサービス内通貨のみが持つグループでユニークな仕様
上記のような通貨ごとに微妙に異なる仕様(有効期限がそれぞれで違うとか、有効な商品グループが異なるなど)を手続き的に処理してしまうと以下のような状態になります。
手続き的決済.php
// ユーザと商品を受け取って購入処理を実行する function purchase(User $user, Item $item) { if ($item->currency_type === 'point') { $user->point = $user->point - $item->price; $user->save(); } elseif ($item-currency_type === 'coin') { ... } ... }
理想形は以下です。
宣言的決済.php
// ユーザと商品を受け取って購入処理を実行する function purchase(User $user, Item $item) { $transaction = $user->buy($item); // どの通貨を消費すべきかはオブジェクトが知っているのでpurchase処理が意識しなくても良い $this->orderRepository->store($transaction); // buyメソッドが返す取引情報をデータベースへ永続化 }
前者は「商品の種類によって消費すべき通貨の種類が異なる」という仕様の管理に失敗しており、メインルーチンにビジネスロジックが漏洩しています。
一方後者はそうしたシステムの重要な仕様についての情報をオブジェクトに持たせることが出来ているので、メインルーチンにはシンプルに宣言的なメソッドコールが並ぶのみです。
このようにModelがシステム上重要な仕様を管理する責任を適切に負えば、他のレイヤーに属する実装のコストは大きく下がります。
例示したコードは抽象的に書いていますが、実際には「取引」の責任を負うオブジェクトを設計し、そのオブジェクトに「ユーザ」、「商品」の情報を渡せば適切なユーザの通貨残高と商品在庫の差し引きの計算を行ってくれるような設計で対応しています。
開発チームへの設計概念の浸透
見本となるような実装を先にある程度用意して、似たようなチケットを既存実装を真似しながら書いてみてもらうというやり方が最も速いと思います。
この時、丸投げしてしまってはうまくいきません。
統制の取れた開発を行うには一貫した方針と基盤、規約が必要です。
そうした軸が、理想定義があって初めて開発メンバーのアウトプットに対して妥当なフィードバックが行えるようになります。
コード レビュー依頼されても特にフィードバックすることないんだよなぁという人は、その人が設計や理想定義を放棄しているか、プロダクト品質について理解していないです。
まずは責任者、リードエンジニアに類するロールを持つエンジニアがしっかりと要件を把握しベースとなる基盤を構築しましょう。
これをちゃんとできるのが開発マネージャや開発責任者というロールの最低要件と考えているのですが、現実問題として請負開発のような仕様を下請けに流すだけといったようなスタイルの開発マネージャも多いのではないかと感じています。
まとめ
要件が複雑だったり開発者やステークホルダーの多い規模の大きなweb開発においてフレームワークが提供してくれる責任分離の仕組みだけでは足りない課題になりやすいポイントとその構造、解決手法について書いてみました。
長くなってしまいましたが、本稿を最後まで読んでくださった方にまずは、ありがとうございました。
何か1つでも参考になるポイントがあったならば嬉しいです。
今回扱った内容の普遍的で概念的な部分は、今後ノーコードやローコードが主流となった世界線に置いても役立つ内容と見ています。
コンピュータと人を繋ぐプロトコルとしてのプログラミング言語が失われたとしても、情報を正しく扱う力、データ構造を設計する力、モジュールに対して適切に責務を与えて合理的な依存関係を構築し管理する力、こうしたスキルが解決する課題というのは、プログラムの記述コストと関係のないところに存在するからです。
そして、このスキルを持たずに行うノーコード開発では低品質なコードに悩まされることは無くても、低品質な情報管理によって結局はメンテナンスが難しくなって悩むことになるのです。
これこそが「プログラミング自体は簡単だけど綺麗に書くのが難しい」と言う難しさの正体そのものです。
言語やフレームワーク、実行環境、開発環境のトレンド変化に惑わされず、普遍的で陳腐化しない、「情報を扱うプロとしてのエンジニアリングスキル」に正しく投資していきたいですね。
文責:ナイル株式会社 工藤