オタク時間-デザインパターン理論7の美しさ:LSPと多形性の違いは何ですか?どのコードがLSPに違反していますか?

最後の2つのレッスンでは、SOLIDの原則で、単一の責任の原則と開始と終了の原則を学びました。これら2つの原則はより重要であり、柔軟に適用することはより困難です。実際には、より多くの練習と経験が必要です。今日は、SOLIDの「L」に対応する原理、Li置換の原理を学びましょう。

全体として、この設計原則は比較的単純で、理解しやすく、習得しやすいものです。今日、私は主にいくつかの反例を使用して、Li置換の原則に違反するコードを示します。Li置換の原則を満たすようにそれらをどのように変換できますか?さらに、この原則は、前述の「多形性」と定義が似ています。

それで、今日はそれと多形性の違いについても話します。言うまでもありませんが、今日の学習を正式に始めましょう!

「li置換原理」を理解するには?

Liskov SubstitutionPrincipleの英語訳は次のとおりです。LiskovSubstitutionPrinciple、略してLSP。この原則は、1986年にBarbaraLiskovによって最初に提案されました。彼はこの原則を次のように説明しました。

SがTのサブタイプである場合
、プログラムを中断することなく、タイプTのオブジェクトをタイプSのオブジェクトに置き換えることができます

1996年、ロバート・マーティンはこの原則を彼のSOLID原則で再記述しました。元の英語の単語は、次のとおりです。

基本クラスへの参照のポインタを使用する関数は、
それを知らなくても派生クラスのオブジェクトを使用できなければなりません

2つの説明を組み合わせて、この原則を中国語で説明します。つまり、サブタイプ/派生クラスのオブジェクトは、プログラム内の基本クラス/親クラスのオブジェクトを置き換えることができます。どこでも、元のプログラムの論理的な動作が変更されないままであり、正確性が損なわれないようにします。それはまだかなり抽象的なので、例を通して説明しましょう。

次のコードでは、親クラスTransporterがorg.apache.httpライブラリのHttpClientクラスを使用してネットワークデータを送信しています。サブクラスSecurityTransporterは、親クラスTransporterを継承し、appIdおよびappTokenセキュリティ認証情報の送信をサポートする機能を追加します。


public class Transporter {
    
    
  private HttpClient httpClient;
  
  public Transporter(HttpClient httpClient) {
    
    
    this.httpClient = httpClient;
  }

  public Response sendRequest(Request request) {
    
    
    // ...use httpClient to send request
  }
}

public class SecurityTransporter extends Transporter {
    
    
  private String appId;
  private String appToken;

  public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
    
    
    super(httpClient);
    this.appId = appId;
    this.appToken = appToken;
  }

  @Override
  public Response sendRequest(Request request) {
    
    
    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
    
    
      request.addPayload("app-id", appId);
      request.addPayload("app-token", appToken);
    }
    return super.sendRequest(request);
  }
}

public class Demo {
    
        
  public void demoFunction(Transporter transporter) {
    
        
    Reuqest request = new Request();
    //...省略设置request中数据值的代码...
    Response response = transporter.sendRequest(request);
    //...省略其他逻辑...
  }
}

// 里式替换原则
Demo demo = new Demo();
demo.demofunction(new SecurityTransporter(/*省略参数*/););

上記のコードでは、サブクラスSecurityTransporterの設計は、Li置換の原則に完全に準拠しています。これにより、親クラスが表示される任意の位置を置き換えることができ、元のコードの論理動作は変更されず、正確性が損なわれることはありません。

しかし、あなたはそのような質問があるかもしれません、コードデザインはちょうど今オブジェクト指向の多形性を使用しているのではありませんか?多形性とLi置換原理は同じことを言っていますか?今の例と定義の説明から判断すると、Li置換の原理と多形性は少し似ているように見えますが、実際には完全に異なります。なんでそんなこと言うの?

今すぐ例を使って説明しましょう。ただし、SecurityTransporterクラスのsendRequest()関数を少し変更する必要があります。前者の変換、appIdまたはappTokenが設定されていない場合、チェックは行われません。変換後、appIdまたはappTokenが設定されていない場合、NoAuthorizationRuntimeException無許可の例外を直接スローします。変換前後のコード比較は次のとおりです。


// 改造前:
public class SecurityTransporter extends Transporter {
    
    
  //...省略其他代码..
  @Override
  public Response sendRequest(Request request) {
    
    
    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
    
    
      request.addPayload("app-id", appId);
      request.addPayload("app-token", appToken);
    }
    return super.sendRequest(request);
  }
}

// 改造后:
public class SecurityTransporter extends Transporter {
    
    
  //...省略其他代码..
  @Override
  public Response sendRequest(Request request) {
    
    
    if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {
    
    
      throw new NoAuthorizationRuntimeException(...);
    }
    request.addPayload("app-id", appId);
    request.addPayload("app-token", appToken);
    return super.sendRequest(request);
  }
}

