どんな時にクラスを final と宣言するのか
まとめ:インタフェースを実装していて他のパブリックメソッドが定義されていない場合、いつもクラスを final
にしてください。
この 1 ヶ月で、私は PHP クラスへの final
の使い方について何度か議論をしました。
そして以下のような流れが繰り返されました。
- 私が新しく作られたクラスへ
final
を宣言するよう頼む - コードを書いた人は嫌がり
final
は柔軟性を損なうと主張 - 柔軟性は良い抽象化から生まれるのであって、継承から生まれるのではないという説明が必要となる
この流れから明らかなのは、コードを書く人達にどんな時に final
を使うのか、そしてまたどんな時に避けるべきなのかについて、より良い説明が必要ということです。
この話題については他にも幾つもの記事があるのですが、この記事の主旨は、将来同じ質問をしてくる人への「クイックリファレンス」となることです。
いつ "final" を使うのか
final
は可能な限りいつでも使うべきです。
なぜ final を使わなければならないのか
あるクラスを final
とする理由は幾つもあります。個人的に特に大事だと思うものをリストアップして、それぞれ説明してみましょう。
1. 大規模な継承連鎖を防ぐ
開発者には悪い習慣があり、それは問題を解決するために既存の(不適切な)対応の具体的なサブクラスを作ろうとすることです。
多分以下のような例から見てとれるはずです。
<?php
class Db { /* ... */ }
class Core extends Db { /* ... */ }
class User extends Core { /* ... */ }
class Admin extends User { /* ... */ }
class Bot extends Admin { /* ... */ }
class BotThatDoesSpecialThings extends Bot { /* ... */ }
class PatchedBot extends BotThatDoesSpecialThings { /* ... */ }
これは、疑いようもなく、コードをデザインする際にとるべきではないやり方です。
上述のアプローチを通常採用するのは、開発者の中でも OOP(Object Oriented Programming) を「問題を継承によって解決する方法」と取り違えている人達です(「継承志向プログラミング」とでもいったところでしょうか?)。
2. 集約(composition)の奨励
一般に継承を(デフォルトとして)強制的なやり方で防ぐことには利点があり、開発者にもっと集約について考えさせることができます。
既存のコードが継承によって機能の詰め込みを少なくできる、というのは私の考えでは、feature creep1 と結び付いた軽率さのあらわれです。
以下の単純な例について考えてみましょう。
<?php
class RegistrationService implements RegistrationServiceInterface
{
public function registerUser(/* ... */) { /* ... */ }
}
class EmailingRegistrationService extends RegistrationService
{
public function registerUser(/* ... */)
{
$user = parent::registerUser(/* ... */);
$this->sendTheRegistrationMail($user);
return $user;
}
// ...
}
RegistrationService
を final
とすることで、EmailingRegistrationService
をその子クラスとするというアイディアは分かりやすく禁止され、先に示されたような愚かな間違いは簡単に避けることができます。
<?php
final class EmailingRegistrationService implements RegistrationServiceInterface
{
public function __construct(RegistrationServiceInterface $mainRegistrationService)
{
$this->mainRegistrationService = $mainRegistrationService;
}
public function registerUser(/* ... */)
{
$user = $this->mainRegistrationService->registerUser(/* ... */);
$this->sendTheRegistrationMail($user);
return $user;
}
// ...
}
3. 公開 API について考える事を開発者に強制できる
開発者達は継承を使ってアクセサーを加え、既存クラスへ API を追加しようとする傾向があります。
<?php
class RegistrationService implements RegistrationServiceInterface
{
protected $db;
public function __construct(DbConnectionInterface $db)
{
$this->db = $db;
}
public function registerUser(/* ... */)
{
// ...
$this->db->insert($userData);
// ...
}
}
class SwitchableDbRegistrationService extends RegistrationService
{
public function setDb(DbConnectionInterface $db)
{
$this->db = $db;
}
}
この例は思考プロセスにおける幾つかの欠陥を示しており、SwitchableDbRegistrationService
のようなものが作られてしまいます。
-
setDb
メソッドはDbConnectionInterface
を実行時に変更するために使われ、解決すべき別の問題を隠しているように見えます。多分本当に必要なのはMasterSlaveConnection
でしょうか? -
setDb
メソッドはRegistrationServiceInterface
によってカバーされておらず、したがってこれが使えるのはコードを完全にSwitchableDbRegistrationService
へ依存させて使う場合のみであり、幾つかの文脈においてその誓約の意図を台無しにします -
setDb
メソッドは実行時に依存対象を変えますが、これはRegistrationService
のロジックによってサポートされていないかもしれず、バグを招きさえするかもしれません。 -
setDb
メソッドは元々の実装のバグのために導入されたのかもしれません。なぜ修正がこのような形で行われたのでしょうか? これは本当の修正といえるでしょうか? 対症療法に過ぎないのではないでしょうか?
この setDb
の例にはもっと多くの問題がありますが、上述の点は final
がこういった状況を未然に防ぐということを説明するのに、最もふさわしいものです。
4. 開発者がオブジェクトの公開 API を削減するよう強制できる
クラスが多くのパブリックメソッドを持つのは、おそらく SRP(Single Responsiblity Principle: 単一責任の原則) を破っていることが多く、よくあることとして、開発者はそのようなクラスの特定の API をオーバーライドしたくなります。
新規実装は毎回 final
とするようにすれば、新しい API を可能な限り小さく保つことについて、あらかじめ考えさせるよう開発者に強制することができます。
5. final
クラスはいつでも継承可能に変えられる
新規のクラスを final
としてコードしても、任意のタイミングで(本当に必要なら)継承可能に変えることができます。
それで失うものは何もなく、ただしそのような変更をする際は自分自身やチームメンバーに対する説明が必要となり、そしてその議論は変更がマージされる前に、よりよい解決策をもたらすことにつながるかもしれません。
6. extends
はカプセル化を破壊する
書く人が特に拡張可能なものとして設計するのでない限り、実際どうなのかに関わらず、クラスを final
とすることを考えるべきです。
クラスの継承はカプセル化を破壊し、予期しない結果や後方互換性の破壊につながります。extends
キーワードを使う前によくよく考えることです。もっと良いのは、クラスを final
とし、他の人にも継承を使うことを考えないようにすることです。
7. その柔軟性は必要ない
いつも言われることですが、final
はコードベースの使われ方の柔軟性を減らすという意見があります。
これに対する反論は非常にシンプルです。そのような柔軟性は必要ありません。
そもそも何故必要なのでしょうか? なぜあるコントラクトに対する実装を、別途で書くことができないのでしょうか? 何故集約を使わないのでしょうか? 問題に対し注意深く考えてみたでしょうか?
もしまだ final
キーワードの削除が必要だというのであれば、そこには別の種類のコードの臭いがありそうです。
8. 自由にコードを変えられる
一度クラスを final
とすれば、そのコードは納得いくまで変えることができます。
カプセル化の維持は保証されているので、注意しなければならないのは公開 API のみです。
つまり何でも書き換えたいだけ、自由に書き換えることができます。
final
が不要な時
final クラスは以下の前提でのみ有効です。
- その final クラスが実装する抽象(interface)がある
- その final クラスの全ての公開 API がその interface の一部である
もしこれら 2 つの事前条件の内の 1 つが欠けているなら、いずれそのクラスを継承可能としたくなる日がくることでしょう。あなたのコードが真に抽象へ依存してはいないということなのですから。
例外となるのは、ある特定のクラスが表しているのが、ある種の誓約や概念で、それらが完全に不変のものであり、柔軟性がなくシステム全体にグローバルである場合です。その 1 つの好例は数学的な操作です。$calculator->sum($a, $b)
は時を経ても変わることがなさそうなものです。このような場合には、あらかじめ依拠する抽象がなくとも、 final
キーワードを使うことは安全だと考えられます。
他に final
キーワードを使いたくない状況は、既存のクラスです。影響するコードベースがあるので、可能となるのは semver に従いメジャーバージョンを上げる場合のみです。
試してみよう!
この記事を読み終えたなら、自分のコードへ戻り、そしてもしまだやったことがないのなら、あなたの最初の final
を実装するクラスに加える事を検討してみましょう。
続くコードが、期待通りの適切な形となっていくことが分かる筈です。
-
訳注: フィーチャー・クリープ、機能をどんどん足して当初意図を超えて複雑化すること ↩