エンジニアの皆さんは日々プロジェクトのコードを目にしていると思いますが、そのコードは本当に「良いコード」でしょうか?
私自身、自分の書いたコードを振り返ると「もっとこうすれば良かった」と感じることがよくあります。しかし、プロジェクトが成熟するとコードの変更は難しくなります。コードを修正すると他への影響が大きくなり、そのリスクを避けようと変更を控える傾向が強まるからです。
結果として、「良いコード」への改善は難しくなり、既存の設計やコーディングスタイルが定着してしまいます。
私もこうした経験があるため、同じ悩みを持つ方々に向けて、この記事を通して改めてオブジェクト指向設計について考えるきっかけを提供できればと思います。
この記事を読むにあたって
私自身、改めてオブジェクト指向の設計を学んでみようと思い、以下のアプリをオブジェクト指向の設計の考え方を意識して開発してみました。
この記事では、このアプリを具体例として、オブジェクト指向設計のポイントを解説します。
使用してみたい方は こちら からインストールしてみてください。
このアプリは、自分の身長や体重、トレーニング情報などを入力すると、BMI や 体脂肪率、トレーニング指標 などを簡単に確認できるシンプルなものです。
設計する際には ドメイン駆動設計(DDD) と SOLID原則 を意識しましたが、この記事ではそれらの詳細な説明は省略します。あくまでオブジェクト指向設計の考え方や方針に焦点を当て、UMLやコードに馴染みのない方でも気軽に読める内容にしています。
オブジェクト思考と向き合う前に
みなさんは、自作PCを組み立てたことがありますか?
マザーボードにCPUやメモリ、グラフィックボード、電源、ケースなどのパーツを取り付けることで、自分だけのオリジナルPCを作ることができます。

多くの人は、マザーボードやCPUの内部構造を詳しく知らなくても、パーツを適切に組み合わせるだけでPCを完成させています。

また、マザーボード自体も、接続するCPUやメモリの詳細な内部構造を知っているわけではありません。ただ、正しく接続されれば各パーツがきちんと動作することを認識しているにすぎません。

私たちユーザーにとっては、「CPUやメモリを取り付ける」というインターフェースが提供されているだけで、内部がどのように動いているかを知らなくても問題ありません。
同様にマザーボード側も、CPUやメモリの内部仕様を理解しているわけではありませんが、「取り付ければそれらが動作する」ということだけを知っています。
実はこの考え方は、プログラミングの世界にも通じます。
私が設計やコーディングをする際に大切にしている考えは、「プログラムとはブラックボックスの集合体である」という方針です。
ここでいうブラックボックスとは、「インターフェースのみが提供され、その内部構造を知らなくても利用できるもの」を指しています。
このように、内部の詳細を意識せずに利用可能な設計を行うことこそが、プログラムを作成する上で重要だと考えています。
この記事で登場する人物
名前 | 役割 | イメージ |
---|---|---|
画面 | ユーザーにUIを提供する役割を持ちます。 | ![]() |
コンポーネント | 画面に配置するUI部品です。 | ![]() |
メトリクス情報 | BMIや体脂肪率などの指標を表すクラスです。 | - |
ユーザー情報 | ユーザーの身長や体重などの情報を表すクラスです。 | - |
リポジトリ | データの永続化や取得を行うクラスです。 | - |
仕様 | バリデーションを行うクラスです。 | - |
オブジェクト指向とは?
本当にオブジェクト指向で設計できていますか?
単にクラスを作り、メソッドを定義し、インスタンスを生成して利用しているからといって、オブジェクト指向として適切に設計されているとは限りません。
例えば、次のような例を考えてみましょう。
「身体情報入力画面」で 身長 と 体重 を入力し、それを DBサービス クラスを使ってデータベースに保存する処理です。
「サービス」というクラスを導入して UI と ビジネスロジック を分離しているため、一見オブジェクト指向的に感じるかもしれません。

