「final」キーワードが役立つのはなぜですか?


54

Javaには長い間派生できないクラスを宣言する力があったように思われますが、現在はC ++にもあります。しかし、SOLIDのOpen / Close原則に照らして、なぜそれが役立つのでしょうか?私には、finalキーワードのように聞こえますfriend-それは合法ですが、あなたがそれを使用している場合、ほとんどの場合、設計が間違っています。派生不可能なクラスが優れたアーキテクチャまたはデザインパターンの一部になるような例をいくつか提供してください。


43
クラスに注釈が付いているのに、クラスが間違って設計されていると思うのはなぜfinalですか?多くの人々(私を含む)は、すべての非抽象クラスを作成するのが良いデザインであることに気付きfinalます。
発見

20
継承よりも合成を優先すると、すべての非抽象クラスを持つことができますfinal
アンディ

24
オープン/クローズの原則は、ある意味で、マントラがクラスから継承するクラスの階層を作成し、他のクラスから継承するという20世紀の時代錯誤です。これは、オブジェクト指向プログラミングを教えるのには便利でしたが、実際の問題に適用すると、もつれた、維持できない混乱を引き起こすことが判明しました。クラスを拡張可能に設計するのは困難です。
デビッドハンメン

34
@DavidArnoばかげてはいけません。継承は、必須条件でオブジェクト指向プログラミングの、および特定の過度に独断的な個人が宣べ伝えたいと、それはどこにも近いような複雑なまたは厄介です。他のツールと同様にツールであり、優れたプログラマーは仕事に適切なツールを使用する方法を知っています。
メイソンウィーラー

48
回復する開発者として、私はfinalが重要なセクションに有害な動作が入らないようにする素晴らしいツールであることを思い出すようです。また、継承がさまざまな方法で強力なツールであることを思い出すようです。あたかも息苦しいさまざまなツールに長所と短所があり、エンジニアとしてソフトウェアを作成する際にそれらの要素のバランスを取る必要があります。
corsiKa

回答:


136

final 意図を表現します。クラス、メソッド、または変数のユーザーに「この要素は変更されることはありません。変更したい場合は、既存の設計を理解していません」と伝えます。

これは重要です。なぜなら、すべてのクラスやメソッドをサブクラスごとにまったく異なる動作をするように変更する可能性がある予想しなければならない場合、プログラムアーキテクチャは非常に難しいからです。変更可能な要素とそうでない要素を事前に決定し、を介して変更不可能な機能を適用することをお勧めしfinalます。

コメントやアーキテクチャドキュメントを介してこれを行うこともできますが、将来のユーザーがドキュメントを読んで従うことを期待するよりも、コンパイラにできることを強制させる方が常に良いです。


14
しかし、広く再利用された基本クラス(フレームワークやメディアライブラリなど)を作成したことがある人は、アプリケーションプログラマーが正常に動作することを期待するよりもよく知っています。それらは、鉄製のグリップで固定しない限り、考えられなかった方法で発明を覆し、誤用し、歪めます。
キリアンフォス

8
@KilianFothわかりました、しかし正直に言って、あなたの問題はどのようにアプリケーションプログラマーがしているのですか?
コアダンプ

20
@coredump私のライブラリを使用している人は、ひどく悪いシステムを作成します。悪いシステムは悪い評判を生み出します。ユーザーは、Kilianの優れたコードとFred Randomの非常に不安定なアプリを区別することはできません。結果:プログラミングの信条と顧客を失います。コードを誤用しにくくすることは、ドルとセントの問題です。
キリアンフォス

21
「この要素は変更されることは想定されていません。変更したい場合、既存の設計を理解していません。」信じられないほど慢であり、一緒に仕事をした図書館の一部として私が望む態度ではありません。途方もなくカプセル化されたライブラリが、重要なユースケースを予測できなかったために変更必要な内部状態の一部を変更するメカニズムを残さないたびに、1ダイムしかなかった場合...
Mason Wheeler

