yujiro's blog

エンジニアリング全般の事書きます

オープン・クローズドの原則(OCP)について

Robert C. Martin のCleanArchitecture にはSOLID原則について記載があるが、どの原則もアーキテクチャの観点から論じられている。

「単一責任の原則(SRP)」をコンポーネント向けに言い換えたものが「閉鎖性共通の原則(CCP)」にあたり

「インターフェース分離の原則(ISP)」ををコンポーネント向けに言い換えたものが全再利用の原則(CRP) にあたるのは以前書いた記事でも少し述べた。

bamboo-yujiro.hatenablog.com

「オープン・クローズドの原則」もアーキテクチャの単位で述べられているのだけれど、 まずは理解を深めるために、ここでは一般的なオープン・クローズドの原則(OCP)についてまとめてみようと思う。


オープン・クローズドの原則というのは、SOLID 原則のO にあたるもので「修正に対して閉じていて、拡張に対して開いていなければいけない」と言われているものである。

この「修正に対して閉じていて、拡張に対して開いていなければいけない」を言い換えると、

「ソフトウェアの振る舞いは、既存のコードを変えずに、新しくコードを追加するだけで拡張できなければならない」

ということである。

ソフトウェアの振る舞いの拡張機能の追加 と言い換えても良いかもしれない。

ポイントになるのは抽象ポリモーフィズム

具体例を出そう。

社員の名前と年齢、所属部署を出力する例を考えてみる。

今まで管理画面上からCSV でダウンロードできるようになっていたけど、HTML でもダウンロードできるようにしてほしいという依頼があったとする。

OCP に則っていないプログラムでは下記のようになる。

class Outputter {

    let memberList: [[String: Any]]
    let outputType: OutputType
    
    enum OutputType {
        case csv
        case html
    }

    init(memberList: [[String: Any]],
        type: OutputType) {
        self.memberList = memberList
    }

    func reportString() {
        switch type {
        case .csv:
            // ここでCSV文字列を生成する
        case .html
            // ここでHTML文字列を生成する
        }
    }
}

class MemberListMaker {
    
    private let outputter: Outputter

    func init(outputter: Outputter) {
        self.outputter = outputter
    }

    func download() {
        let output = self.outputter.reportString()
        // ダウンロード処理
    }
}

これだと今後、「xml 形式にしたい」とか「マークダウン形式にしたい」といった要望がでてきたときにOutputter クラスのreportString メソッドを改変する必要がでてくる。

これをOCP に沿う形で実装してみる。

protocol Outputter {
    init(memberList: [[String: Any]])
    func reportString()
}

class HtmlOutputter: Outputter {

    private let memberList: [[String: Any]]

    init(memberList: [[String: Any]]) {
        self.memberList = memberList
    }

    func reportString() {
        // ここでHTML文字列を生成する
    }
}

class CSVOutputter: Outputter {

    private let memberList: [[String: Any]]

    init(memberList: [[String: Any]]) {
        self.memberList = memberList
    }

    func reportString() {
        // ここでCSV文字列を生成する
    }
}

class MemberListMaker {
    
    private let outputter: Outputter

    func init(outputter: Outputter) {
        self.outputter = outputter
    }

    func download() {
        let output = self.outputter.reportString()
        // ダウンロード処理
    }
}

この形であれば、今後、フォーマットを増やしたかったら、Outputter インターフェースに準拠したクラス(XMLOutputter や MarkdownOutputter)を生成してMemberListMaker に渡してやればよい。

これで、既存コードに手を加えずに機能を拡張できる。

先の「ソフトウェアの振る舞いは、既存のコードを変えずに、新しくコードを追加するだけで拡張できなければならない」の達成ができた。


次回は、この原則をアーキテクチャ下で当てはめたときにどうなるかについてまとめてみようと思う。