駆け出しエンジニアぱかぱかの成長記録

引くほど忘れっぽい新卒2年目駆け出しSEぱかぱかの備忘録です。

【オブジェクト指向】SOLIDの原則について

こんにちは!ぱかぱかです!
今日は現在設計を進めている飲食店記録アプリ「グルメノート」のクラス設計にあたり、SOLIDの原則をおさらいしてみたいと思います。

SOLIDの原則とは

SOLIDの原則はソフトウェア設計をより平易かつ柔軟にして保守しやすくすることを目的としたオブジェクト指向で用いられる原則で、以下の5つの項目からなります。

S → Single Responsibility Principle: 単一責任の原則
O → Open-Closed Principle: 開放閉鎖の原則
L → Liskov Substitution Principle: リスコフの置換原則
I → Interface Segregation Principle: インタフェース分離の原則
D → Dependency Inversion Principle: 依存性逆転の原則

以前プロジェクトの勉強会で取り上げられておりメンバーで具体例など考えながら話してましたが、改めてみると忘れている…
改めてグルメノートの開発ではどう気をつけたらいいかそれぞれ例を挙げて考えてみます。

今回の記事では以下の本も参考にしました。

グルメノートの構成(仮)

まず今のところ決まっているグルメノートの構成をまとめておきます。

画面構成

行ったお店リスト
 行ったお店をリスト形式で表示。

行きたいお店リスト
 行きたいお店をリスト形式で表示。

お気に入り店リスト
 お気に入り店をリスト形式で表示。

お店詳細画面
 行ったお店の詳細を閲覧する画面。

お店登録画面
 行きたいお店/行ったお店を登録する画面。


操作フロー


Single Responsibility Principle: 単一責任の原則

関数やクラスが保持する責務は1つであるべきだという原則です。
これはSOLIDの原則の中でも一番言葉のまま意味がわかりやすい気がしています。

例えばグルメノートでお店クラスを作るとします。
ここに行きたい店登録メソッド行った店登録メソッドを作ったらどうなるでしょうか。
これでは1つのお店クラス行きたい店の登録行ったお店の登録の2つの責務を担うことになります。

登録情報として行きたい店は「店名、ジャンル、行きたい理由メモ」を持っていて、行った店は「店名、ジャンル、訪問日、評価、行った理由メモ」を持っている場合、お店クラスで持つプロパティはそれら両方を含んだものである必要が出てきます。
こういうクラスにしてしまうと、行った店にはあるが行きたい店にはないプロパティのようなものが存在してしまいます。
行きたい店に対して評価を取得するようなロジックを誤って書いてしまった場合に例外が発生してしまう、といった問題が起きかねません。

単一責任の原則に従うなら、行きたい店クラス行った店クラスに分けたほうが良いことになるでしょうか。
もっと正しくやるなら、そもそも登録を行うという責務を持った登録クラスというものを作ってさらに責務を分けてもいいかもしれませんね。


Open-Closed Principle: 開放閉鎖の原則

クラスは、拡張にはオープンで、変更にはクローズドであるべきだという原則です。
わかりやすく言うと、変更が発生した場合に既存のコードには修正を加えずに、新しくコードを追加するだけで対応できるような状態にする必要があります。

例えばグルメノートのお店クラスで店の登録をなんでも店登録メソッドで行うよう設計した場合を考えます。
しかし本当は、行きたい店の登録と行った店の登録では登録の内容が異なっています。
先に行きたい店登録用のメソッドが存在した場合、行った店の登録に対応するためには既存の店登録メソッドの内容に手を加える必要が出てきます。
ここからさらにお気に入り店登録を行おうとした場合はどうなるでしょう。
さらに店登録メソッド内で条件分岐が増えどんどんぐちゃぐちゃしてしまいます。

もしも店登録メソッドというものを使うならば、それをお店クラスで持つのではなく行きたい店クラス行った店クラスに分けた上でそれぞれ店登録メソッドをオーバーライドするようにすれば、新しい店のタイプが出てきた時も元の店登録メソッドを修正することなく新しい登録方法を実装することができます。

そもそも継承すること自体が色々な危険を孕んでいるので、周りではあまり推奨されていないのですが…
イメージとしてはこんな感じでしょうかね。

Liskov Substitution Principle: リスコフの置換原則

親クラスを継承した子クラスがあった場合、子クラスを親クラスで置換可能でなければならないという原則です。

例えばお店クラス行きたい店クラス行った店クラスお気に入り店クラスが継承している構成だったとします。
お店クラスがどのお店でも共通に使うお気に入り登録メソッドを持っていたが、お気に入り登録は行った店でしかできないという仕様になっていた場合、親クラスであるお店クラスで行きたい店クラスやお気に入り店クラスを置き換えられないため、リスコフの置換原則に則っていない状態になってしまいます。

そんなのお気に入り登録メソッドはお店クラスに定義せず行った店クラスにだけ追加すればいいだけではと言われたらそれまでなのですが…
ちょっと自分のアプリで全て例えようとするのには限界を感じてきました…トンチンカンになっているかもしれませんがご了承ください…

Interface Segregation Principle: インタフェース分離の原則

クラスは不要なインターフェースを持たないようにすべきという原則です。
インターフェース継承先で使わないメソッドがないようにインターフェースを分けることがポイントになります。
これは先ほどのリスコフの置換原則の例と関連付けられるかもしれません。
インターフェースを登録インターフェースとお気に入りインターフェースみたいな分け方にしてみたらどうでしょう。

これで仕様を知らない人が行きたい店クラスを実装する際に、行きたい店にはできないはずの「お気に入り登録」を実装してしまうという事態を避けることができます。

Dependency Inversion Principle: 依存性逆転の原則

使う側が使われる側に依存してはいけないという原則です。
登録Viewから登録メソッドを呼び出したい場合、登録の内容が行きたい店・行った店・お気に入り店で異なります。
このとき使う側である「登録View」が使われる側である「行きたい店・行った店・お気に入り店」の種類を意識して登録メソッドを使い分ける必要が出てきます。
これでは使う側が使われる側に依存している状態になっています。

これは間にファクトリークラスとインターフェースを挟むことで解決できます。
「登録View」は登録メソッドを呼び出す際にまずファクトリークラスの登録作成メソッドを呼びます。
「行きたい店」「行った店」「お気に入り店」のいずれに対する処理を行うべきかの判断はファクトリークラスの中で行います。
ファクトリークラスではその条件に応じていずれかの店クラスのインスタンスを生成します。
「登録View」では登録作成メソッドで生成されたインスタンスから何も考えずに登録メソッドを呼び出すことで、生成されたインスタンスが継承している登録メソッドが動くようになります。


まとめ

いざ書いてみると、それぞれ正しく説明できているのか自信がありません…
誤りあればご指摘いただけると幸いです。
クラス構成を考えるにも、色々気をつけることがありそうです。
これから慎重に考えていきたいと思います。
ではまた!