変更されたコードでは、demoFunction()関数がスーパークラスTransporterオブジェクトに渡された場合、demoFunction()関数は例外をスローしませんが、demoFunction()関数がサブクラスSecurityTransporterオブジェクトに渡された場合、 DemoFunction()は例外をスローする場合があります。コードはランタイム例外(Runtime Exception)をスローしますが、コードを明示的にキャプチャして処理することはできませんが、サブクラスが親クラスを置き換えてdemoFunction関数に渡された後、プログラム全体のロジック動作が変更されました。

変更されたコードは、Javaのポリモーフィック構文を使用して、親クラスTransporterをサブクラスSecurityTransporterに動的に置き換えることができますが、プログラムのコンパイルやエラーの実行は発生しません。ただし、設計アイデアに関しては、SecurityTransporterの設計はLi置換の原則に準拠していません。

さて、それを少し要約しましょう。定義の説明とコードの実装の観点からは、多態性とLi置換は多少似ていますが、それらは異なる角度に焦点を合わせています。多態性は、オブジェクト指向プログラミングの主要な機能であり、オブジェクト指向プログラミング言語の構文です。それはコード実装のアイデアです。Li置換は、継承関係でサブクラスの設計をガイドするために使用される設計原則です。サブクラスの設計では、親クラスを置き換えるときに、元のプログラムのロジックが変更されたり、元のプログラムが破壊されたりしないようにする必要があります。正しさ。

どのコードが明らかにLSPに違反していますか?

実際、Liスタイルの代替原則については、「契約による設計」という、より実用的で有益な説明がもう1つあります。

もっと抽象的なようです。さらに解釈させてください。サブクラスを設計するときは、親クラスの動作規則(または合意)に準拠する必要があります。親クラスは関数の動作規則を定義します。サブクラスは関数の内部実装ロジックを変更できますが、関数の元の動作規則を変更することはできません。ここでの動作合意には、関数宣言によって実装される関数、入力、出力、および例外に関する合意、さらにはコメントにリストされている特別な指示も含まれます。実際、定義内の親クラスとサブクラスの間の関係は、インターフェースと実装クラスの間の関係に置き換えることもできます。

この文をよりよく理解するために、Li置換の原則に違反するいくつかの例を挙げて説明しましょう

1.サブクラスは、親クラスによって宣言された関数に違反しています

親クラスで提供されるsortOrdersByAmount()注文並べ替え関数、金額に応じて注文を小から大に並べ替え、サブクラスはsortOrdersByAmount()注文並べ替え関数を書き換えて、作成日に従って注文を並べ替えます。そのサブカテゴリの設計は、Liスタイルの置換の原則に違反しています。

2.サブクラスは、入力、出力、および例外に関する親の規則に違反しています。

親クラスでは、関数の規則:操作でエラーが発生した場合はnullを返し、取得したデータが空の場合は空のコレクションを返します。サブクラスが関数をオーバーロードした後、実装が変更され、操作エラーが例外(例外)を返し、データを取得できず、nullが返されます。そのサブカテゴリの設計は、Liスタイルの置換の原則に違反しています。

親クラスでは、特定の関数が入力データを任意の整数にすることに同意しますが、サブクラスが実装されると、入力データのみが正の整数になり、負の数値がスローされます。つまり、入力データに対するサブクラスのチェック比率です。親カテゴリはより厳密であり、そのサブカテゴリの設計はLi置換の原則に違反しています。

親クラスでは、特定の関数がArgumentNullExceptionのみがスローされることを規定しています。そのサブクラスの設計と実装では、ArgumentNullExceptionのみがスローされます。他の例外がスローされると、サブクラスは置換の原則に違反します。

3.サブクラスは、親クラスのコメントに記載されている特別な指示に違反しています

親クラスで定義されているwithdrawal()関数のコメントは、「ユーザーの引き出し額は口座残高を超えてはならない...」と書かれており、サブクラスは、withdrawal()関数を書き直した後、VIPアカウントのオーバードラフト引き出し関数を実装しました。つまり、引き出し額は口座残高よりも多くなる可能性があるため、このサブカテゴリの設計はLi置換の原則に準拠していません。

上記は、Li置換の原則に違反する3つの典型的な状況です。さらに、サブクラスの設計と実装がLiの置換の原則に違反しているかどうかを判断するために、親クラスのユニットテストを使用してサブクラスのコードを検証するという別のトリックがあります。一部のユニットテストが失敗した場合は、サブクラスの設計と実装が親クラスの規則に完全に準拠していないことを示している可能性があり、サブクラスは置換の原則に違反している可能性があります。

実際、Liスタイルの置換の原則が非常に緩いことに気づきましたか。通常の状況では、私たちが書いたコードはそれに違反しません。ですから、私が今日話していることを理解できる限り、この原則を理解して適用することは難しくありません。

おすすめ

転載: blog.csdn.net/zhujiangtaotaise/article/details/110434627