LoginSignup
37
44

More than 3 years have passed since last update.

オブジェクト指向習得のための5ステップ【SOLID原則】

Last updated at Posted at 2020-04-12

はじめに

どうもtak@オブジェクト指向おじさんです。
Twitterで"オブジェクト指向"を検索すると、日々「オブジェクト指向わからん」「オブジェクト指向完全に理解した(わからんの意)」といった嘆きを目にします。

何故オブジェクト指向は難しいのか?
いくつもの理由は考えられますが、その一つに用語の煩雑さがあると考えています。
オブジェクト指向の3要素に始まり、SOLID原則、デザインパターン、ドメイン駆動設計・・・それぞれ独立した概念が、整理されないまま浴びせられている状況。
乳幼児にミルクと固いお煎餅を同時に与えているようなものだと思います。

今回はオブジェクト指向に関する用語を、SOLID原則を軸に5ステップにまとめました。
初心者に習得して欲しいステップ順に並べ変えており、関連する用語を整理しました。
「オブジェクト指向大体理解した(本当にわかったの意)」まで習得できるよう、解説します。

※私はC#erなので、用語は.NetFramework基準になります。適宜読み替えてください。

大前提

オブジェクト指向設計の目的は保守性拡張性の向上にあります。
これからの説明で「どうしてこうなるんだ?」と感じる点があれば、保守性のため?拡張性のため?という視点で読んでみてください。この視点を持つかどうかで、オブジェクト指向の理解は100倍変わります。

オブジェクト指向習得のための5ステップ

ステップ   主要素    副要素    
1 単一責任の原則   クラス、カプセル化、インスタンス、コンストラクタ、デストラクタ、属性、メソッド、アクセス指定子、凝集度
2 インターフェース分離の原則 インターフェース、抽象化
3 依存関係逆転の原則  関連、依存、結合度、MVC
4 解放閉鎖原則     多態性(ポリモーフィズム)、Factory
5 リスコフの置換原則  継承、基底クラス、派生クラス、オーバーライド、オーバーロード、委譲

みんな大好き(?)SOLID原則を習得ステップ順に並べた表になります。
これからステップごとに理解の目標を掲げ、解説していきます。

副要素にはそれぞれのステップで覚えるべき用語を列挙しています。
用語ごとの詳細な解説は割愛しますので、適宜ググりながら読み進めてください。

ステップ1:単一責任の原則

1つのクラスは1つだけの責任を持たなければならない。すなわち、ソフトウェアの仕様の一部分を変更したときには、それにより影響を受ける仕様は、そのクラスの仕様でなければならない。(出典:wikipedia)

目標:責任を意識したクラスが作れるようになる。

オブジェクト指向プログラミングをする上で、絶対に必要になるのがクラスです。
このクラス、どういった単位で作るのが良いのでしょうか?
答えは上の引用にある通り、1つだけの責任を持つように作ります。

責任とはソフトウェアを構成するための要素と捉えてください。
画面、手続き、判定、データベースetc...あらゆる構成要素が責任と言えます。
そして、わざわざ単一と付いているのは、それらの責任一つ一つをクラスに分割するという意味になります。

では、画面、手続きなどの構成要素をそのままクラスにすればいいのかというと、それでは不十分です。
ここでの単一とはもっと厳密なものです。
クラスの役割が誰の目にも明らかで、一言で言い表せるほどにシンプルである必要があります。

例えばいくつかのファイルを読み込むFileReaderクラスがあったとします。
一見、単一責任のように思えますが、第3者にはこれが何を読み込むのか、どんな結果が得られるのか分かりません。責任が明確でなく、曖昧な状態です。
責任分割の例を下図に示します。(Getterとか諸々は省略)
単一責任の原則.png
FileReaderという大きな責任を、SettingFileReaderクラスとUserFileReaderクラスの具体的な責任に分割しています。

単一責任にすることで以下のメリットが得られます。
- 第3者がクラスの目的、使い方を理解しやすくなる
- クラスの変更による影響をそのクラスに限定できる

単一責任になっているかどうかの目安は、クラス名で機能を明確に示せているか?です。
あなたの同僚、後輩のために、直感的に理解できるクラス名を付けてあげてください。

ステップ2:インターフェース分離の原則

汎用的な目的のインターフェイスが1つだけあるよりも、特定のクライアント向けのインターフェイスが多数あった方がよりよい。(出典:wikipedia)

目標:インターフェースクラスの役割を理解する。

インターフェースクラスとは、あるクラスが提供する機能の使い方のみを定義したクラスです。
インターフェース分離の原則では、特定のクライアント向けのインターフェースが多数あることを推奨しています。
どういうことかを説明していきます。

クラスは、時にいくつかの側面を持つことがあります。
人が仕事とプライベートで見せる顔が違うように、クラスも、そのクラスを使う相手によって違った側面を見せたいという場合があります。
例えば、ファイルを操作するクラスがあるとして、これをあるクライアントから見たときは読み込みクラスになり、別のクライアントから見たときは書き込みクラスになる、といった具合です。
相手に必要な側面だけを見せるようにする。これがインターフェース分離です。