しかし、このような設計は、オブジェクト指向の本質からは少し外れています。
本来のオブジェクト指向とは、「オブジェクトという単位」でプログラムを設計する考え方です。
「身長」や「体重」は プリミティブ型 のデータです。
プリミティブ型をそのまま使ってやりとりするのは、オブジェクト指向的には望ましくありません。
言語によってはプリミティブ型が存在せず、すべてのデータがオブジェクトとして扱われる場合もあります。
ここでは、言語が標準で提供する基本的なデータ型(文字列型や整数型など) を「プリミティブ型」として表現しています。
また、開発者が独自に定義したデータ型(クラスや構造体など) を「ユーザ定義データ型」とし、それらを「オブジェクト」として表現しています。
そこで、以下のように設計を見直してみましょう。

このように、ユーザーオブジェクト を作成し、このオブジェクトに 身長 や 体重 といった情報を持たせます。
そして、このオブジェクトをDBサービスに渡し、データベースへの保存処理を行います。
プリミティブ型のデータを 明確なオブジェクト に変換することで、オブジェクト指向の本質により近づくことができます。
そのため、まずは「プリミティブ型でやりとりする」という考えを捨て、「全てをオブジェクトとして扱う」ことを意識して設計するようにしてみましょう。
私自身も、以前はなんとなくオブジェクト指向を使っていて、「プリミティブ型を使った設計が再利用性を高め、疎結合なコードになる」と考えていました。
そして「疎結合であるほど良いコードだ」と単純に考えていたのです。
自分以外には無知であれ
「画面」は、ユーザーに「BMI」などの情報を表示するために「コンポーネント」を利用します。
「コンポーネント」は、渡された「メトリクス情報(BMIなど)」を表示するだけです。

ここで重要なのは、「画面」は「メトリクス情報」や「コンポーネント」の内部構造を 知ってはいけない ということです。同様に、「コンポーネント」も、「メトリクス情報」の内部構造を 知ってはいけません。

では、このような設計をどのように実現すれば良いのでしょうか?
先ほどの自作PCの例を思い出してください。
CPUやメモリは、マザーボードに取り付けるだけで動作します。
マザーボードはCPUやメモリの内部構造を知らなくても、「正しく接続されれば動作する」ことだけを知っています。マザーボードが知っているのは、CPUやメモリの インターフェース だけなのです。
すべての部品が インターフェース を介して連携し、動作しています。
この関係をプログラムで表現すると、次のようになります。

「画面」と「コンポーネント」は、メトリクス情報のIF(インターフェース) の存在は知っていますが、それが具体的に 何のメトリクス(BMIや体脂肪率など)であるかは知りません。
「画面」は、「よく分からないけど、何らかのメトリクス情報をコンポーネントに渡せば、それを適切に表示してくれる」とだけ認識しています。
一方、「コンポーネント」も、「なんだか分からないけれど、渡されたメトリクスを表示する」という役割だけを持ちます。
メトリクスの名前や計算方法は、メトリクス自身が管理しています。コンポーネントはそれらの詳細を知る必要はなく、「ただそれを表示・実行する」だけなのです。

数学で例えるならば、「公式がなぜ成り立つか知らなくても、公式を使えば計算できる」というイメージに近いでしょう。
このように、各クラスが自分自身以外の役割や内部構造に対して 無知であること を意識して設計することが重要です。
責任は負いたくない
誰でも、多くの責任を負うのは避けたいものです。
これはプログラムの設計でも同じことが言えます。
プログラム設計において重要なのは、それぞれのクラスに与える 責任を明確 にし、各クラスが「自身の持つべき責任」だけを負うようにすることです。
理想的には、一つのクラスには一つの責任だけを与える のが望ましいとされています。
よくある問題として、すべてのバリデーション処理をひとまとめにした「バリデーションクラス」を作ってしまうことがあります。しかし、「バリデーションクラス」という概念自体が抽象的すぎて、機能が肥大化し、手に負えなくなることが多いため、あまりお勧めできません。
責任の範囲と役割を明確にする
例えば、次のように責任を明確化してみましょう。
- 画面の責任は、「コンポーネントを表示すること」だけ。
- コンポーネントの責任は、「メトリクス情報を画面に表示すること」だけ。
- メトリクス情報の責任は、「自身の名前や計算結果が正しいこと」だけです。

