オープン・クローズドの原則
はじめに
Java等のオブジェクト指向言語の設計・実装をしていると、よく「実装に依存するな。インタフェースに依存せよ」と耳にすることがある。実装に依存するとはどういうことか、インタフェースになぜ依存するようにしないといけないのか、というについて本記事では述べる。
オープン・クローズドの原則
「インタフェースに依存する」という考えは「オープン・クローズドの原則」と呼ばれる。この原則は「オブジェクト指向設計の原則」のひとつである。
オブジェクト指向設計の原則は、過去のソフトウェア開発の知見から生まれた設計や開発における原則集で、オープン・クローズドの原則以外にも様々な原則が存在する。
話をオープン・クローズドの原則に戻す。
オープン・クローズドの原則は、「拡張に対して開いていて、修正に対して閉じていなければならない」という原則のことである。
これは、「拡張がしやすく、拡張しても修正箇所はできるだけ少なくなるような設計にするべき」と言い換えることができる。
もっと言うと、オープン・クローズドの原則を適用することで、設計者は以下のメリットを享受できるといえる。
オープン・クローズドの原則を適用するメリット
- 拡張性がある。
- ある処理を修正するときの修正範囲を少なくすることができる。
…とはいえ、言葉だけではよく分からないので、サンプルプログラムを交えて説明する。
なお、サンプルプログラムはJavaで記載する。
サンプルコード(オープン・クローズドの原則 適用前)
DBに接続する責務を担うオブジェクトを受け取り、DBへの接続を行うメソッドを有するControllerクラスがあるとする。package com.edu.my.app.controller; import com.edu.my.app.connector.WinDataBaseConnector; public class Controller { public void connectDataBase(WinDataBaseConnector connector) { connector.connectDataBase(); } }
DBに接続するクラスは以下のように定義されているとする。
package com.edu.my.app.connector; /** * WindodwsのDBへの接続を行うクラス. * */ public class WinDataBaseConnector { public void connectDataBase() { // 何かしらの処理 } }
このような実装をしていることによる問題点は以下の通り。
- 拡張性が低い
今の実装は、「WindowsのDBに接続する」という具象クラスに依存している。
これが冒頭で記載した「実装に依存する」ということである。
これでは、Windows以外のDBに接続する、という修正が今後発生したとき、その都度Connectorに手を加える必要が出る。
public class Controller { public void connectDataBase(WinDataBaseConnector connector) { connector.connectDataBase(); } public void connectDataBase(LinuxDataBaseConnector connector) { connector.connectDataBase(); } public void connectDataBase(OtherDataBaseConnector connector) { connector.connectDataBase(); } ・・・ }
修正が発生するたびに都度ソースコードに手を加えるのはあまり好ましいとは言えない。
どうしてこうなった??
- Controllerクラスが、具象(実装。今回の場合はWinDataBaseConnector)に依存しているから。
どうしたらいい??
- 具象に対してではなく、抽象(インタフェース)に対して依存するようにする。
サンプルコード(オープン・クローズドの原則 適用後)
まず、DB接続のI/Fを定義する。package com.edu.my.app.connector_if; public interface IDataBaseConnector { public void connectDataBase(); }
I/Fの実装クラスを用意する。
package com.edu.my.app.connector; import com.edu.my.app.connector_if.IDataBaseConnector; /** * WindodwsのDBへの接続を行うクラス. * */ public class WinDataBaseConnector implements IDataBaseConnector{ public void connectDataBase() { // 何かしらの処理 } }
Controller #connectDataBaseの引数の型をインタフェースにする。
package com.edu.my.app.controller; import com.edu.my.app.connector_if.IDataBaseConnector; public class Controller { public void connectDataBase(IDataBaseConnector connector) { connector.connectDataBase(); } }
結局何が変わった??
Controllerが抽象(インタフェース)に依存するようになった。
修正前: 「WindodwsのDBに接続する」オブジェクトを受け取る。
修正後: 「なんでもいいからとりあえずDBに接続する」オブジェクトを受け取る。
これが「抽象に依存する」ということで、このようにすることで、「なんでもいいのでとりあえずDBに接続する」IDataBaseConnectorを実装するオブジェクトをすべて引数として受け取ることができるようになる。
つまり、WindowsのDBに接続するWinDataBaseConnectorも受け取ることができるし、LinuxのDBに接続するLinuxDataBaseConnectorも受け取ることができる。その他のDBに接続するOtherDataBaseConnectorも受け取ることができる。
今後、他のDBに接続する処理を行うクラスが作られてconnectDataBaseメソッドを呼ぶことになったとしても、Controllerクラスには修正を加える必要が一切なくなる。
なぜなら、具象(実装)ではなく抽象(インタフェース)に依存しているから。
何かしらのDBに接続するクラスが作られるたびにConnectorに手を加えていたオープン・クローズドの原則適用前とは大きく異なる。
先述したオープン・クローズドの原則の定義「拡張に対して開いていて、修正に対して閉じていなければならない」「拡張がしやすく、拡張しても修正箇所はできるだけ少なくなるような設計にするべき」とは、こういうことである。
終わりに
本投稿では、オープン・クローズドの原則について述べた。オープン・クローズドの原則を適用するということは、具象(実装)ではなく抽象(インタフェース)に依存する、ということを意味する。
このようにすることで設計における拡張性が増し、修正時の修正量を最小限に抑えることができる。
本投稿では、そのことをサンプルプログラムを交えて記載した。