31
@JimmyB経験則:作成物の用途がわかっていると思うなら、あなたはすでに間違っています。 ベルは、電話を本質的にムザックシステムと考えていました。クリネックスは、より便利にメイクを落とすことを目的として発明されました。IBMのトーマス・ワトソン社長はかつて「5台のコンピューターの世界市場があると思う」と言っていました。2001年にパトリオット法を導入したジムセンセンブレンナー議員は、「パトリオット法の権限で」NSAが行っていることをNSA が阻止することを特に意図していたと述べています。以下のように...
メイソンウィーラー

59

壊れやすい基本クラスの問題を回避します。すべてのクラスには、暗黙的または明示的な保証と不変式のセットが付属しています。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
  • インターフェイスを継承せず、実装のみを継承するプライベート継承を提供します。
  • 基本クラスメソッドを仮想としてマークする必要があり、すべてのオーバーライドもマークする必要があります。これにより、サブクラスが新しいメソッドを定義したが、同じシグネチャを持つメソッドが後で基本クラスに追加されたものの、仮想として意図されていないという問題を回避できます。

3
「壊れやすい基本クラスの問題」に言及するには、少なくとも+100に値します。:)
デビッドアルノ

6
ここでのポイントに納得していません。はい、壊れやすい基本クラスは問題ですが、finalは実装の変更に関するすべての問題を解決するわけではありません。PasswordCheckerのすべての可能なユースケースを知っていると仮定しているため、最初の例は不適切です(「全員をロックアウトするか、全員にアクセスを許可することはOKではありません」-誰が言いますか?)。あなたの最後の「したがって推奨される...」リストは本当に悪いです-あなたは基本的に何も拡張せず、すべてを最終としてマークすることを主張しています-これはOOP、継承、コード再利用の有用性を完全に抹消します。
アデルフス

4
最初の例は、壊れやすい基本クラスの問題の例ではありません。壊れやすい基本クラスの問題では、基本クラスを変更するとサブクラスが壊れます。しかし、その例では、サブクラスはサブクラスのコントラクトに従っていません。これらは2つの異なる問題です。(さらに、特定の状況下では、パスワードチェッカーを無効にすることは実際には合理的です(開発用など))
ウィンストンエワート

5
「壊れやすい基本クラスの問題を回避します」-自分を殺すと空腹になるのを避けるのと同じ方法で。
user253751

9
@immibis、それは食物中毒を避けるために食べることを避けることに似ています。確かに、食べないことが問題になります。しかし、信頼できる場所でのみ食事をするのは理にかなっています。
ウィンストンユワート

33

Joshua BlochによるEffective Java、2nd Editionについてはまだ誰も言及していないことに驚いています(少なくともすべてのJava開発者にとって、これを読む必要があります)。本の項目17でこれを詳細に説明しており、タイトルは「継承またはその他の禁止のための設計と文書化」です。

この本ですべての良いアドバイスを繰り返すことはしませんが、これらの特定の段落は関連しているようです:

しかし、通常の具体的なクラスはどうでしょうか?伝統的に、それらは最終的なものでも、サブクラス用に設計および文書化されたものでもありませんが、この状況は危険です。そのようなクラスで変更が行われるたびに、クラスを拡張するクライアントクラスが破損する可能性があります。これは単なる理論上の問題ではありません。継承用に設計および文書化されていない非最終的なコンクリートクラスの内部を変更した後、サブクラス関連のバグレポートを受け取ることは珍しくありません。

この問題の最善の解決策は、安全にサブクラス化されるように設計および文書化されていないクラスのサブクラス化を禁止することです。サブクラス化を禁止するには、2つの方法があります。2つの方が簡単なのは、クラスfinalを宣言することです。別の方法は、すべてのコンストラクターをプライベートまたはパッケージプライベートにし、コンストラクターの代わりにパブリック静的ファクトリーを追加することです。サブクラスを内部で使用する柔軟性を提供するこの代替方法については、項目15で説明します。どちらのアプローチも受け入れられます。


21

理由の1つは、final親クラスのコントラクトに違反するような方法でクラスをサブクラス化できないことを保証することです。このようなサブクラス化は、SOLID(何よりも「L」)の違反となり、クラスfinalを作成することはそれを防ぎます。

