はじめに
この記事は、私がハッカソンなどで一緒に開発するチームメンバーへ向けて話した内容を再構成したものです。改めて言語化してみたのでぜひ読んでいただき、少しでも設計に興味を持ってもらえると嬉しいです。
なぜ僕らは「良い設計」を目指すのか? そのたった一つのモチベーション
僕にとって、良い設計、良いコードが目指すゴールは、驚くほどシンプルです。それは 「読みやすいレベルを目指す」 こと。そして、この「読みやすさ」は、突き詰めれば 「人間が楽できること」 に繋がります。
僕らの仕事は、一度書いたら終わりの芸術作品を作ることじゃない。書いたコードは、未来の自分が、そしてチームの仲間が、何度も読み返し、修正し、拡張していくものです。
だからこそ、僕が設計において最も大切にしているモチベーションは、「見たくないものは見たくないし、見たいものは見たい」 という、ごく自然な感覚です。
触ってほしくない大事な部分は隠し(カプセル化)、安全に変更できる部分だけを公開する。この「制御」を徹底することで、僕らは安心してコードに触れることができます。SOLID原則は、この理想を実現するための、非常に強力な思考のフレームワークなんです。
僕なりのSOLID原則の解釈
SOLID原則の各項目は、すべてこの「人間が楽をする」という目的に繋がっています。僕が各原則をどう捉えているか、少し話させてください。
| 原則 | 名称 | 僕が思う、その原則の本質 |
|---|---|---|
| S | 単一責任の原則 | 「こいつ、何するやつ?」が一言で言えること。 責務が一つなら、コードを読む時に余計なことを考えなくて済む。 |
| O | オープン・クローズドの原則 | 「既存のコードを壊さずに機能追加できること。」 これが守られていれば、変更に対する心理的なハードルが劇的に下がる。 |
| L | リスコフの置換原則 | 「親のフリをした子が、期待を裏切らないこと。」 これにより、ポリモーフィズムが信頼できる武器になる。 |
| I | インターフェース分離の原則 | 「関係ない仕事を押し付けられないこと。」 必要な機能だけを実装すれば良いので、クラスがシンプルに保たれる。 |
| D | 依存性逆転の原則 | 「直接的な関係を持たず、『約束』で繋がること。」 これがチーム開発とテストのしやすさを爆発的に向上させる。 |
【S】単一責任の原則:「演劇」で考えると分かりやすい
「一つのクラスが持つべき責任は、ただ一つ」
SOLID原則の核となるこの考え方は、コードを人間が理解できる単位に保つためのものだと僕は捉えています。
ただ、個人的な経験上、この原則の難しいところは、細かく分けすぎると逆に全体像が追えなくなって、読みづらくなる ことがある点です。この「塩梅」を見極めるのが、設計の難しさであり、面白さでもあります。
単一責任の原則を実践する上で、僕はクラスをざっくり以下の3種類に分けることを意識しています。
- 他人に見せる/触らせるクラス
- 他人に見せない/触らせないクラス
- これらを繋ぐクラス
特に重要なのが3つ目の 「繋ぐクラス」 です。これをViewやModelに混ぜ込むと、途端に見通しが悪くなる、というのが僕の意見です。繋ぐ役が独立しているからこそ、ModelとPresenterだけでロジックのテストができるようになります。
このように責任を分離することで、各クラスの目的が明確になります。バグが発生した際も、「データの問題か」「描画の問題か」「制御の問題か」というように、問題の切り分けが格段に容易になるのです。
【D】依存性逆転の原則:開発のボトルネックを「集合場所」で解消する
「具体的な実装ではなく、抽象に依存せよ」
チーム開発において、「あの人の実装が終わらないと先に進めない」という手待ち状態は頻繁に発生します。この依存の連鎖を断ち切るのがDIPの考え方です。
ここでの鍵となるのは、「集合場所」 という概念です。
クラス同士が直接お互いを参照するのではなく、まず**共通のインターフェース(API仕様など)を定義します。これが、お互いが守るべきルールの決められた「集合場所」**です。
具体的な流れはこうです。
-
「集合場所」のルールを決める: まず、「プレイヤーのデータを保存するには、
SavePlayerDataという関数を呼び、プレイヤー情報を渡す」という共通のルール(インターフェース)を決めます。 -
各々が「集合場所」に向かう:
- Aさんは、ゲームのロジック担当。プレイヤーのレベルが上がったら、先ほど決めた
SavePlayerData関数を呼び出す処理を実装します。この時、データが実際にどうやって保存されるか(データベースなのか、ファイルなのか)は一切気にする必要がありません。 - Bさんは、データベース担当。Aさんの進捗を待つことなく、
SavePlayerData関数が呼び出された時に、実際にデータベースへ書き込む処理を実装します。
- Aさんは、ゲームのロジック担当。プレイヤーのレベルが上がったら、先ほど決めた
このアプローチにより、各クラスは具体的な実装から解放され、抽象的なルールにのみ依存するようになります。これにより、並行開発が可能になるだけでなく、テストの時にBさんが作った本物のデータベースの代わりに、偽物の保存機能(モック)に差し替えることも容易になり、ユニットテストの質も飛躍的に向上します。
設計に「絶対解」はない — ケースバイケースという現実
ここで、非常に重要な話をします。それは 「設計に絶対の正解はない」 ということです。僕らが議論したように、設計は常に 「仕様によって異なる」 のです。
例1:インターフェース分離の「やりすぎ」問題
インターフェース分離の原則(ISP)は、機能を細かく分けることを推奨します。しかし、考えてみてください。今作っている車のゲームで、「登場する全ての車が必ずエンジンをかける」という仕様だったとします。そして、今後もエンジンのかからない車が登場する予定は全くない。
この状況で、わざわざ「運転するインターフェース」と「エンジンをかけるインターフェース」を分離する必要はあるでしょうか?
僕は、ないと考えます。このコンテキストにおいては、それらを一つにまとめた方が可読性が上がり、むしろそれが「単一責任」を満たすことにもなり得ます。
例2:MVPが不要な時
同様に、すべてのクラスにMVPを適用すれば良いわけではありません。
例えば、ゲームのゴール判定をするだけのコンポーネントや、10〜20行で収まるような小さなクラスに対して、わざわざModel, View, Presenterの3つにファイルを分けるのは、明らかにやりすぎです。工数がかかるだけで、得られるメリットはほとんどありません。
設計は、作りたいもの、期間、メンバーの力量、設計者の思想など、様々な要因によって最適な粒度や方法が変わります。だから、「答えがない」と言われるのです。
設計スキルの重要性と、それが「必須ではない」理由
チームメンバーから「学生のうちに設計を学ぶべきか?」という問いがありましたが、これに対する僕の考えは少し複雑です。
なぜ、設計を早くから学ぶことが重要なのか
僕が設計の学習を推奨する最大の理由は、設計能力は「経験(場数)」によってしか身につかないスキル だからです。
実務に入ると、多くの場合、すでに誰かが作った設計の上で開発を進めることになります。ゼロから設計を学ぶ機会は、実はほとんどありません。「すでにある設計から意図を逆算できる力」を学生のうちに養っておくことは、とてつもないアドバンテージになります。
しかし、今のあなたにとって「必須ではない」
矛盾しているように聞こえるかもしれませんが、僕は学生の皆に対して、完璧な設計を「必須スキルだ」とは思いません。その理由は3つあります。
- 高度な設計は、時に「縛り」になる: 厳格な設計は、実装の自由度を下げ、スピードを落とす側面があります。期間が短く、まずは形にすることが重要なプロジェクトにおいて、ガチガチの設計は足枷になりかねません。
- 小規模プロジェクトではシンプルさが正義: 小さなものを作るのに、巨大なクレーンは必要ありません。小さなクラスに複雑なデザインパターンを適用するのは、ただのオーバーエンジニアリングです。
- 「温度感」は経験からしか学べない: 設計の「ちょうどいい具合」の温度感を掴むには、コードを書き、バグらせ、リファクタリングする…というサイクルを何度も経験する必要があります。これは、一朝一夕で身につくものではありません。
結論:僕の考えと、これから設計を学ぼうとしているあなたに伝えたいこと
ここまで話してきたことをまとめると、僕のスタンスはこうです。
設計は、僕自身が開発を楽にするために導入している、最高に便利な道具だ。その考え方、特にDIPのようなチーム開発を円滑にする発想は、知っておくと間違いなくあなたの武器になる。
しかし、あなたの現在のプロジェクトにおいて、必ずしも高度な設計が成功の鍵となるわけではない。だから、今すぐに完璧な設計ができることは必須ではないよ。
何よりも大切なのは、SOLID原則の根本にある 「なぜそうするのか?」という思想 を理解することです。「単一責任にしたいから」「結合を疎にしたいから」という目的意識を持つことです。
設計は経験です。実務で困る前に、その種をまいておく。この姿勢が、将来必ず役に立つと信じています。