もしメトリクス情報の計算結果が間違っていた場合、それはメトリクス情報クラスの責任です。画面やコンポーネントには責任はありません。
もう少し具体的な例を考えてみます。
身体情報を永続化するクラス(以下、「身体情報リポジトリ」と呼びます)は、「身体情報を永続化する」という責任のみを負っています。
では、この「身体情報を永続化する」という責任の範囲はどこまででしょうか?
身体情報リポジトリの責任範囲は、あくまでも 身体情報の保存や取得を行うこと のみです。
たとえば、身体情報が正しい値かどうかをバリデーションするのは、このクラスの責任範囲ではありません。

身体情報リポジトリは、保存や取得以外の処理には責任を持つべきではありません。
もしバリデーションのロジックが間違っているときに身体情報リポジトリが怒られるとしたら、それは責任過多で不適切です。

そのため、バリデーション処理は別のクラスに切り出しましょう。
身体情報リポジトリには、バリデーションのルール(仕様)を外部から渡せるようにしておきます。身体情報リポジトリはその仕様を実行するだけで、中身を知る必要はありません。

もし仕様(バリデーション処理)が間違っていれば、責任を負うのは仕様です。身体情報リポジトリの責任ではありません。

また、もし永続化の処理にミスがあった場合のみ、身体情報リポジトリが責任を負うことになります。

何でもかんでも公開しない
「BMI」や「体脂肪率」などのメトリックを配列で管理する メトリクス というクラスがあります。このクラスを使ってメトリックの一覧を画面に表示する際、このアプリでは カテゴリー(身体組成、健康、食事、トレーニング) ごとにメトリックを表示しています。

各メトリックには、それぞれカテゴリーが設定されています。
このカテゴリーに紐付いたメトリックを取得したい場合、よく以下のような処理をしてしまいがちです。

これは、メトリックのプロパティに 直接アクセス してメトリックを取得しています。このようにあちこちからプロパティに直接アクセスしていると、プロパティの値が変更された場合、その変更に追従するために多くのコードを修正する必要が生じます。
この問題を防ぐために、以下のように「門番」としてメソッドを作成し、プロパティに直接アクセスしないようにします。

この「門番」を通すことで、プロパティに直接触れることなく、メソッドを介してメトリックを取得することができます。
基本的には、クラスのプロパティへ直接アクセスするのは避け、メソッドを介してアクセスさせるように設計しましょう。
意図を明確にする
メソッド名は、処理の意図が伝わるような適切な名前を付けることが重要です。
また、処理を適切な単位でメソッドに分割することで、メソッドの再利用性を高めることができます。
例えば、「メトリックリストを取得する」という処理は、やや抽象的で意図が曖昧になることがあります。
呼び出し元で、何のために メトリックリストを取得しているのか、また どのように 使っているのかが分かりにくくなります。
そのため、「カテゴリーに応じたメトリックリストを取得する」という明確な意図を持つメソッドを作成すると、処理の意図をはっきりと伝えることができます。
また、「メトリックの個数を取得する」といった処理があった場合は、これも別のメソッドとして分割することで、呼び出し元で処理の意図が明確になります。

プログラム設計においては、「漠然とした抽象的な処理」は避けるべきです。
プロパティへの直接アクセスや不適切なメソッド名はコードの意図を不明確にしてしまいます。
さらに、メソッドの単位を可能な限り小さくすることで、コードが何を意図しているかを明確に表現できます。
まとめ
オブジェクト指向では、オブジェクトという単位 でプログラムを設計することが重要です。
オブジェクト指向を意識した設計をする際には、次のポイントを押さえましょう。
- できるだけプリミティブ型を使わず、オブジェクト として扱う
- 各クラスが自分以外の役割について 無知 であるように設計する
- クラスごとの 責任を明確 に定義し、その範囲を限定する
- 公開するプロパティを最小限に抑え、メソッドを通じてアクセスする
この記事が、改めてオブジェクト指向設計を考えるきっかけになれば幸いです。
最後に、オブジェクト指向の設計について、私が愛用しているおすすめの書籍を紹介しますので、ぜひ手にとって学習してみてください。