Unityでインディゲームを作る!

Unityでのゲーム制作を目指し、それに関わる話題についてのブログ

『SOLID原則』について考える。思考メモ [ オブジェクト指向プログラミング ]

SOLIDprinciples

 オブジェクト指向プログラミングにおいて指針となるSOLID原則について、自分なりに考えをまとめます。つまり、『あくまで個人的な見解です』

 

 現在読み進めているUnityによるe-book "Level Up Your Code With Programming Patterns"はプログラミング・パターンについての本ですが、前半部では予備知識としてSOLID原則の解説が入ります。もちろんSOLIDについては知ってはいましたが、このe-bookを読んで改めて考えさせられ、ここで自分なりの考えをまとめておく必要があるな、と感じました。

 色々と考えを書き連ねるので、まとまりに欠けたウダウダと書かれる文章になってしまうかもしれませんが、ご了承ください。

 

SOLIDとは

 いわゆるオブジェクト指向プログラミング(以下、OOP)とは、要するにclass(クラス)でプログラム全体を分割する、という分割統治法(Divide & Conquer)に基づいた方法論ですが、そのOOPにおけるソフトウェア・デザインで気をつけるべき五カ条をまとめたものがSOLID原則です。

Single responsibility

Open-closed

Liskov subsutitution

Interface segregation

Dependency inversion

 それぞれの頭文字を取ってSOLIDとなるわけですが、それぞれについて自分の考えをまとめていきたいと思います。

 このSOLID原則は、ここ二十年間の大企業レベルのソフトウェア・デザインにおいて支配的であった、とされており、個人レベルの自分としてはそこまで徹底するものではないかもしれませんが、一応ちゃんと抑えておきたい所です。

 

Single Responsibility

 単一責任原則、となります。クラスやメソッドなどは、ひとつの仕事についてのみ責任を負うべきであり、変更する場合、その理由は一つでないといけないということです。

 つまり、これも『分割して統治せよ』という考えに基づくものであり、的確に仕事や責任を分割しろ、ということですが、個人的には一つ引っかかることがあります。

 

 クラスは一つのことだけに責任を持つべきである、という理屈も正当性も分かるのですが、そのひとまとまりの規模というのは、そのクラスの抽象度によってかなり幅があるということです。

 つまり、高い抽象度を持つ、高級なクラスは基本的な概念やモノが合わさって一つのまとまりとなっているのに対して、より低級なクラスはより原始的な内容となってきます。これらの抽象度の異なる各クラスを同質な一つのクラスとして捉えたり、まとめたりするのって無理じゃない?と思ってしまいます。どこまでを『ひとつ』とするのか。

 私達人間は一つの存在ではあるけども、その中の一つの細胞もまた一つの存在だよね、っていう哲学的な話になってきます。人間がある仕事をしている間に、細胞もまた一つの存在として、その生命の責務をやっているわけですからね。

 

 だから、結局は『どう分割するか』という話になってくるわけですが、そこがやっぱり難しいってことに終始します。

 意味の有る、必然性のある一つのまとまりとしてクラス設計していくにせよ、何をもって区切るのか、という問題ですよね。

 なんで後述のDependency Inversionとは、つまり(そういう問題はあるけど、せめて)クラス内の記述はそのクラスの抽象度に合ったものにしろってことに繋がっているのだと思うんですが、そう考えるとSingle-Responsibilityは意外と曖昧な部分が多い原則なのかな、と思ってしまいます。

 