典型的な例の1つは、サブクラスを可変にするような方法で不変クラスをサブクラス化することを不可能にすることです。特定の場合、このような動作の変更は非常に驚くべき効果をもたらす可能性があります。たとえば、実際には変更可能なサブクラスを使用しているときにキーが不変であると考えるマップでキーとして何かを使用する場合

Javaでは、Stringこれらのオブジェクトが渡されるときにサブクラス化して変更可能(または誰かがそのメソッドを呼び出したときにホームにコールバックし、システムから機密データを引き出す可能性がある)できる場合、多くの興味深いセキュリティ問題が導入される可能性がありますクラスのロードとセキュリティに関連するいくつかの内部コードの周り。

また、Finalは、メソッド内の2つのことで同じ変数を再利用するなどの単純なミスを防ぐのに役立つ場合があります。Scalaではval、Javaの最終変数にほぼ対応するものだけを使用することをお勧めします。varまたは非最終変数が疑われて見られます。

最後に、少なくとも理論的には、クラスまたはメソッドがfinalであることがわかっている場合、コンパイラは追加の最適化を実行できます。最終クラスでメソッドを呼び出すと、どのメソッドが呼び出されるかが正確にわかるためです。仮想メソッドテーブルを介して継承を確認します。


6
最後に、少なくとも理論的には、コンパイラーは Clangの仮想化パスを個人的にレビューしましたが、実際に使用されていることを確認しています。
マチューM.

しかし、コンパイラは、マークされたfinalかどうかに関係なく、誰もクラスまたはメソッドをオーバーライドしていないことを事前に伝えることはできませんか?
JesseTG

3
@JesseTGすべてのコードに一度にアクセスできる場合、おそらく。ただし、個別のファイルのコンパイルはどうですか?
モニカを

3
@JesseTG devirtualizationまたは(monomorphic / polymorphic)インラインキャッシングはJITコンパイラの一般的な手法です。システムは現在どのクラスがロードされているかを知っており、オーバーライドメソッドがないという仮定がfalseであることが判明した場合、コードを非最適化できるためです。ただし、事前コンパイラーはできません。1つのJavaクラスとそのクラスを使用するコードをコンパイルすると、後でサブクラスをコンパイルして、消費するコードにインスタンスを渡すことができます。最も簡単な方法は、クラスパスの前に別のjarを追加することです。これは実行時に行われるため、コンパイラはそのすべてを知ることはできません。
アモン

5
@MTilstedリフレクションによる文字列の変更はを介して禁止できるため、文字列は不変であると想定できますSecurityManager。ほとんどのプログラムはこれを使用しませんが、信頼されていないコードも実行しません。+++ 文字列は不変であると仮定する必要があります。そうしないと、セキュリティがゼロになり、ボーナスとして任意のバグが多数発生します。Java文字列が生産的なコードで変更される可能性があると想定しているプログラマーは間違いなく正気ではありません。
-maaartinus

7

2番目の理由はパフォーマンスです。最初の理由は、一部のクラスには、システムが機能するために変更されるべきではない重要な動作または状態があるためです。たとえば、「PasswordCheck」というクラスがあり、そのクラスを構築する場合、セキュリティの専門家チームを雇い、このクラスはよく研究され定義されたプロトコルで何百ものATMと通信します。大学を卒業したばかりの新入社員が、上記のクラスを拡張する「TrustMePasswordCheck」クラスを作成すると、システムに非常に有害になる可能性があります。これらのメソッドはオーバーライドされることは想定されていません、それだけです。



JVMは、サブクラスがなければ、クラスがfinalとして宣言されていなくても、クラスをfinalとして扱うのに十分スマートです。
user253751

「パフォーマンス」の主張が何度も反証されているため、ダウン投票。
ポールスミス

7

クラスが必要なときは、クラスを作成します。サブクラスが必要ない場合、サブクラスは気にしません。クラスが意図したとおりに動作することを確認し、クラスを使用する場所では、クラスが意図したとおりに動作することを前提としています。

誰かが私のクラスをサブクラス化したい場合、私は何が起こったとしても責任を完全に否定したいと思います。クラスを「最終」にすることでそれを達成します。サブクラス化する場合は、クラスの作成時にサブクラス化を考慮しなかったことを思い出してください。したがって、クラスのソースコードを取得し、「最終」を削除する必要があります。それ以降は、発生するすべての責任を完全に負います。

