序文
●「高凝集性と緩い結合」とは何ですか?
●ディミットの法則を使用して「高い凝集性と緩い結合」を実現するにはどうすればよいですか。
●どのコードデザインが明らかにディミットの法則に反していますか?これを再構築する方法は?
「高凝集性と緩い結合」とは何ですか?
「高凝集性と緩い結合」は非常に重要な設計アイデアであり、コードの読みやすさと保守性を効果的に改善し、機能の変更によって引き起こされるコード変更の範囲を減らすことができます。実際、前の章では、この設計アイデアについて何度も言及しました。多くの設計原則は、実装プログラミングではなくインターフェイスに基づいて、単一責任原則など、コードの「高い凝集性と緩い結合」を実現することを目的としています。
実際、「高凝集性と緩い結合」は、より一般的な設計アイデアであり、システム、モジュール、クラス、さらには関数など、さまざまな詳細コードの設計と開発をガイドするために使用でき、さまざまな開発シナリオにも適用できます。 、マイクロサービス、フレームワーク、コンポーネント、クラスライブラリなど。説明を簡単にするために、このデザインアイデアのアプリケーションオブジェクトとして「クラス」を使用して説明を拡張します。他のアプリケーションシナリオを自分で比較できます。
この設計哲学では、「高凝集性」を使用してクラス自体の設計をガイドし、「緩い結合」を使用してクラス間の依存関係の設計をガイドします。ただし、この2つは完全に独立していて、無関係ではありません。高い凝集力は緩い結合を助け、緩い結合は高い凝集力のサポートを必要とします。
では、「高凝集性」とは正確には何ですか?
いわゆる高凝集性とは、類似した関数を同じクラスに配置し、異なる関数を同じクラスに配置してはならないことを意味します。多くの場合、同様の関数が同時に変更され、同じクラスに配置され、変更がより集中され、コードの保守が容易になります。実際、前述の単一責任の原則は、高いコードのまとまりを実現するための非常に効果的な設計原則です。
もう一度見てみましょう、「ルーズカップリング」とは何ですか?
いわゆるルーズカップリングとは、コード内でクラス間の依存関係が単純で明確であることを意味します。2つのクラスに依存関係がある場合でも、1つのクラスでコードを変更しても、依存クラスでコードが変更されることはありません。実際、依存関係の注入、インターフェイスの分離、実装ではなくインターフェイスに基づくプログラミング、および今日説明したDimitのルールはすべて、コードの緩い結合に関するものです。
「ディミットの法則」の理論的記述
デメテルの法則の英語訳は次のとおりです。デメテルの法則、略してLOD。名前だけから判断すると、この原則が何であるかを推測することはできません。ただし、これには「最小知識の原則」と呼ばれるもう1つの表現力のある名前があり、英語では「最小知識の原則」と翻訳されています。
この設計原則に関して、最も独創的な英語の定義を見てみましょう。
各ユニットは、他のユニットについて限られた知識しか持っていない必要があり
ます。現在のユニットに「密接に」関連しているユニットだけです。または:各ユニットは、その友達とのみ話す必要があります。見知らぬ人と話をしないでください。
文字通り中国語に翻訳します。次のようになります。
各モジュール(ユニット)は、それらのモジュールの限られた知識のみを理解する必要があります(ユニット:現在のユニットに「密接に」関連するユニットのみ)。言い換えれば、各モジュールはそれ自身の友人と「話す」だけで、見知らぬ人と「話す」ことはありません。
先ほど申し上げましたように、デザインの原則やアイデアのほとんどは非常に抽象的であり、さまざまな解釈がありますが、実際の開発に柔軟に適用するには、実際の戦闘経験の蓄積が必要です。ディミットの法則も例外ではありません。それで、私は自分の理解と経験を組み合わせて、今、定義を再記述しました。なお、一律に説明するために、定義説明の「モジュール」を「クラス」に置き換えました。
直接の依存関係を持たないクラス間、依存関係を持たないクラス間。依存関係を持つクラス間では、必要なインターフェイス(つまり、定義内の「限られた知識」)のみに依存するようにしてください。
上記の説明から、ディミットの法則は前後の2つの部分で構成されていることがわかります。これらの2つの部分は、2つのことについて説明しています。実際の2つのケースを使用して、別々に解釈します。
理論的解釈と実際の戦闘コード
この原則の前半を最初に見てみましょう。「直接の依存関係を持つべきではないクラス間に依存関係があってはなりません。」例を挙げて説明しましょう。
この例では、Webページをクロールする検索エンジンの簡略化されたバージョンを実装します。コードには3つの主要なクラスが含まれています。その中で、NetworkTransporterクラスは、基盤となるネットワーク通信を担当し、要求に応じてデータを取得します。HtmlDownloaderクラスは、URLを介してWebページを取得するために使用されます。DocumentはWebドキュメントを表し、その後のWebコンテンツの抽出、単語のセグメント化、およびインデックス作成はすべてこれに基づいて処理されます。具体的なコードの実装は次のとおりです。
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(HtmlRequest htmlRequest) {
//...
}
}
public class HtmlDownloader {
private NetworkTransporter transporter;//通过构造函数或IOC注入
public Html downloadHtml(String url) {
Byte[] rawHtml = transporter.send(new HtmlRequest(url));
return new Html(rawHtml);
}
}
public class Document {
private Html html;
private String url;
public Document(String url) {
this.url = url;
HtmlDownloader downloader = new HtmlDownloader();
this.html = downloader.downloadHtml(url);
}
//...
}
このコードは「使用可能」であり、必要な機能を実現できますが、「使用可能」ではなく、多くの設計上の欠陥があります。最初にそれについて考えて、欠陥が何であるかを確認してから、以下の私の説明を見てください。
まず、NetworkTransporterクラスを見てみましょう。低レベルのネットワーク通信クラスとして、その機能がHTMLのダウンロードだけでなく、可能な限り一般的であることを望んでいます。したがって、あまりにも具体的な送信オブジェクトHtmlRequestに直接依存しないでください。この観点から、NetworkTransporterクラスの設計は、Dimitの法則に違反しており、直接依存してはならないHtmlRequestクラスに依存しています。
NetworkTransporterクラスがDimitの法則を満たすようにするには、どのようにリファクタリングする必要がありますか?私はここに鮮やかな類似点があります。何かを買うために店に行く場合、あなたは間違いなく直接キャッシャーに財布を渡してキャッシャーにお金を受け取らせるのではなく、財布からお金を取り出してキャッシャーに渡します。ここHtmlRequestオブジェクトは、お財布に相当し、そして内のアドレスとコンテンツオブジェクトHtmlRequestは、お金と同等です。HtmlRequestをNetworkTransporterに直接渡すのではなく、アドレスとコンテンツをNetworkTransporterに渡す必要があります。この考えによると、NetworkTransporterのリファクタリングされたコードは次のとおりです。
public class NetworkTransporter {
// 省略属性和其他方法...
public Byte[] send(String address, Byte[] data) {
//...
}
}
HtmlDownloaderクラスをもう一度見てみましょう。このクラスの設計に問題はありません。ただし、NetworkTransporterのsend()関数の定義を変更し、このクラスはsend()関数を使用するため、それに応じて変更する必要があります。変更されたコードは次のとおりです。
public class HtmlDownloader {
private NetworkTransporter transporter;//通过构造函数或IOC注入
// HtmlDownloader这里也要有相应的修改
public Html downloadHtml(String url) {
HtmlRequest htmlRequest = new HtmlRequest(url);
Byte[] rawHtml = transporter.send(
htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
return new Html(rawHtml);
}
}
最後に、Documentクラスを確認します。このカテゴリーには、主に3つの点で多くの問題があります。まず、コンストラクターのdownloader.downloadHtml()はロジックが複雑で時間がかかるため、コードのテスト容易性に影響を与えるため、コンストラクターに配置しないでください。コードのテスト可能性については後で説明します。ここでは、これを知っておくだけで済みます。次に、HtmlDownloaderオブジェクトは、コンストラクターのnewによって作成されます。これは、プログラミングではなくインターフェイスに基づく設計哲学に違反し、コードのテスト容易性にも影響します。第三に、ビジネスへの影響に関して、ドキュメントWebドキュメントはHtmlDownloaderクラスに依存する必要がありません。これはDimitの法則に違反します。
Documentクラスには多くの問題がありますが、変更は比較的簡単で、すべての問題は1回の変更で解決できます。変更されたコードは次のとおりです。
public class Document {
private Html html;
private String url;
public Document(String url, Html html) {
this.html = html;
this.url = url;
}
//...
}
// 通过一个工厂方法来创建Document
public class DocumentFactory {
private HtmlDownloader downloader;
public DocumentFactory(HtmlDownloader downloader) {
this.downloader = downloader;
}
public Document createDocument(String url) {
Html html = downloader.downloadHtml(url);
return new Document(url, html);
}
}
理論的解釈とコード実際の戦闘II
ここで、この原則の後半を見てみましょう。「依存関係のあるクラス間では、必要なインターフェイスのみに依存するようにしてください」。例を挙げて説明しましょう。次のコードは非常に単純です。Serializationクラスは、オブジェクトのシリアル化と逆シリアル化を担当します。
public class Serialization {
public String serialize(Object object) {
String serializedResult = ...;
//...
return serializedResult;
}
public Object deserialize(String str) {
Object deserializedResult = ...;
//...
return deserializedResult;
}
}
このクラスのデザインを見るだけで問題ありません。ただし、特定のアプリケーションシナリオに配置した場合でも、継続的な最適化の余地があります。このプロジェクトで、一部のクラスはシリアル化操作のみを使用し、他のクラスは逆シリアル化操作のみを使用するとします。Dimitのルールの後半である「依存クラス間では、必要なインターフェイスのみに依存するようにしてください」に基づいて、シリアル化操作のみを使用するクラスの部分は、逆シリアル化インターフェイスに依存しないでください。同様に、逆シリアル化操作のみを使用するクラスの部分は、シリアル化インターフェイスに依存しないでください。
この考えによれば、Serializationクラスを2つのより小さなクラスに分割する必要があります。1つはシリアル化のみを担当し(Serializerクラス)、もう1つは逆シリアル化のみを担当します(Deserializerクラス)。分割後、シリアル化操作を使用するクラスはSerializerクラスのみに依存する必要があり、逆シリアル化操作を使用するクラスはDeserializerクラスのみに依存する必要があります。分割後のコードは次のとおりです。
public class Serializer {
public String serialize(Object object) {
String serializedResult = ...;
...
return serializedResult;
}
}
public class Deserializer {
public Object deserialize(String str) {
Object deserializedResult = ...;
...
return deserializedResult;
}
}
分割後のコードはディミットの法則をよりよく満たすことができますが、それは高凝集性の設計哲学に違反していることがわかるかどうかはわかりません。凝集性が高いと、同様の関数を同じクラスに配置する必要があります。これにより、関数が変更されたときに、変更された場所が分散しすぎないようになります。今の例では、JSONからXMLなど、シリアル化の実装を変更する場合、逆シリアル化の実装ロジックも変更する必要があります。分割せずに、1つのクラスを変更するだけで済みます。分割した後、2つのクラスを変更する必要があります。明らかに、この設計アイデアのコード変更の範囲は大きくなっています。
高凝集性の設計哲学やディミットの法則に違反したくないのであれば、どうすればこの問題を解決できるでしょうか。実際、この問題は2つのインターフェースを導入することで簡単に解決できます。具体的なコードを以下に示します。
public interface Serializable {
String serialize(Object object);
}
public interface Deserializable {
Object deserialize(String text);
}
public class Serialization implements Serializable, Deserializable {
@Override
public String serialize(Object object) {
String serializedResult = ...;
...
return serializedResult;
}
@Override
public Object deserialize(String str) {
Object deserializedResult = ...;
...
return deserializedResult;
}
}
public class DemoClass_1 {
private Serializable serializer;
public Demo(Serializable serializer) {
this.serializer = serializer;
}
//...
}
public class DemoClass_2 {
private Deserializable deserializer;
public Demo(Deserializable deserializer) {
this.deserializer = deserializer;
}
//...
}
シリアル化と逆シリアル化を含むSerialization実装クラスをDemoClass_1のコンストラクターに渡す必要がありますが、依存するSerializableインターフェイスにはシリアル化操作のみが含まれ、DemoClass_1はSerializationクラスで逆シリアル化インターフェイスを使用できません。ディミットのルールの後半で述べたように、「制限されたインターフェースに依存する」という要件も満たす逆シリアル化操作の認識はありません。
実際、上記のコード実装のアイデアは、「実装プログラミングではなくインターフェイスに基づく」という設計原則も反映しています。Dimitのルールを組み合わせることで、「最大ではなく最小のインターフェイスに基づく」という新しい設計原則を結論付けることができます。プログラミングを実現する」。一部の学生は、新しい設計パターンと設計原則がどのように作成されるかを前に尋ねました。実際、それは多くの実践における開発の問題点について要約されたルーチンでした。
方言的思考と柔軟なアプリケーション
実際の戦闘IIの最終的なデザインのアイデアについて何か異なる見解がありますか?
クラス全体には、シリアル化と逆シリアル化の2つの操作のみが含まれ、シリアル化操作のみを使用するユーザーは、逆シリアル化関数を1つしか認識できない場合でも、問題は大きくありません。そこで、ディミットの法則を満たすために、非常に単純なクラスを2つのインターフェースに分割しましたが、少し過剰に設計されていますか?
設計原理自体は正しいか間違っているかではなく、使用できるかどうかだけです。設計原則を適用するために設計原則を適用しないでください。設計原則を適用する場合、特定の問題を詳細に分析する必要があります。
現在のSerializationクラスの場合、含まれている操作は2つだけであり、実際には2つのインターフェイスに分割する必要はありません。ただし、Serializationクラスに関数を追加し、さらに便利なシリアル化および逆シリアル化関数を実装する場合は、この問題を再検討してみましょう。変更された特定のコードは次のとおりです。
public class Serializer {
// 参看JSON的接口定义
public String serialize(Object object) {
//... }
public String serializeMap(Map map) {
//... }
public String serializeList(List list) {
//... }
public Object deserialize(String objectString) {
//... }
public Map deserializeMap(String mapString) {
//... }
public List deserializeList(String listString) {
//... }
}
このシナリオでは、2番目の設計アイデアの方が優れています。前のアプリケーションシナリオに基づいているため、ほとんどのコードはシリアル化機能を使用するだけで済みます。これらのユーザーの場合、逆シリアル化の「知識」を理解する必要はなく、変更されたシリアル化クラスである逆シリアル化の「知識」が1つの関数から3つの関数に変更されました。逆シリアル化操作でコードが変更されたら、Serializationクラスに依存するすべてのコードが引き続き正常に機能するかどうかを確認およびテストする必要があります。結合とテストの作業負荷を減らすために、ディミットの法則に従って逆シリアル化とシリアル化の機能を分離する必要があります。
キーレビュー
1.「高凝集性と緩い結合」をどのように理解しますか?「」
「高凝集性と緩い結合」は非常に重要な設計アイデアであり、コードの読みやすさと保守性を効果的に改善し、機能の変更によって引き起こされるコード変更の範囲を減らすことができます。「高凝集性」はクラス自体の設計をガイドするために使用され、「緩い結合」はクラス間の依存関係の設計をガイドするために使用されます。いわゆる高凝集性とは、類似した機能を同じカテゴリに配置し、異なる機能を同じカテゴリに配置してはならないことを意味します。同様の機能が同時に変更されることが多く、同じカテゴリに分類されると、変更がより集中します。いわゆるルーズカップリングとは、コード内でクラス間の依存関係が単純で明確であることを意味します。2つのクラスに依存関係がある場合でも、1つのクラスでコードを変更しても、依存クラスでコードが変更されることはありません。
2.「デメテルの法則」を理解する方法は?
直接の依存関係を持つべきではないクラス間、依存関係はありません。依存関係を持つクラス間では、必要なインターフェイスのみに依存するようにしてください。ディミットの法則は、クラス間の結合を減らして、独立しているほど良いということです。各クラスは、システムの他の部分についてあまり知らないはずです。変更が発生すると、変更を理解する必要のあるクラスが少なくなります。