読者です 読者をやめる 読者になる 読者になる

final について?

final 周辺について。理想論?いや、理想論大事。

OCP (Open-Closed Principle)

開放閉鎖原則とも。
簡単に言うと、モジュール (ここでは class) は拡張できるべきだが、修正は行うべきではない、という原則。
これを原則に従うと、(もっと一般的な意味での) 修正が容易になる。
「拡張できるべき」と「修正は行うべきではない」を両立しないといけないので、一見、継承はこの原則を守るためには使っても良さそうなものだけど・・・

実装の継承が OCP を破る例

例えば、

class Rectangle {
    int w;
    int h;
    Rectangle(int w, int h) {
        this.w = w;
        this.h = h;
    }
    
    int width() { return w; }
    int height() { return h; }
    
    void setWidth(int newW) { w = newW; }
    void setHeight(int newH) { h = newH; }
}

こんなクラスがあったとする。
で、正方形が欲しくなったとして、Rectangle から継承したとする。

class Square extends Rectangle {
    Square(int l) {
        super(l, l);
    }
    
    @Override
    void setWidth(int newL) { w = h = newL; }
    
    @Override
    void setHeight(int newL) { w = h = newL; }
}

こんな感じ・・・なのだが、例えば以下のようなメソッドが既存コード中に存在したとする。

void method(Rectangle rect) {
    rect.setWidth(20);
    rect.setHeight(10);
    
    assert rect.width() != rect.height();
}

このコードには、Rectangle はもちろん、Square のインスタンスも渡すことが出来てしまうが、アサーションで引っかかってしまう。
つまり、既存コードに修正が必要となる

LSP (Liskov Substitution Principle)

リスコフの置換原則とも。
簡単に言うと、サブクラスはスーパークラスと同じように使えなければならない、という原則。
上の例は、実はこちらの原則を破っているために OCP 違反にもなっている例になっている。
LSP に違反すると、OCP にも違反することになるため、こちらの原則を満たせないような「拡張性」は、何かしら既存コードを「修正」する必要性がある、ということになる*1
ではどうすれば LSP を守れるのか。

DbC (Design by Contract)

契約による設計とも。
LSP はこの DbC とつながりがあり、以下の条件を満たすようにメソッドをオーバーライドすれば、LSP を守ることが出来る。

  • 事前条件はスーパークラスの事前条件と同じにするか、弱い条件と置き換える
  • 事後条件はスーパークラスの事後条件と同じにするか、強い条件と置き換える

実際的な例を出すと、オーバーライドするメソッドでは、

  • より広い入力を受け付ける (より広い定義域)
  • より抽象的な型を受け付ける
  • より狭い出力を返す (より狭い値域)
  • より具体的な型を返す

ようにする、などなど。

でも・・・

Java で事前条件はともかく、事後条件って書きにくいよね・・・

そこで final!

final 付けとけば LSP 守るの簡単だよ!
ってことで上の例はどうすれば良かったのか?の一例。

public interface Rectangle {
    int width();
    int height();
    void setWidth(int newW);
    void setHeight(int newH);
}

final class ResizableRectangle implements Rectangle {
    int w;
    int h;
    ResizableRectangle(int w, int h) {
        this.w = w;
        this.h = h;
    }
    
    public int width() { return w; }
    public int height() { return h; }
    
    public void setWidth(int newW) { w = newW; }
    public void setHeight(int newH) { h = newH; }
}

void method(Rectangle rect) {
    rect.setWidth(20);
    rect.setHeight(10);
    
    assert rect.width() != rect.height();
}

このクラスは、継承を final により禁止しているので、LSP はもちろん破っていない。というか破りようがない。
また、既存コードを変更することなく、Square クラスを追加するという形で拡張することが出来るため、OCP 違反にもなっていない。

問題になるところ

いいことばかりではない。
例えば、上の例では ResizableRectangle は final なので継承できない。そら、LSP と OCP 守るために final 付けたんだから継承できるわけないんだけど・・・
ResizableRectangle で何か超複雑なことをしていて、その機能が欲しい・・・でも、ResizableRectangle のソースはない。
こういう状況だったら委譲を使うことになるだろうけど、Java で委譲・・・いくら IDE のサポートがあるとはいえ、何とかならんものか・・・
ResizableRectangle が final でさえなければ、継承という手っ取り早い方法が使えるのだけれども・・・

結局 final は付けるべき?付けないべき?

それは、まぁ、場合によるよね、としか。
final を付けるか付けないか迷うと言うことは、そのクラス設計がおかしいのではないか?とか言ってみる。
final を外したいという衝動が「流動的な要素をなにか見逃していないか」という不安から来ているのだとすれば、もう少しクラスを分割してみるとか。
メソッドの引数として受け取るクラスが final でどうしようもない・・・と言うときは、より抽象的なクラスをメソッドの引数にすべきではなかったのかとか*2


final を多用したとしても、出来る限りそれを苦に思わせないようなやり方、ってのはあると思うんです。
現在あるライブラリ *3 で、final に苦しめられたからと言って、無条件で「final 付けるな」というのは、無条件で「final 付けろ」というのと変わらないんじゃないですかね。
委譲が面倒って、それ SRP *4 破りまくってるからじゃないですかね。
つか、package private なクラスだったら final 付けなくても final のようなもんなんですよね。class はすべて何らかの interface を実装しなければならない、という極論を言うつもりはないんですけど、public な部分に関してはこれ言っちゃっていいんじゃないだろうか・・・*5 *6
・・・と、半ば本気で思っていたり。夢見がちな年頃なのです。


以下追記

ここら辺の話は何読めばいいの?

とりあえず、この 2 冊。

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)

オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)


アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技

あと流動的要素とかその辺の話。

デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)

デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)

*1:もちろん、上で例示した method の様なコードがなければ問題は表面化しない。しかし、逆に言うと上のようなメソッドを追加してしまった場合、修正が必要となるので結局 OCP を破っている

*2:これを推し進めると、public な API では final な具象型を引数に取らいとか、そういう方向に進むことに

*3:標準のものを含む

*4:Single Responsibility Principle:単一責任原則

*5:値クラスは別かな・・・とは言っても、値クラスは値クラスで、Comparable とか Serializable とか実装してる可能性ががが

*6:ユーティリティクラスは別か。そもそもユーティリティクラスがあまり好きではないのだけど、それはまた別の話