それは「オブジェクト指向ではない」と思いますか?私はそれがすることになっていることをするクラスを作るために支払われました。サブクラス化できるクラスを作成したことに対して、誰も私に支払いをしませんでした。私のクラスを再利用可能にするために報酬を受け取った場合は、歓迎します。「final」キーワードを削除することから始めます。

(それ以外では、「最終」は多くの場合最適化を可能にします。たとえば、パブリッククラスまたはパブリッククラスのメソッドのSwift「最終」では、コンパイラはメソッド呼び出しが実行するコードを完全に認識できることを意味します。動的ディスパッチを静的ディスパッチに置き換えることができ(小さな利点)、多くの場合、静的ディスパッチをインライン化に置き換えることができます(おそらく大きな利点))。

adelphus:「サブクラス化する場合、ソースコードを取得し、「最終」を削除するのはあなたの責任です」と理解するのが難しいのは何ですか?「最終」は「公正な警告」に等しい。

そして、私は再利用可能なコードを作るために支払われていません。私はそれがすることになっていることをするコードを書くために支払われます。同様の2つのコードを作成するための支払いが行われた場合、共通部分を抽出します。それはより安価であり、時間を無駄にすることもありません。再利用されないコードを再利用可能にすることは、私の時間の無駄です。

M4ks:外部からアクセスされるべきではないすべてのものを常にプライベートにします。繰り返しますが、サブクラス化する場合は、ソースコードを取得し、必要に応じて「保護」に変更し、実行内容に対して責任を負います。私がプライベートとマークしたものにアクセスする必要があると思うなら、あなたは何をしているのかをよりよく知っています。

両方:サブクラス化は、コードの再利用のごく一部です。サブクラス化せずに適応できるビルディングブロックを作成することは、ブロックのユーザーが取得したものに依存できるため、はるかに強力であり、「最終」から大きな利益を得ます。


4
-1この回答は、ソフトウェア開発者にとって間違っているすべてのことを説明しています。誰かがサブクラス化してクラスを再利用する場合は、それらを許可します。なぜ彼らがそれをどのように使用する(または乱用する)のはあなたの責任でしょうか?基本的に、finalをaf kとして使用していますが、私のクラスは使用していません。*「サブクラス化できるクラスを作成しても、誰も私に支払いません」。真剣ですか?それが、ソフトウェアエンジニアが採用されている理由であり、堅牢で再利用可能なコードを作成するためです。
アデルフス

4
-1ただ、すべてがプライベートにするので、誰もがすべてのさえサブクラス化を考えるんだろう...
M4ks

3
@adelphusこの回答の言葉遣いは、厳密ではなく、鈍いものですが、「間違った」視点ではありません。実際、これは、これまでのところ、この質問に対する回答の大部分と同じ視点であり、臨床的トーンはそれほど高くありません。
NemesisX00

「最終」を削除できると言及した場合は+1。コードのすべての可能な使用法の先見を主張することは慢です。しかし、いくつかの可能な用途を維持できないこと、そしてそれらの用途がフォークを維持する必要があることを明確にすることは謙虚です。
-gmatht

4

プラットフォームの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は、だれもがクラスをサブクラス化することを防ぎます。そうすれば、クラス内のどのメソッドをどこで誰かが正確にどのメソッド呼び出しを行うかに依存することを心配することなく変更できます。


1

最終的には、クラスは、下流の継承ベースのクラスに影響を与えることなく(クラスがないため)、クラスのスレッドの安全性に関する問題に影響を与えずに将来変更しても安全であることを意味しますいくつかのスレッドベースの高ジンクス)。

Finalは、あなたのベースに依存している他の人のコードに意図しない動作の変更を加えることなく、クラスの動作を自由に変更できることを意味します。

例として、私はHobbitKillerと呼ばれるクラスを作成します。これは素晴らしいです。なぜなら、すべてのホビットは巧妙で、おそらく死ぬはずだからです。スクラッチ、彼らは間違いなく死ぬ必要があります。

