finalの使いどころ、継承を禁止して意図を明示する

finalは、クラスの継承やメソッドのオーバーライドを禁止する修飾子です。ここは拡張ポイントではないという設計の意図を型で明示し、意図しない継承を防ぎます。

finalは「これ以上いじるな」の宣言

final を付けると、そのクラスは継承できず、メソッドはオーバーライドできなくなります。制限を増やす修飾子なので敬遠されがちですが、私はむしろ「デフォルトで付けておく」くらいの気持ちで使っています。継承される前提のクラスは意外と少なく、開いたままにしておくと想定外の使われ方をされる余地が残るからですね。

final class Money
{
    public function __construct(private int $amount) {}

    public function add(Money $other): self
    {
        return new self($this->amount + $other->amount);
    }
}

// class SpecialMoney extends Money {} // これは書けない

値オブジェクトのように「不変で、振る舞いが完結している」クラスは final と相性がいいです。継承で無理やり中身を変えられると不変性が壊れかねないので、閉じておくほうが安全だと思います。

意図を型で伝える

final の効能は、実行時の制約というより「このクラスは拡張ポイントではない」という意図が読み手に伝わることだと思っています。継承していい親クラスなのか、それとも単体で使い切る部品なのか、宣言を見ただけで判断できるわけですね。設計の意図をコメントではなくコードで残せるのは地味に大きいです。

// メソッド単位でも封じられる
class BaseController
{
    final public function handle(Request $req): Response
    {
        // この流れは変えさせない
        return $this->process($req);
    }

    protected function process(Request $req): Response { /* ... */ }
}

handle() だけを final にして「入口の流れは固定、中身の process() は自由」とする書き方も便利ですね。テンプレートメソッドで骨組みを守りたいときに効きます。

拡張性とのバランス

もちろん、拡張されることを前提にしたライブラリのクラスに final を付けると、利用者が困る場面もあります。要は「拡張してほしいか、してほしくないか」を都度考えて選ぶということですね。テストでモック化したいクラスに final を付けると差し替えづらくなることもあるので、そこはインターフェースを切って回避する、といった判断が要ります。

まとめ

final は不自由を押し付ける道具に見えて、実際は「意図しない継承を防ぎ、設計の意思を明示する」ための道具だと思います。迷ったらまず final にしておき、拡張が必要になった時点で外す、くらいの運用が私には合っています。開けっ放しより閉じてから開けるほうが、後で困りにくい気がします。

よくある質問

Q. finalを付けると何ができなくなりますか。
A. そのクラスは継承できず、finalを付けたメソッドはオーバーライドできなくなります。

Q. クラス全体でなくメソッドだけ封じられますか。
A. できます。メソッドにfinalを付ければ、入口の流れは固定しつつ他のメソッドは自由にオーバーライドさせられます。

Q. finalだとモックが作れず困りませんか。
A. 差し替えたい箇所はインターフェースを切って回避します。拡張が必要になった時点でfinalを外す運用もしやすいです。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です