インターフェース分離の例を下図に示します。
インターフェース分離の原則.png

SettingFileクラスのインターフェースを、読み込みクラス(IFileReader)と書き込みクラス(IFileWriter)に分離し、クライアントには必要な側面のみ見せるようになっています。
これにより、SettingFFileクラスの一部に変更があったときに、クライアントへの影響を最小限に留めることができます。
例えば、Readメソッドを変更した場合、ClientAクラスには影響がありますが、ClientBクラスには何も影響がないことになります。
このようにクラス間の接点を減らし、変更の影響を小さくすることを疎結合と呼びます。

また、インターフェースクラスの名前がSettingFileクラスとは別になっていることに注目してください。
このようにクラスの持つ特性を切り出して、一般的な名前を付けることを抽象化と呼びます。

疎結合抽象化も保守性を高めるための重要な考え方ですので、是非身に着けましょう。

インターフェースクラスについては、別記事でも解説していますので良ければ参考にしてください。
Interfaceクラスの使い方ポイント解説【オブジェクト指向】@Qiita

ステップ3:依存関係逆転の原則

具体ではなく、抽象に依存しなければならない(出典:wikipedia)

目標:クラス間の依存関係ルールを理解する。
   レイヤー間の依存関係ルールを理解する。

クラスとクラスの間には、必ず何らかの依存関係があります。
依存関係上での、使う側のクラスを上位クラス、使われる側のクラスを下位クラスと言います。
また、クラスの変更による影響は依存関係と逆方向に作用します。
依存関係の例を下図に示します。
依存関係.png

一般的に、上位クラス、下位クラスの依存関係は一方的である必要があります。
もし下位クラスから上位クラスに依存してしまうと、どうなるでしょう?
依存関係2.png
下位クラスを変更すると、上位クラスが影響を受けます。
そして上位クラスを変更すると、下位クラスが影響を受けます。
そしてそして・・・影響は永久ループして収拾がつかなくなってしまいますね。

実際にはそうではないかもしれません。クラスの中身を知っているあなたには、大丈夫とわかるかもしれません。
しかし、第3者には分からないのです。
変更による影響範囲が特定できないことは、保守性に大きな悪影響を与えます。
上位クラス、下位クラスがお互いに依存することを相互依存と言い、避けるべきパターンとされています。

ここでMVCアーキテクチャをご紹介しましょう。
MVCアーキテクチャとは、ソフトウェアの要素をModel(判断)、View(画面)、Control(制御)の3つのレイヤーに分割して組み立てる考え方です。
一般的にソフトウェアの運用において、画面は変わりやすく、判断は変わりにくいとされています。
よって、View(画面)→Control(制御)→Model(判断)のように依存関係を組み立てることで、変わりやすい画面の影響を他に与えないようにすることができます。これにより、変化に強いソフトウェアを作ることができます。
MVC1.png
これだけでもかなり強力なアーキテクチャなのですが、ある問題があります。
それは再利用性の低さです。
3つのレイヤーが直接依存しているため、Viewを他のソフトで再利用する場合、紐づくControlとModelも会わせて再利用しなければならないという制約が付きまといます。

これを解消するのが依存関係逆転の原則です。
直接依存していたクラス間にインターフェースクラスを挟むことにより、依存関係を断ち切ります。
MVC2.png
こうすることで、□で囲った範囲での再利用が可能になります。

これが具体ではなく抽象に依存するの目的です。
依存関係逆転の原則は地味ではありますが、長期的なソフトウェアの開発では再利用性という点で大きな役割を果たしますので、是非押さえてください。

ステップ4:開放閉鎖の原則

ソフトウェアのエンティティは(中略)拡張に対して開かれていなければならないが、変更に対しては閉じていなければならない。(出典:wikipedia)

目標:多態性によるクラスの使い分けができるようになる。

ステップ2:インターフェース分離の原則にて、インターフェースによる抽象化に触れました。
ここでは抽象化について、もう少し踏み込んだ話をします。

抽象化とは、「多くの物や事柄や具体的な概念から、それらの範囲の全部に共通な属性を抜き出し、これを一般的な概念としてとらえること。(出典:Google)」と定義されています。
これはオブジェクト指向では、「クラスに共通な概念を抜き出し、一般的な概念として捉えること」と言い換えられます。

抽象化の例を以下に示します。
開放閉鎖1.png
ここでは、いくつかのFileクラスからReadという共通概念を抜き出し、IFileReaderインターフェースとして定義しています。
これにより、IFileReaderインターフェースは、SettingFileクラス、LogFileクラス、ConditionFileクラスの代わりをすることができます。
このような性質を、多様な態勢を持つ性質として多態性と呼びます。

では多態性を使うことで、どんなメリットがあるのでしょうか?

