🔐

オープン・クローズドの原則 〜プリンシプルオブプログラミングを読んで〜

2022/07/03に公開約3,000字

あらためて プリンシプルオブプログラミング を読んだので、本書のなかで取り上げられている「オープン・クローズドの原則(OCP)」について書いてみたいと思います。

オープン・クローズドの原則とは?

オープン・クローズドの原則とは「ソフトウェアの要素は、拡張に対して開いていて、修正に対して閉じていなければならない」という原則です。

要するに、機能追加などで変更が発生したとき「既存コードは変更せず、コードを追加するだけで対応できるようにしよう」というものです。

どのように実装するのか?

インターフェースを使い、具象(クラス)ではなく「抽象(インターフェース)」に依存するように実装します。

デザインパターンでいうと、オープン・クローズドの原則は「Strategy」「Observer」「Template Method」「Decorator」が該当します。

実装例

今回、実装例としてStrategyで実装してみたいと思います。
釣りが好きなので、釣具メーカーの釣り道具(タックル)一覧を取得する、というアプリを想定して実装してみたいと思います 笑。

適用していないパターン

まずは、オープン・クローズドの原則を適用していないパターンです。

RemoteDataSource#fetchTacklesに、メーカーを判定するEnumクラスを渡して、内部で処理を振り分けています。

class_image_01

// 釣具メーカーのEnumクラス
enum class Maker {
  DAIWA,
  SHIMANO
}

class DaiwaTackleListFetcher {
    fun fetch(): List<Tackle> {
        // Daiwa の釣り道具を取得する処理
    }
}

class ShimanoTackleListFetcher {
    fun fetch(): List<Tackle> {
        // Shimano の釣り道具を取得する処理
    }
}

// API連携を行うクラス
class RemoteDataSource {
    // タックル一覧を取得する処理
    fun fetchTackles(maker: Maker): List<Tackle> = when (maker) {
	Maker.DAIWA -> DaiwaTackleListFetcher().fetch()
        Maker.SHIMANO -> ShimanoTackleListFetcher().fetch()
    }
}

例えば、新たにMariaというメーカーのタックル一覧を取得することになったとします。

enum class Maker {
  DAIWA,
  SHIMANO,
  // 追加
  MARIA
}

// 追加
class MariaTackleListFetcher {
    fun fetch(): List<Tackle> {
        // Maria の釣り道具を取得する処理
    }
}

class RemoteDataSource {

    fun fetchTackles(maker: Maker): List<Tackle> = when (maker) {
	Maker.DAIWA -> DaiwaTackleListFetcher().fetch()
        Maker.SHIMANO -> ShimanoTackleListFetcher().fetch()
	// 追加
        Maker.MARIA -> MariaTackleListFetcher().fetch()
    }
}

上記の実装だとメーカーを追加するたび、RemoteDataSource#fetchTacklesに、メーカーの判定を追加していくことになってしまいます。

機能追加とは関係のない既存コードにも変更が発生してしまうので、よくありません。

適用したパターン

TackleListFetcherというインターフェースを作成して、各メーカーのFetcherクラスで継承し、RemoteDataSource#fetchTacklesに渡すようにします。

class_image_02

interface TackleListFetcher {
    fun fetch(): List<Tackle>
}

class DaiwaTackleListFetcher : TackleListFetcher {
    override fun fetch(): List<Tackle> {}
}

class ShimanoTackleListFetcher : TackleListFetcher {
    override fun fetch(): List<Tackle> {}
}

class MariaTackleListFetcher : TackleListFetcher {
    override fun fetch(): List<Tackle> {}
}

class RemoteDataSource {
    fun fetchTackles(fetcher: TackleListFetcher): List<Tackle> {
        return fetcher.fetch()
    }
}

このようにすると、新たにメーカーを追加しても既存コードを修正せずに済みます。

オープン・クローズドの原則の適用範囲

プリンシプルオブプログラミングには、変更が発生しない場合、ただコードが複雑になるだけなので、どのようなコードにもオープン・クローズドの原則を適用するのはやり過ぎだ、と書かれています。

では、どうすれば良いかというと...

  1. 実際に変更が発生するのを待つ。あえて一度目の変更は受け入れて、今後、同じ修正をしなくて良いように、そのとき初めて原則を適用する
  2. 変更の内容を予測するのではなく、変化しそうな部分を予測する

まとめ

個人的には、変更を予想しすぎるがあまり、コードが冗長になったり実装に時間がかかってしまっては良くないので、1.の実際に変更が発生するのを待つというのが、取り入れやすそうだなと思いました。

Discussion

ログインするとコメントできます