Javaには長い間派生できないクラスを宣言する力があったように思われますが、現在はC ++にもあります。しかし、SOLIDのOpen / Close原則に照らして、なぜそれが役立つのでしょうか?私には、final
キーワードのように聞こえますfriend
-それは合法ですが、あなたがそれを使用している場合、ほとんどの場合、設計が間違っています。派生不可能なクラスが優れたアーキテクチャまたはデザインパターンの一部になるような例をいくつか提供してください。
final
。
Javaには長い間派生できないクラスを宣言する力があったように思われますが、現在はC ++にもあります。しかし、SOLIDのOpen / Close原則に照らして、なぜそれが役立つのでしょうか?私には、final
キーワードのように聞こえますfriend
-それは合法ですが、あなたがそれを使用している場合、ほとんどの場合、設計が間違っています。派生不可能なクラスが優れたアーキテクチャまたはデザインパターンの一部になるような例をいくつか提供してください。
final
。
回答:
final
意図を表現します。クラス、メソッド、または変数のユーザーに「この要素は変更されることはありません。変更したい場合は、既存の設計を理解していません」と伝えます。
これは重要です。なぜなら、すべてのクラスやメソッドをサブクラスごとにまったく異なる動作をするように変更する可能性があると予想しなければならない場合、プログラムアーキテクチャは非常に難しいからです。変更可能な要素とそうでない要素を事前に決定し、を介して変更不可能な機能を適用することをお勧めしfinal
ます。
コメントやアーキテクチャドキュメントを介してこれを行うこともできますが、将来のユーザーがドキュメントを読んで従うことを期待するよりも、コンパイラにできることを強制させる方が常に良いです。
壊れやすい基本クラスの問題を回避します。すべてのクラスには、暗黙的または明示的な保証と不変式のセットが付属しています。Liskov Substitution Principleでは、そのクラスのすべてのサブタイプもこれらすべての保証を提供する必要があります。ただし、を使用しない場合、これに違反するのは本当に簡単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;
}
}
これは通常、サブクラスの動作が元のものと非常に互換性がないため、通常は問題ありません。もしクラスが他の振る舞いで拡張されることを本当に意図しているなら、責任の連鎖がより良いでしょう:
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
の一部となっている私たちのインターフェース。ユーザーはremove()
物事、またはget()
特定のインデックスで物事をすることができます。これはそのように意図されていましたか?OK。しかし、多くの場合、すべての結果を慎重に検討しません。
したがって、以下をお勧めします
extend
慎重に考えずにクラスを作ることはありません。final
メソッドをオーバーライドする場合を除き、常にクラスにマークを付けます。この「ルール」を破る必要がある多くの例がありますが、通常は適切で柔軟な設計に導き、基本クラスの意図しない変更(または基本クラスのインスタンスとしてのサブクラスの意図しない使用)によるバグを回避します。 )。
一部の言語には、より厳格な実施メカニズムがあります。
virtual
Joshua BlochによるEffective Java、2nd Editionについてはまだ誰も言及していないことに驚いています(少なくともすべてのJava開発者にとって、これを読む必要があります)。本の項目17でこれを詳細に説明しており、タイトルは「継承またはその他の禁止のための設計と文書化」です。
この本ですべての良いアドバイスを繰り返すことはしませんが、これらの特定の段落は関連しているようです:
しかし、通常の具体的なクラスはどうでしょうか?伝統的に、それらは最終的なものでも、サブクラス用に設計および文書化されたものでもありませんが、この状況は危険です。そのようなクラスで変更が行われるたびに、クラスを拡張するクライアントクラスが破損する可能性があります。これは単なる理論上の問題ではありません。継承用に設計および文書化されていない非最終的なコンクリートクラスの内部を変更した後、サブクラス関連のバグレポートを受け取ることは珍しくありません。
この問題の最善の解決策は、安全にサブクラス化されるように設計および文書化されていないクラスのサブクラス化を禁止することです。サブクラス化を禁止するには、2つの方法があります。2つの方が簡単なのは、クラスfinalを宣言することです。別の方法は、すべてのコンストラクターをプライベートまたはパッケージプライベートにし、コンストラクターの代わりにパブリック静的ファクトリーを追加することです。サブクラスを内部で使用する柔軟性を提供するこの代替方法については、項目15で説明します。どちらのアプローチも受け入れられます。
理由の1つは、final
親クラスのコントラクトに違反するような方法でクラスをサブクラス化できないことを保証することです。このようなサブクラス化は、SOLID(何よりも「L」)の違反となり、クラスfinal
を作成することはそれを防ぎます。
典型的な例の1つは、サブクラスを可変にするような方法で不変クラスをサブクラス化することを不可能にすることです。特定の場合、このような動作の変更は非常に驚くべき効果をもたらす可能性があります。たとえば、実際には変更可能なサブクラスを使用しているときにキーが不変であると考えるマップでキーとして何かを使用する場合
Javaでは、String
これらのオブジェクトが渡されるときにサブクラス化して変更可能(または誰かがそのメソッドを呼び出したときにホームにコールバックし、システムから機密データを引き出す可能性がある)できる場合、多くの興味深いセキュリティ問題が導入される可能性がありますクラスのロードとセキュリティに関連するいくつかの内部コードの周り。
また、Finalは、メソッド内の2つのことで同じ変数を再利用するなどの単純なミスを防ぐのに役立つ場合があります。Scalaではval
、Javaの最終変数にほぼ対応するものだけを使用することをお勧めします。var
または非最終変数が疑われて見られます。
最後に、少なくとも理論的には、クラスまたはメソッドがfinalであることがわかっている場合、コンパイラは追加の最適化を実行できます。最終クラスでメソッドを呼び出すと、どのメソッドが呼び出されるかが正確にわかるためです。仮想メソッドテーブルを介して継承を確認します。
SecurityManager
。ほとんどのプログラムはこれを使用しませんが、信頼されていないコードも実行しません。+++ 文字列は不変であると仮定する必要があります。そうしないと、セキュリティがゼロになり、ボーナスとして任意のバグが多数発生します。Java文字列が生産的なコードで変更される可能性があると想定しているプログラマーは間違いなく正気ではありません。
2番目の理由はパフォーマンスです。最初の理由は、一部のクラスには、システムが機能するために変更されるべきではない重要な動作または状態があるためです。たとえば、「PasswordCheck」というクラスがあり、そのクラスを構築する場合、セキュリティの専門家チームを雇い、このクラスはよく研究され定義されたプロトコルで何百ものATMと通信します。大学を卒業したばかりの新入社員が、上記のクラスを拡張する「TrustMePasswordCheck」クラスを作成すると、システムに非常に有害になる可能性があります。これらのメソッドはオーバーライドされることは想定されていません、それだけです。
クラスが必要なときは、クラスを作成します。サブクラスが必要ない場合、サブクラスは気にしません。クラスが意図したとおりに動作することを確認し、クラスを使用する場所では、クラスが意図したとおりに動作することを前提としています。
誰かが私のクラスをサブクラス化したい場合、私は何が起こったとしても責任を完全に否定したいと思います。クラスを「最終」にすることでそれを達成します。サブクラス化する場合は、クラスの作成時にサブクラス化を考慮しなかったことを思い出してください。したがって、クラスのソースコードを取得し、「最終」を削除する必要があります。それ以降は、発生するすべての責任を完全に負います。
それは「オブジェクト指向ではない」と思いますか?私はそれがすることになっていることをするクラスを作るために支払われました。サブクラス化できるクラスを作成したことに対して、誰も私に支払いをしませんでした。私のクラスを再利用可能にするために報酬を受け取った場合は、歓迎します。「final」キーワードを削除することから始めます。
(それ以外では、「最終」は多くの場合最適化を可能にします。たとえば、パブリッククラスまたはパブリッククラスのメソッドのSwift「最終」では、コンパイラはメソッド呼び出しが実行するコードを完全に認識できることを意味します。動的ディスパッチを静的ディスパッチに置き換えることができ(小さな利点)、多くの場合、静的ディスパッチをインライン化に置き換えることができます(おそらく大きな利点))。
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は、だれもがクラスをサブクラス化することを防ぎます。そうすれば、クラス内のどのメソッドをどこで誰かが正確にどのメソッド呼び出しを行うかに依存することを心配することなく変更できます。
最終的には、クラスは、下流の継承ベースのクラスに影響を与えることなく(クラスがないため)、クラスのスレッドの安全性に関する問題に影響を与えずに将来変更しても安全であることを意味しますいくつかのスレッドベースの高ジンクス)。
Finalは、あなたのベースに依存している他の人のコードに意図しない動作の変更を加えることなく、クラスの動作を自由に変更できることを意味します。
例として、私はHobbitKillerと呼ばれるクラスを作成します。これは素晴らしいです。なぜなら、すべてのホビットは巧妙で、おそらく死ぬはずだからです。スクラッチ、彼らは間違いなく死ぬ必要があります。
これを基本クラスとして使用し、火炎放射器を使用するための素晴らしい新しいメソッドを追加しますが、ホビットをターゲットにするための素晴らしい方法があるため、私のクラスをベースとして使用します火炎放射器の照準を合わせるために使用します。
3か月後、ターゲット方法の実装を変更します。今、あなたが知らないうちにライブラリをアップグレードする将来のある時点で、あなたが依存するスーパークラスメソッドの変更のために、クラスの実際のランタイム実装が根本的に変更されました(そして一般的には制御しません)。
ですから、私が良心的な開発者であり、クラスを使ってホビットをスムーズに死ぬようにするには、拡張可能なクラスに加えた変更に非常に注意する必要があります。
クラスを拡張することを明確に意図している場合を除いて、拡張機能を削除することで、多くの頭痛を回避できます。
final
の使用はfinal
、SOLID原則の違反ではありません。残念ながら、Open / Closed Principle(「ソフトウェアエンティティは拡張のために開かれるが、修正のために閉じられる」)を「クラスを修正するのではなく、サブクラス化し、新しい機能を追加する」ことを意味するものとして解釈することは非常に一般的です。これはもともとそれが意図したものではなく、一般的にその目標を達成するための最良のアプローチではないと考えられています。
OCPに準拠する最善の方法は、オブジェクトに依存関係を注入することによってパラメーター化された抽象動作を具体的に提供することにより、拡張ポイントをクラスに設計することです(たとえば、Strategyデザインパターンを使用)。これらの動作は、新しい実装が継承に依存しないように、インターフェイスを使用するように設計する必要があります。
別のアプローチは、パブリックAPIを抽象クラス(またはインターフェイス)としてクラスを実装することです。その後、同じクライアントにプラグインできるまったく新しい実装を作成できます。新しいインターフェイスが元のインターフェイスとほぼ同様の動作を必要とする場合、次のいずれかを実行できます。
私にとっては、デザインの問題です。
従業員の給与を計算するプログラムがあるとします。国に基づいて2つの日付間の就業日数を返すクラスがある場合(国ごとに1つのクラス)、その最終日を置き、すべての企業がカレンダーにのみ休日を提供する方法を提供します。
どうして?シンプル。開発者がクラスWorkingDaysUSAmyCompanyの基本クラスWorkingDaysUSAを継承し、ストライキ/メンテナンス/火星の2番目の理由で企業が閉鎖されることを反映するように変更したいとします。
クライアントの注文と配達の計算は遅延を反映し、実行時にWorkingDaysUSAmyCompany.getWorkingDays()を呼び出すとそれに応じて動作しますが、休暇時間を計算するとどうなりますか?火星の2番目をすべての人の休日として追加する必要がありますか?いいえ。しかし、プログラマは継承を使用しているため、クラスを保護しなかったため、混乱を招く可能性があります。
または、この会社が土曜日に半時間働いている土曜日に働いていないことを反映するために、クラスを継承および変更するとします。その後、地震、電力危機、または何らかの事情により、大統領は最近ベネズエラで起こったように3日間の休業日を宣言します。継承されたクラスのメソッドが毎週土曜日にすでに減算されている場合、元のクラスを変更すると、同じ日が2回減算される可能性があります。各クライアントの各サブクラスに移動し、すべての変更に互換性があることを確認する必要があります。
解決?クラスをfinalにして、addFreeDay(companyID mycompany、Date freeDay)メソッドを提供します。そうすれば、WorkingDaysCountryクラスを呼び出すときに、サブクラスではなくメインクラスになります。
final
ですか?多くの人々(私を含む)は、すべての非抽象クラスを作成するのが良いデザインであることに気付きfinal
ます。