Open-Closed

 拡張に対してはオープンに、変更に対してはクローズドに、という原則です。新しいコードを書き加えやすい状態に保ち、クラス内部に変更を加える場合には他のクラスを巻き込まないように、依存関係や結合度を疎にしろ、ということになります。

 これは上手く実践できるかはさておき、一番分かりやすい部分だと思います。要はクリーンでシンプルなコードを書けているかということであり、正しくクラス設計出来ているか、ということなので。

 

 これを守れていないとドンドン無駄な時間が掛かってしまうぞ!ということを考えると、そのシンプルさに反して重い教訓だと思います。

 

 具体的な実践例としては、図形の面積を計算するクラス(Area Calculatorクラスとしましょう)に各図形の計算法を記述するのではなく、Shape抽象クラスを元に、それを継承した三角形クラスや円クラスを作り、その各々のクラスに具体的な計算法を書くというものです。

 こうすれば、面積計算するクラスではShapeタイプとして各図形を抽象化し、コードをクリーンに簡略化でき、かつ他クラスへの依存度を減らせます。そして、新しい図形を加えるにShapeクラスを継承した新たなクラスを書けば良いので、拡張しやすく、他のクラスへの影響も与えにくく、ということになります。

 つまり、継承やインターフェイスを上手く使いなさい、ということであり、OOPの様々な機能を活用しなさい、ということを言っているのでしょう。

 

Liskov Subsutitution

 リスコフ置換(代替)、スーパークラス(子クラス)とベースクラス(親クラス)は置き換え可能でなければならない、というバーバラ・リスコフさんが提唱した考えです。

 つまり、どんなクラス間においても継承ができるということではなく、共通項があり、本質的に同質なクラス同士で継承しろ、コンテキストを考えろ、ということだと思うのですが、これもまた難しい問題です。

 

 厳密でモノリシックなクラスを設計するのは難しい、だからis-a関係ではなく、has-a関係を目指そう、ということですが、こうした継承に関する問題に対する具体的な解決策としてinterfaceがあり、これはコンテキストを考えずに様々なクラスに同じメソッドを持たせられます。

 interfaceのデメリットってなんかあるんですかね?依存関係や結合度に悪影響は与えないので、かなり使い得ですよね。

 厳密に親子関係を考えなければいけない継承は難しい、単純に複数のクラスに同様な機能を追加したい、ということならinterfaceを使えばいい、ということなのでしょう。

 

Interface Segregation

 そんなinterfaceを短くして、少ないメソッドに絞り、クライアントに余計な機能をつけさせないようにしよう、というのが、インターフェイス分離の原則です。

 これもまた分割統治法ですね。interfaceは必要な機能から逆算して考えられるので、クラス設計と比べても、この原則は実践しやすいものだと思います。

 

 継承を使って、きっちりクラス設計した方が良いのかなと思って来ていましたが、そう考えるとinterfaceをどんどん使っていった方が良いのかなと思いました。

 実際前述のe-bookでは、継承は共通のパラメータを持たせたい時に使え、という感じで、それ以外は多重継承みたいなこともでき、依存関係にも影響与えないインターフェイスの方が明らかに良い、というような扱いです。

 

Dependency Inversion

 高級なクラスは低級なクラスから直接的に何かを取り込んではいけない、という原則ですが、要するに抽象的なクラスに具体的なコードを書くな、ということです。

 具象的なコードは具象的なクラスに書け、ということであり、やはりこれもまた、ちゃんと分割しろ、という分割統治法に基づく原則ということになるでしょう。

 

 "操作するな、命じよ"という言葉もありますが、監督役となるクラスは命令を出して、実際に作業するクラスが具体的な理解に基づいて実務を負う、という意味で役割分担をちゃんとしろということですよね。

 

 そのクラスの持つ(べき)抽象度を考えて、その抽象度に合わせたコードを書くべき、となると、この原則は実践が難しそうです。結局、オブジェクト指向においては、物事の本質を見極める能力が大事なんだなと思います。

 

まとめ

 SOLID原則から得られる教訓を更にまとめると、どう分割するか、抽象度を揃えろ、コンテキストを考えろ、という三大原則が導かれたような気がします。

 分割、抽象度、コンテキストの三つがOOPにおけるキーワードだと改めて自分は思いました。

 

 原則というものは基本的には漠然とした抽象的なものなので、やっぱ自分なりの考えをまとめておくのは大事だなと思いました。それと同時に自分はプログラマーとしては、まだまだだなと改めて思ってしまったので、気合を入れ直していきたいです。