まず、Clientクラス側からの視点で考えてみましょう。
Clientクラスからの依存するのはIFileReaderのみとなります。つまり、それぞれのFileクラスを意識することなく、Clientクラスの処理を完結させられることになります。
簡単に言えば、If文が不要となり処理がシンプルになるということです。(※補足)

次に、Fileクラス側からの視点で考えてみましょう。
今は3つのFileクラスですが、仕様拡張により扱うFileが増えた場合はどうなるでしょう?
答えは、これまでと同じように、IFileReaderインターフェースを持つ新たなFileクラスを追加するだけです。
では、あるFileクラスに変更があった場合はどうでしょう?
答えは、該当されるFileクラスの中身を変更するだけです。

拡張、変更いずれの場合も、FileクラスとClientクラスはIFileReaderクラスにより依存関係が切られているため、Clientクラスには一切影響を与えません。
つまり、拡張に対し開いている。変更に対して閉じている。と言える状態です。
これが開放閉鎖の原則です。

(※補足)
Clientクラスの「If文が不要となる」との表現に違和感を覚えられた方がいるかもしれません。
確かにClientクラス自体にはIf文が無くなるわけですが、ではFileクラスの使い分けは一体どのように実現するのでしょうか?
これを解決するのがFactoryクラスです。
開放閉鎖2.png
このように生成に対して責任を持つFactoryクラスを用意し、そちらでFileクラスの生成を行うことで、Clientクラスが具体的なFileクラスを意識しなくてよいようにします。

この形はFactoryパターンと呼ばれ、現場の設計でもよく出てきますので覚えておいてください。

ステップ5:リスコフの置換原則

プログラムの中にある任意のオブジェクトは、プログラムの正しさを変化させることなく、そのサブクラスのオブジェクトと置換できなければならない。(出典:wikipedia)

目標:継承の仕組みを理解する。
   破綻しない継承の考え方を理解する。

いよいよ最後のステップに入りました。
ここでは継承についてお話します。
継承というとオブジェクト指向の3要素として真っ先に説明されることが多いのですが、私はあえて最後のステップに持ってきました。
それは継承を正しく使うのは非常に難しく、ステップ1から4までの内容をちゃんと理解して使わないと設計が簡単に破綻してしまうからです。

継承とは「元のクラスの責任と要素を受け継ぎ、新たなクラスを作成すること」です。
まずは良い継承の例を見てみましょう。
継承1.png
Fileクラスという抽象化された責任を持つ基底クラスがあり、それをSettingFileクラスとConditionFileクラスが具体的な責任を持つ派生クラスとして定義しています。
これは元クラスの責任と要素を完全に受け継いでいる形となります。

次に悪い継承の例を見てみましょう。
継承2.png
こちらはFileクラスを継承して、まったく責任の異なるUtilityクラスを定義しています。
このように実装の流用を目的とした継承は実装継承と呼ばれ、良くないパターンとされています。

この2つの例の大きな違いは、Clientクラスからの視点で考えるとよくわかります。
良い例では、FileクラスをSettingFileクラスやConditionFileクラスに置き換えても、Clientクラスは何も気にせず動作することができます。
一方、悪い例では、FileクラスとUtilityクラスの責任が異なるため、置き換えることができません。

このように、継承元と継承先は完全に置き換え可能なものとして設計する必要があります。
これがリスコフの置換原則です。

(補足)
悪い例で示したUtilityクラスのように、責任が曖昧で無関係な要素をどんどん取り込んでしまうクラスを神クラスと呼びます。
なんでも取り込んでしまうので何でも出来てしまうのですが、それ故にあらゆるクラスと依存関係を持ってしまい、依存関係がめちゃくちゃに絡み合う原因となります。
現場でもよく見かける存在ですが、新たな神クラスを生み出さないように気を付けましょう。

神クラスを避ける方法としては、継承をせず委譲によって機能を借りる手があります。
継承3.png
継承を使おうと思った時は、まず委譲でできないか?を考えてみてください。

まとめ

ステップ1:単一責任の原則
 →クラスは単一責任でつくること
ステップ2:インターフェース分離の原則
 →インターフェースクラスを使って疎結合を実現すること
ステップ3:依存関係逆転の法則
 →具体ではなく、抽象に依存すること
ステップ4:開放閉鎖の原則
 →拡張、変更による影響を最小限にすること
ステップ5:リスコフの置換原則
 →継承クラスは置き換え可能にすること

最後に

お疲れさまでした!
ステップ1~5までを理解したあなたば、オブジェクト指向の入門を果たしたと言えます。
今のあなたであれば、さらに上位の要素であるデザインパターン、クリーンアーキテクチャ、ドメイン駆動開発などの内容も読み解くことができるでしょう。

今後の更なるレベルアップを応援しています。
ご不明な点がありましたら、コメントをお寄せください。
記事が良いと思われたらLGTMお願いします!

謝辞

本記事で掲載しているクラス図はChange Vision社のastah UMLを使って描きました。
Change Vision社では新型コロナウイルスで影響を受けるエンジニアのために、2020年5月31日までのライセンス無償提供を実施されています。ありがとうございました!

37
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
44