これを基本クラスとして使用し、火炎放射器を使用するための素晴らしい新しいメソッドを追加しますが、ホビットをターゲットにするための素晴らしい方法があるため、私のクラスをベースとして使用します火炎放射器の照準を合わせるために使用します。

3か月後、ターゲット方法の実装を変更します。今、あなたが知らないうちにライブラリをアップグレードする将来のある時点で、あなたが依存するスーパークラスメソッドの変更のために、クラスの実際のランタイム実装が根本的に変更されました(そして一般的には制御しません)。

ですから、私が良心的な開発者であり、クラスを使ってホビットをスムーズに死ぬようにするには、拡張可能なクラスに加えた変更に非常に注意する必要があります。

クラスを拡張することを明確に意図している場合を除いて、拡張機能を削除することで、多くの頭痛を回避できます。


2
ターゲティング方法によって、なぜそれを公開したのかが変わるとしたら?あなたが大幅にクラスの振る舞いを変更した場合、あなたはむしろ過保護とは別のバージョンが必要ですfinal
M4ks

なぜこれが変わるのか分かりません。ホビットは巧妙であり、変更が必要でした。要点は、最終版としてビルドする場合、他の人がコードに感染するのを防ぐための継承を防ぐことです。
スコットテイラー

0

の使用はfinal、SOLID原則の違反ではありません。残念ながら、Open / Closed Principle(「ソフトウェアエンティティは拡張のために開かれるが、修正のために閉じられる」)を「クラスを修正するのではなく、サブクラス化し、新しい機能を追加する」ことを意味するものとして解釈することは非常に一般的です。これはもともとそれが意図したものではなく、一般的にその目標を達成するための最良のアプローチではないと考えられています。

OCPに準拠する最善の方法は、オブジェクトに依存関係を注入することによってパラメーター化された抽象動作を具体的に提供することにより、拡張ポイントをクラスに設計することです(たとえば、Strategyデザインパターンを使用)。これらの動作は、新しい実装が継承に依存しないように、インターフェイスを使用するように設計する必要があります。

別のアプローチは、パブリックAPIを抽象クラス(またはインターフェイス)としてクラスを実装することです。その後、同じクライアントにプラグインできるまったく新しい実装を作成できます。新しいインターフェイスが元のインターフェイスとほぼ同様の動作を必要とする場合、次のいずれかを実行できます。

  • デコレータデザインパターンを使用して、元の既存の動作を再利用する、または
  • 保持したい動作の一部をヘルパーオブジェクトにリファクタリングし、新しい実装で同じヘルパーを使用します(リファクタリングは変更ではありません)。

0

私にとっては、デザインの問題です。

従業員の給与を計算するプログラムがあるとします。国に基づいて2つの日付間の就業日数を返すクラスがある場合(国ごとに1つのクラス)、その最終日を置き、すべての企業がカレンダーにのみ休日を提供する方法を提供します。

どうして?シンプル。開発者がクラスWorkingDaysUSAmyCompanyの基本クラスWorkingDaysUSAを継承し、ストライキ/メンテナンス/火星の2番目の理由で企業が閉鎖されることを反映するように変更したいとします。

クライアントの注文と配達の計算は遅延を反映し、実行時にWorkingDaysUSAmyCompany.getWorkingDays()を呼び出すとそれに応じて動作しますが、休暇時間を計算するとどうなりますか?火星の2番目をすべての人の休日として追加する必要がありますか?いいえ。しかし、プログラマは継承を使用しているため、クラスを保護しなかったため、混乱を招く可能性があります。

または、この会社が土曜日に半時間働いている土曜日に働いていないことを反映するために、クラスを継承および変更するとします。その後、地震、電力危機、または何らかの事情により、大統領は最近ベネズエラで起こったように3日間の休業日を宣言します。継承されたクラスのメソッドが毎週土曜日にすでに減算されている場合、元のクラスを変更すると、同じ日が2回減算される可能性があります。各クライアントの各サブクラスに移動し、すべての変更に互換性があることを確認する必要があります。

解決?クラスをfinalにして、addFreeDay(companyID mycompany、Date freeDay)メソッドを提供します。そうすれば、WorkingDaysCountryクラスを呼び出すときに、サブクラスではなくメインクラスになります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.