Javaには、年齢から派生できないクラスを宣言する力がありましたが、今ではC++にもそれがあるようです。しかし、SOLIDのOpen/Closeの原則に照らして、なぜ私にとって、final
キーワードはfriend
と同じように聞こえますが、これは合法ですが、使用している場合は、おそらくデザインが間違っていると考えられます。クラスは、優れたアーキテクチャまたはデザインパターンの一部になります。
final
意図を表す。これは、クラス、メソッド、または変数のユーザーに、「この要素は変更されないはずであり、変更したい場合、既存の設計を理解していない」と伝えます。
これは重要ですすべてのクラスとすべてのメソッドを変更する可能性があることを予測する必要がある場合、プログラムアーキテクチャは本当に非常に困難になります。サブクラスによって完全に異なります。変更可能であるはずの要素と変更できない要素を前もって決定し、final
を介して変更不可を強制する方がはるかに優れています。
コメントやアーキテクチャドキュメントを介してこれを行うこともできますが、将来のユーザーがドキュメントを読んで従うことを期待するよりも、コンパイラにできることを強制するほうが常に良いです。
壊れやすい基本クラスの問題 を回避します。すべてのクラスには、暗黙的または明示的な保証と不変のセットが付属しています。リスコフ置換原則では、そのクラスのすべてのサブタイプがこれらのすべての保証も提供する必要があることを義務付けています。ただし、final
を使用しない場合、これに違反することは本当に簡単です。たとえば、パスワードチェッカーを用意しましょう。
_public class PasswordChecker {
public boolean passwordIsOk(String password) {
return password == "s3cret";
}
}
_
そのクラスのオーバーライドを許可すると、1つの実装が全員をロックアウトし、別の実装が全員にアクセスを許可する可能性があります。
_public class OpenDoor extends PasswordChecker {
public boolean passwordIsOk(String password) {
return true;
}
}
_
サブクラスは、元のクラスと非常に互換性のない動作をするようになったので、これは通常OKではありません。クラスを他の振る舞いで拡張することを本当に意図している場合、責任の連鎖はより良いでしょう:
_PasswordChecker passwordChecker =
new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
new DefaultPasswordChecker(
new OpenDoor(null)
);
public interface PasswordChecker {
boolean passwordIsOk(String password);
}
public final class DefaultPasswordChecker implements PasswordChecker {
private PasswordChecker next;
public DefaultPasswordChecker(PasswordChecker next) {
this.next = next;
}
@Override
public boolean passwordIsOk(String password) {
if ("s3cret".equals(password)) return true;
if (next != null) return next.passwordIsOk(password);
return false;
}
}
public final class OpenDoor implements PasswordChecker {
private PasswordChecker next;
public OpenDoor(PasswordChecker next) {
this.next = next;
}
@Override
public boolean passwordIsOk(String password) {
return true;
}
}
_
より複雑なクラスが独自のメソッドを呼び出し、それらのメソッドをオーバーライドできる場合、問題はより明白になります。データ構造をきれいに印刷したり、HTMLを書き込んだりするときに、これに遭遇することがあります。各メソッドは、いくつかのウィジェットを担当します。
_public class Page {
...;
@Override
public String toString() {
PrintWriter out = ...;
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print("<head>");
out.print("</head>");
out.print("<body>");
writeHeader(out);
writeMainContent(out);
writeMainFooter(out);
out.print("</body>");
out.print("</html>");
...
}
void writeMainContent(PrintWriter out) {
out.print("<div class='article'>");
out.print(htmlEscapedContent);
out.print("</div>");
}
...
}
_
次に、もう少しスタイルを追加するサブクラスを作成します。
_class SpiffyPage extends Page {
...;
@Override
void writeMainContent(PrintWriter out) {
out.print("<div class='row'>");
out.print("<div class='col-md-8'>");
super.writeMainContent(out);
out.print("</div>");
out.print("<div class='col-md-4'>");
out.print("<h4>About the Author</h4>");
out.print(htmlEscapedAuthorInfo);
out.print("</div>");
out.print("</div>");
}
}
_
これは、HTMLページを生成するためのあまり良い方法ではないことを一瞬無視して、もう一度レイアウトを変更したい場合はどうなりますか?そのコンテンツを何らかの方法でラップするSpiffyPage
サブクラスを作成する必要があります。ここで確認できるのは、テンプレートメソッドパターンの偶発的なアプリケーションです。テンプレートメソッドは、オーバーライドすることを目的とした、基本クラスの明確に定義された拡張ポイントです。
そして、基本クラスが変更されるとどうなりますか? HTMLコンテンツの変更が多すぎると、サブクラスによって提供されるレイアウトが壊れる可能性があります。したがって、後で基本クラスを変更しても安全ではありません。これは、すべてのクラスが同じプロジェクト内にある場合は明らかではありませんが、基本クラスが他の人が構築する公開ソフトウェアの一部である場合は非常に顕著です。
この拡張戦略が意図されていた場合、各パーツの生成方法をユーザーが交換できるようにすることもできます。どちらの場合も、外部から提供できる各ブロックの戦略が存在する可能性があります。または、デコレータをネストすることもできます。これは上記のコードと同等ですが、はるかに明示的ではるかに柔軟です。
_Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());
public interface PageLayout {
void writePage(PrintWriter out, PageLayout top);
void writeMainContent(PrintWriter out, PageLayout top);
...
}
public final class Page {
private PageLayout layout = new DefaultPageLayout();
public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
layout = wrapper.apply(layout);
}
...
@Override public String toString() {
PrintWriter out = ...;
layout.writePage(out, layout);
...
}
}
public final class DefaultPageLayout implements PageLayout {
@Override public void writeLayout(PrintWriter out, PageLayout top) {
out.print("<!DOCTYPE html>");
out.print("<html>");
out.print("<head>");
out.print("</head>");
out.print("<body>");
top.writeHeader(out, top);
top.writeMainContent(out, top);
top.writeMainFooter(out, top);
out.print("</body>");
out.print("</html>");
}
@Override public void writeMainContent(PrintWriter out, PageLayout top) {
... /* as above*/
}
}
public final class SpiffyPageDecorator implements PageLayout {
private PageLayout inner;
public SpiffyPageDecorator(PageLayout inner) {
this.inner = inner;
}
@Override
void writePage(PrintWriter out, PageLayout top) {
inner.writePage(out, top);
}
@Override
void writeMainContent(PrintWriter out, PageLayout top) {
...
inner.writeMainContent(out, top);
...
}
}
_
(追加のtop
パラメータは、writeMainContent
への呼び出しがデコレータチェーンの最上部を通過することを確認するために必要です。これは、と呼ばれるサブクラス化の機能をエミュレートします)再帰を開く。)
複数のデコレータがある場合は、それらをより自由にミックスできるようになりました。
既存の機能をわずかに変更したいという要望よりもはるかに頻繁に、既存のクラスの一部を再利用したいという要望があります。私は、誰かがアイテムを追加してすべてを反復できるクラスを望んでいるケースを見てきました。正しい解決策は次のとおりです。
_final class Thingies implements Iterable<Thing> {
private ArrayList<Thing> thingList = new ArrayList<>();
@Override public Iterator<Thing> iterator() {
return thingList.iterator();
}
public void add(Thing thing) {
thingList.add(thing);
}
... // custom methods
}
_
代わりに、サブクラスを作成しました:
_class Thingies extends ArrayList<Thing> {
... // custom methods
}
_
これは突然、ArrayList
のインターフェース全体がourインターフェースの一部になったことを意味します。ユーザーは、特定のインデックスでremove()
を実行したり、get()
を実行したりできます。これはそのように意図されていましたか? OK。しかし、多くの場合、すべての結果を注意深く検討しません。
したがって、次のことをお勧めします
extend
しないでください。final
としてマークしてください。この「ルール」を破る必要がある多くの例がありますが、通常は、優れた柔軟な設計に導き、基本クラスの意図しない変更(または基本クラスのインスタンスとしての意図しないサブクラスの使用)によるバグを回避します)。
一部の言語には、より厳しい強制メカニズムがあります。
virtual
として明示的にマークする必要がありますJoshua BlochによるEffective Java、2nd Editionについてまだ誰も言及していないことに驚きます(Java開発者少なくとも)本の項目17はこれを詳細に説明し、タイトルが付けられています: "継承のための設計と文書化、またはそれ以外の場合は禁止"。
私は本のすべての良いアドバイスを繰り返すことはしませんが、これらの特定の段落は関連しているようです:
しかし、通常の具象クラスはどうですか?伝統的に、それらは最終的なものでも、サブクラス化のために設計および文書化されたものでもありませんが、この状況は危険です。そのようなクラスで変更が行われるたびに、クラスを拡張するクライアントクラスが壊れる可能性があります。これは単なる理論上の問題ではありません。継承のために設計および文書化されていない非最終的な具象クラスの内部を変更した後、サブクラス化関連のバグレポートを受け取ることは珍しくありません。
この問題の最善の解決策は、安全にサブクラス化されるように設計および文書化されていないクラスでサブクラス化を禁止することです。サブクラス化を禁止する方法は2つあります。 2つの方法の方が簡単なのは、クラスをfinal宣言することです。代わりに、すべてのコンストラクターをプライベートまたはパッケージプライベートにし、コンストラクターの代わりにパブリックstaticファクトリーを追加します。内部でサブクラスを使用する柔軟性を提供するこの代替案は、項目15で説明されています。どちらのアプローチでもかまいません。
final
が役立つ理由の1つは、親クラスの規約に違反するような方法でクラスをサブクラス化できないことです。そのようなサブクラス化は、SOLID(ほとんどすべての "L"))の違反であり、クラスfinal
を作成するとそれが防止されます。
典型的な例の1つは、サブクラスを可変にするような方法で不変クラスをサブクラス化することを不可能にすることです。特定のケースでは、このような動作の変更は非常に驚くべき効果につながる可能性があります。たとえば、実際には変更可能なサブクラスを使用しているのに、キーが不変であると考えてマップでキーとして何かを使用する場合です。
Javaでは、String
をサブクラス化して変更可能にした場合(または誰かがそのメソッドを呼び出したときにホームにコールバックして、機密データをシステムから引き出した場合)、多くの興味深いセキュリティ問題が発生する可能性があります。 )これらのオブジェクトは、クラスのロードとセキュリティに関連するいくつかの内部コードの周りに渡されるためです。
また、Finalは、メソッド内で2つの事柄に同じ変数を再利用するなどの単純な間違いを防ぐのに役立つこともあります。Scalaでは、Javaのfinal変数にほぼ対応するval
のみを使用することをお勧めします。実際、var
または非final変数の使用は疑わしく見られます。
最後に、コンパイラーは、少なくとも理論的には、クラスまたはメソッドが最終であることを知っている場合、追加の最適化を実行できます。これは、最終クラスでメソッドを呼び出すと、呼び出されるメソッドが正確にわかるため、移動する必要がないためです。仮想メソッドテーブルを使用して継承を確認します。
2番目の理由は performance です。最初の理由は、一部のクラスには、システムを機能させるために変更する必要のない重要な動作または状態があるためです。たとえば、「PasswordCheck」というクラスがあり、そのクラスを構築するためにセキュリティの専門家のチームを雇った場合、このクラスは十分に研究され定義されたプロコルを持つ数百のATMと通信します。大学を出たばかりの新入社員が、上記のクラスを拡張する "TrustMePasswordCheck"クラスを作成することを許可すると、私のシステムにとって非常に有害になる可能性があります。これらのメソッドはオーバーライドされるべきではありません、それだけです。
クラスが必要な場合は、クラスを作成します。サブクラスが必要なければ、サブクラスは気にしません。クラスが意図したとおりに動作することを確認します。クラスを使用する場所では、クラスが意図したとおりに動作すると想定しています。
誰かが私のクラスをサブクラス化したい場合、私は何が起こるかについての責任を完全に否定したいと思います。クラスを「最終」にすることでそれを達成します。サブクラス化したい場合は、クラスの作成中にサブクラス化を考慮しなかったことを思い出してください。したがって、クラスのソースコードを取得し、「最終版」を削除する必要があります。それ以降、発生することは完全にあなたの責任です。
それは「オブジェクト指向ではない」と思いますか?私はそれがすることになっていることをするクラスを作るために支払われました。サブクラス化できるクラスを作ったことに対して誰も私にお金を払わなかった。私のクラスを再利用可能にするために支払われた場合、それを行うことができます。 「final」キーワードを削除することから始めます。
(それ以外の場合、 "final"は多くの場合、実質的な最適化を許可します。たとえば、Swift "final"は、パブリッククラスまたはパブリッククラスのメソッドで、コンパイラが完全にメソッド呼び出しがどのコードを実行するかを知っており、動的ディスパッチを静的ディスパッチに置き換えることができ(小さな利点)、静的ディスパッチをインラインに置き換えることができます(おそらく大きな利点)。
adelphus:「サブクラス化したい場合は、ソースコードを取得し、「最終版」を削除し、それはあなたの責任です」について理解するのが難しいのは何ですか? 「最終」は「公正な警告」に等しい。
そして、私は再利用可能なコードを作るために支払われません。私はそれがすることになっていることをするコードを書くために支払われます。似たようなコードを2つ作成するために支払われた場合は、共通部分を抽出します。これは、コストが安く、時間の無駄に支払われないためです。再利用されないコードを再利用可能にすることは、私の時間の無駄です。
M4ks:あなたは常に外部からのアクセスを想定していないすべてのものを非公開にします。この場合も、サブクラス化する場合は、ソースコードを取得し、必要に応じて「保護された」ものに変更し、実行する処理について責任を負います。私がプライベートとマークしたものにアクセスする必要があると思われる場合は、自分が何をしているのかをよく理解してください。
両方:サブクラス化は、コードの再利用のごく一部です。サブクラス化せずに適応できるビルディングブロックを作成することは、ブロックのユーザーが取得したものに依存できるため、はるかに強力であり、「ファイナル」から大きな恩恵を受けます。
プラットフォームのSDKに次のクラスが付属しているとしましょう。
class HTTPRequest {
void get(String url, String method = "GET");
void post(String url) {
get(url, "POST");
}
}
アプリケーションはこのクラスをサブクラス化します。
class MyHTTPRequest extends HTTPRequest {
void get(String url, String method = "GET") {
requestCounter++;
super.get(url, method);
}
}
すべてが順調ですが、SDKに取り組んでいる誰かがget
にメソッドを渡すのはばかげていると判断し、下位互換性を確実に適用することで、インターフェイスを改善します。
class HTTPRequest {
@Deprecated
void get(String url, String method) {
request(url, method);
}
void get(String url) {
request(url, "GET");
}
void post(String url) {
request(url, "POST");
}
void request(String url, String method);
}
上からのアプリケーションが新しいSDKで再コンパイルされるまで、すべてが正常に見えます。突然、オーバーライドされたgetメソッドが呼び出されなくなり、リクエストがカウントされなくなります。
一見無害な変更の結果、サブクラスが破損するため、これは脆弱な基本クラスの問題と呼ばれます。クラス内で呼び出されるメソッドを変更すると、サブクラスが壊れる可能性があります。つまり、ほとんどすべての変更によってサブクラスが壊れる可能性があります。
Finalは、クラスがサブクラス化されるのを防ぎます。そうすれば、クラス内のどのメソッドを変更しても、どこで誰かがどのメソッドの呼び出しが行われるかに依存することを心配する必要はありません。
最終的には、クラスは、下流の継承ベースのクラス(何もないため)またはクラスのスレッドセーフティに関する問題(フィールドの最後のキーワードによって妨げられる場合があると思う)に影響を与えることなく、将来的に変更しても安全であることを意味しますいくつかのスレッドベースのハイジンクス)。
最終的には、あなたのベースに依存している他の人々のコードに意図しない動作の変更が忍び寄ることなく、クラスの動作を自由に変更できることを意味します。
例として、私はHobbitKillerというクラスを作成します。これは素晴らしいことです。すべてのホビットはトリッキーであり、おそらく死ぬはずです。スクラッチ、彼らはすべて死ぬ必要があります。
これを基本クラスとして使用し、火炎放射器を使用するための素晴らしい新しいメソッドを追加しますが、ホビットを対象とするための優れたメソッドがあるため(トリックに加えて、それらは迅速です)、クラスを基本として使用します。火炎放射器の狙いを定めるために使用します。
3か月後、ターゲティング方法の実装を変更しました。さて、あなたが知らないうちにライブラリをアップグレードするある時点で、依存する(そして通常は制御しない)スーパークラスメソッドが変更されたため、クラスの実際のランタイム実装は根本的に変更されました。
したがって、私が良心的な開発者であり、自分のクラスを使用して将来へのホビットの死をスムーズに行えるようにするには、拡張可能なクラスに加えた変更に非常に注意する必要があります。
特にクラスを拡張するつもりである場合を除いて、拡張機能を削除することで、自分自身(そしてできれば他の人)の頭痛の種をたくさん救っています。
私にとってそれはデザインの問題です。
従業員の給与を計算するプログラムがあるとします。国に基づいて2つの日付間の就業日数を返すクラス(国ごとに1つのクラス)がある場合、その最終版を配置し、すべての企業がカレンダーのみに休日を提供する方法を提供します。
どうして?シンプル。開発者がクラスWorkingDaysUSAmyCompanyの基本クラスWorkingDaysUSAを継承し、それを変更して、ストライキ/メンテナンス/火星の2番目の理由にかかわらず、企業が閉鎖されることを反映したいとします。
クライアントの注文と配送の計算は遅延を反映し、実行時にWorkingDaysUSAmyCompany.getWorkingDays()を呼び出すとそれに応じて機能しますが、休暇時間を計算するとどうなりますか?火星の2日をみんなの休日として追加する必要がありますか?いいえ。しかし、プログラマは継承を使用し、クラスを保護しなかったので、混乱を招く可能性があります。
または、彼らがクラスを継承および変更して、この会社が土曜日に働いていない国で土曜日にハーフタイムで働いていることを反映するとします。次に、地震、電力危機、または何らかの状況により、大統領は最近ベネズエラで起こったように3日間の休業を宣言します。継承されたクラスのメソッドが毎週土曜日にすでに減算している場合、元のクラスの変更により、同じ日に2回減算される可能性があります。各クライアントの各サブクラスに移動して、すべての変更に互換性があることを確認する必要があります。
解決?クラスをfinalにして、addFreeDay(companyID mycompany、Date freeDay)メソッドを提供します。そうすれば、WorkingDaysCountryクラスを呼び出すときに、それがサブクラスではなくメインクラスであることが確実になります。
final
の使用は、決してSOLIDの原則の違反ではありません。残念ながら、開閉原理を解釈することは非常に一般的です(「ソフトウェアエンティティは開いている必要があります。拡張用ですが、変更のために閉じられています」)は、「クラスを変更するのではなく、それをサブクラス化して新しい機能を追加する」という意味です。これは、本来そのクラスが意図したものではなく、通常、その目的を達成するための最良のアプローチではないと考えられています。
OCPに準拠する最良の方法は、オブジェクトに依存関係を注入することでパラメーター化された抽象的な動作を具体的に提供することにより、拡張ポイントをクラスに設計することです(たとえば、Strategyデザインパターンを使用)。これらの動作は、新しい実装が継承に依存しないようにインターフェイスを使用するように設計する必要があります。
もう1つのアプローチは、パブリックAPIを使用してクラスを抽象クラス(またはインターフェース)として実装することです。その後、同じクライアントにプラグインできるまったく新しい実装を作成できます。新しいインターフェースで元のインターフェースとほぼ同様の動作が必要な場合は、次のいずれかを行うことができます。