blog

デザインパターンの注意点

1. デザイン・パターンの目的 2. デザイン・パターンの7つの原則 クラスの場合、クラスは1つの義務だけを担当します。例えば、クラスAは1つの義務だけを担当します。クラスAが複数の原則を担当する必要...

Jun 28, 2020 · 15 min. read
シェア

デザインパターンの目的

ソフトウェアを書く過程で、プログラマは結合性、凝集性、保守性、拡張性、再利用性、柔軟性などの課題に直面します。

  1. コードの再利用性
  2. 可読性
  3. 拡張性
  4. 信頼性
  5. 低結合性、高結合性のプログラムを作成

それは高層ビルを建てるようなもので、まず基礎を設計し、建て方を設計し、完璧な設計図ができて初めて建て始めることができます!

デザインパターン7原則

デザインパターンの基礎、すなわちデザインパターンがなぜそのように設計されているのかの根拠となる「デザインパターンの原則

一般的なデザイン・パターンの原則は7つあります:

  1. 単一責任の原則
  2. インターフェース分離の原則
  3. 依存関係の逆転の原則
  4. リヒターの置換原則
  5. オープン・クローズの原則
  6. ディミトリの原則
  7. 合成再利用の原則

単一責任の原則

基本的な導入

クラスの場合、1 つのクラスは 1 つの義務のみを担当する必要があります。例えば、クラス A は 1 つの義務 1 のみを担当します。クラス A が複数の原則を担当する必要がある場合、クラス A をより細かい A1,A2 に分割するのが最善の方法です。

アプリケーションの例

トランスポートの実行メソッドを定義したトランスポートクラスを作成してください。

public class SingleResponsibilityDemo {
 public static void main(String[] args) {
 Transportation transportation = new Transportation();
 transportation.run("カーズ");
 transportation.run("航空機");
 transportation.run("手押し車");
 }
}
class Transportation {
 public void run(String name) {
 System.out.println(name + "地べたを走る");
 }
}

主な方法では、それが車、飛行機、船であるかどうか、彼らは明らかに単一の責任の原則に沿っていない "地面を実行している"、解決策は、実際には非常に簡単ですが、ちょうど輸送の種類が異なっているに応じて、別のクラスを書き込むことができます。

class RoadTransportation {
 public void run(String name) {
 System.out.println(name + "地べたを走る");
 }
}
class AirTransportation {
 public void run() {
 System.out.println("空を走る");
 }
}
class WaterTransportation {
 public void run(String name) {
 System.out.println(name + "地べたを走る");
 }
}

こうすることで、単一責任の原則は尊重されますが、クラスの変更は大きくなり、クライアントも大きな変更を行う必要があります。

この場合、元のTransportationクラスのメソッドを変更することができます。

public class SingleResponsibilityDemo {
 public static void main(String[] args) {
 Transportation transportation = new Transportation();
 transportation.runRoad("カーズ");
 transportation.runAir("航空機");
 transportation.runWater("手押し車");
 }
}
class Transportation {
 public void runRoad(String name) {
 System.out.println(name + "地べたを走る");
 }
 public void runAir(String name) {
 System.out.println(name + "地べたを走る");
 }
 public void runWater(String name) {
 System.out.println(name + "地べたを走る");
 }
}

変更が少ないだけでなく、単一責任の原則はクラスレベルでは守られませんが、メソッドレベルでは新しいメソッドの追加によって守られます。

注意点と詳細

  1. クラスの複雑さを軽減し、1つの責任に1つのクラス
  2. クラスの可読性と保守性の向上
  3. 変化によるリスクの軽減
  4. クラスのメソッドの数が非常に少ない場合を除き、クラスに新しいメソッドを追加することで単一責任の原則を守ることができます。

インターフェース分離の原則

基本的な導入

つまり、あるクラスが他のクラスに依存するのは、最小のインターフェイスに基づくべきです。

アプリケーションの例とコード

以下:

クラスAがInterface1を通じてクラスBに依存する場合、クラスBは必要ないoperation4とoperation5のメソッドを実装し、クラスCも同様に実装します。

上のUMLダイアグラムによると、コードは次のようになります:

public class InterfaceSegrationDemo {
 public static void main(String[] args) {
 B b = new B();
 A a = new A();
 C c = new C();
 D d = new D();
 a.method1(b);
 a.method2(b);
 a.method3(b);
 c.method1(d);
 c.method4(d);
 c.method5(d);
 }
}
interface Interface1 {
 void operation1();
 void operation2();
 void operation3();
 void operation4();
 void operation5();
}
class B implements Interface1 {
 @Override
 public void operation1() {
 System.out.println("B1");
 }
 @Override
 public void operation2() {
 System.out.println("B2");
 }
 @Override
 public void operation3() {
 System.out.println("B3");
 }
 @Override
 public void operation4() {
 System.out.println("B4");
 }
 @Override
 public void operation5() {
 System.out.println("B5");
 }
}
class D implements Interface1 {
 @Override
 public void operation1() {
 System.out.println("D1");
 }
 @Override
 public void operation2() {
 System.out.println("D2");
 }
 @Override
 public void operation3() {
 System.out.println("D3");
 }
 @Override
 public void operation4() {
 System.out.println("D4");
 }
 @Override
 public void operation5() {
 System.out.println("D5");
 }
}
class A {
 public void method1(Interface1 interface1) {
 interface1.operation1();
 }
 public void method2(Interface1 interface1) {
 interface1.operation2();
 }
 public void method3(Interface1 interface1) {
 interface1.operation3();
 }
}
class C {
 public void method1(Interface1 interface1) {
 interface1.operation1();
 }
 public void method4(Interface1 interface1) {
 interface1.operation4();
 }
 public void method5(Interface1 interface1) {
 interface1.operation5();
 }
}

インターフェイス分離の原則に従って改良すると、インターフェイスInterface1は3つの別々のインターフェイスに分割する必要があり、クラスAとCはそれぞれ必要なインターフェイスとの依存関係を確立することができます。

改良されたコードは以下の通り:

public class InterfaceSegrationDemo2 {
 public static void main(String[] args) {
 B2 b2 = new B2();
 A2 a2 = new A2();
 C2 c2 = new C2();
 D2 d2 = new D2();
 a2.method1(b2);
 a2.method2(b2);
 a2.method3(b2);
 c2.method1(d2);
 c2.method4(d2);
 c2.method5(d2);
 }
}
interface Interface2 {
 void operation1();
}
interface Interface3 {
 void operation2();
 void operation3();
}
interface Interface4 {
 void operation4();
 void operation5();
}
class B2 implements Interface2, Interface3 {
 @Override
 public void operation1() {
 System.out.println("B1");
 }
 @Override
 public void operation2() {
 System.out.println("B2");
 }
 @Override
 public void operation3() {
 System.out.println("B3");
 }
}
class D2 implements Interface2, Interface4 {
 @Override
 public void operation1() {
 System.out.println("D1");
 }
 @Override
 public void operation4() {
 System.out.println("D4");
 }
 @Override
 public void operation5() {
 System.out.println("D5");
 }
}
class A2 {
 public void method1(Interface2 interface2) {
 interface2.operation1();
 }
 public void method2(Interface3 interface3) {
 interface3.operation2();
 }
 public void method3(Interface3 interface3) {
 interface3.operation3();
 }
}
class C2 {
 public void method1(Interface2 interface2) {
 interface2.operation1();
 }
 public void method4(Interface4 interface4) {
 interface4.operation4();
 }
 public void method5(Interface4 interface4) {
 interface4.operation5();
 }
}

2つ以上のインターフェイスを生成するようですが、論理的には、各インターフェイスの役割のより多くの独立した、クラスを実装するメソッドを実装する必要はありませんさせる必要はありません。

依存関係の逆転の原則

基本的な導入

  1. 高位モジュールは基礎となるモジュールに依存すべきではありません。
  2. 抽象化は細部に依存すべきではなく、細部は抽象化に依存すべき。
  3. 依存関係逆転の原則の中心にあるのは、インターフェース指向のプログラミングです。
  4. 抽象化は細部の可変性に比べてはるかに安定しており、抽象化に基づいて構築されたアーキテクチャは、細部に基づいて構築されたアーキテクチャよりもはるかに安定しています。Javaでは、抽象化とは抽象クラスまたはインターフェースのことであり、詳細とは具体的な実装のことです。
  5. インターフェイスや抽象クラスを使用する目的は、具体的な操作を伴わない仕様を設定し、詳細を示す作業を実装クラスに任せることです。

アプリケーションの例

電子メールでメッセージを受信するメソッドを持つ Person クラスを定義します:

public class Demo {
 public static void main(String[] args) {
 Person person = new Person();
 person.receive(new Email());
 }
}
class Email {
 public void info() {
 System.out.println("電子メールメッセージ");
 }
}
class Person {
 public void receive(Email email) {
 email.info();
 }
}

コードの改良点は以下の通りです:

public class DemoImprove {
 public static void main(String[] args) {
 Person person = new Person();
 person.receive(new Email());
 person.receive(new ());
 person.receive(new Message());
 }
}
interface Receiver{
 public void info();
}
class Email implements Receiver{
 @Override
 public void info() {
 System.out.println("電子メールメッセージ");
 }
}
class  implements Receiver{
 @Override
 public void info() {
 System.out.println("情報");
 }
}
class Message implements Receiver{
 @Override
 public void info() {
 System.out.println("SMSメッセージ");
 }
}
class Person {
 public void receive(Receiver receiver) {
 receiver.info();
 }
}

デペンデンシー・パッシングの3つの方法と例

上記の Person クラスでは、receive メソッドを使用して Reciever インターフェース・タイプのパラメーターを受け取ります。

2番目は、例えばコンストラクターで渡すこともできます:

public class DemoImprove {
 public static void main(String[] args) {
 Person person = new Person(new Email());
 person.receive();
 
 }
}
class Person {
 private Receiver receiver;
 public Person(Receiver receiver) {
 this.receiver = receiver;
 }
 public void receive() {
 this.receiver.info();
 }
}

3つ目は、セッターメソッドに渡す方法です。

public class DemoImprove {
 public static void main(String[] args) {
 Person person = new Person();
 person.setReceiver(new Email());
 person.receive();
 }
}
class Person {
 private Receiver receiver;
 public void setReceiver(Receiver receiver) {
 this.receiver = receiver;
 }
 public void receive() {
 this.receiver.info();
 }
}

注意事項と詳細

  1. 基礎となるモジュールは、できるだけ抽象クラスかインターフェース、あるいはその両方を持つべきです。
  2. 変数の参照と実際のオブジェクトの間にバッファ層があるため、プログラムの拡張と最適化が容易になります。
  3. 相続にはリヒター代償の原則が適用されます。

リヒターの置換原則

基本的な導入

オブジェクト指向継承の問題点

  • 親クラスに実装されたメソッドは、すべてのサブクラスにその仕様を強制するものではありませんが、実際に仕様と契約を設定していることになります。しかし、サブクラスが実装されたメソッドに恣意的な変更を加えると、継承システム全体にダメージを与えることになります。
  • 継承はプログラミングに利便性をもたらす反面、デメリットももたらします。例えば、継承の使用はプログラムに侵入性をもたらし、プログラムの移植性を低下させ、オブジェクト間の結合を増加させます。クラスが他のサブクラスに継承されている場合、クラスを修正する必要があるときに、すべてのサブクラスを考慮する必要があります。
  • プログラミング、継承の正しい使い方 ----> リヒターの置換原理

リヒターの置換原理:

  1. T2型は、T1型のすべてのオブジェクトo1に対して、T2型のオブジェクトo2が存在し、T1型で定義されたすべてのプログラムpが、すべてのオブジェクトo1をo2に置き換えてもプログラムpの振る舞いに変化がないような場合、T1型のサブタイプです。
  2. 継承を使用する場合は、親クラスですでに実装されているメソッドをオーバーライドしないようにします。
  3. 継承は、実際には2つのクラスの結合をより強固なものにします。適切な状況では、集約、結合、依存関係を使用して問題を解決することができます。

サンプルコード

コードは以下の通りです。

public class Demo {
 public static void main(String[] args) {
 A a = new A();
 System.out.println(a.function1(1,2));
 B b = new B();
 System.out.println(b.function1(1,2));
 }
}
class A {
 public int function1(int a,int b){
 return a+b;
 }
}
class B extends A{
 @Override
 public int function1(int a, int b) {
 return a-b;
 }
}

Bで親クラスA function1メソッドを書き換えるには、もともとA function1メソッドは、2つの数値の和を計算するために使用されますが、元の関数のエラーで、その結果、2つの数値の差を計算するために変更された実際のプログラミングは、通常、親クラスのメソッドを書き換えることで、新しい関数を完了するには、これはコードを書くのは簡単ですが、統合システム全体の再利用性が悪くなります。

一般的には、親クラスAと子クラスBが、より抽象的な親クラスを共同で継承し、クラスAとクラスBの間の元の継承関係は削除され、集約関係、依存関係、または組み合わせ関係に置き換えられます。

改良されたコードは以下の通り:

public class Demo {
 public static void main(String[] args) {
 A a = new A();
 System.out.println(a.function1(1, 2));
 B b = new B();
 System.out.println(b.function1(1, 2));
 }
}
abstract class Base {
 public abstract int function1(int a, int b);
}
class A extends Base {
 @Override
 public int function1(int a, int b) {
 return a + b;
 }
}
class B extends Base {
 private A a = new A();
 @Override
 public int function1(int a, int b) {
 return a - b;
 }
 public int function2(int a, int b) {
 return this.a.function1(a, b) + this.function1(a, b);
 }
}

上位の親クラスBaseにfunction1を定義することで、クラスAとクラスBのfunction1のメソッドが互いに干渉しないようにすることができ、クラスAのfunction1を使いたい場合は、クラスBでクラスAのオブジェクトを定義することで呼び出すことができます。

オープン・クローズの原則

基本的な導入

  1. 開閉の原則は、プログラミングにおける最も基本的で重要な設計原則です。
  2. クラス、モジュール、関数のようなソフトウェアの実体は、プロバイダーによる拡張に対してオープンであるべきで、ユーザーによる変更に対してはクローズドであるべきです。
  3. ソフトウェアを変更する必要がある場合は、既存のコードを修正するのではなく、ソフトウェア・エンティティの動作を拡張することによって変更を実装するようにします。
  4. プログラミングで他の原則に従ったり、デザインパターンを使ったりする目的は、オープン・クローズの原則を実行することです。

アプリケーションの例

グラフを描画するクラスを設計し、異なるパラメータを渡すことで、描画関数の異なるグラフを実現します:

public class Demo {
 public static void main(String[] args) {
 GraphicEditor graphicEditor = new GraphicEditor();
 graphicEditor.draw(new Rectangle());
 graphicEditor.draw(new Circle());
 }
}
//  
class GraphicEditor {
 public void draw(Shape shape) {
 if (shape.type_int == 1) {
 System.out.println("長方形を描く");
 } else if (shape.type_int == 2) {
 System.out.println("円を描く");
 }
 }
}
// プロバイダの親クラス
class Shape {
 int type_int;
}
//  
class Rectangle extends Shape {
 public Rectangle() {
 super.type_int = 1;
 }
}
//  
class Circle extends Shape {
 public Circle() {
 super.type_int = 2;
 }
}

一見、上記のコードで問題ないように見えますが、新しいグラフィックスクラスを追加すると、新しいプロバイダを追加するだけでなく、ユーザーのコードを修正する必要があり、例えば、三角形のクラスを追加するなど、ユーザーのコードでグラフィカルな判断をする必要があります。

// 新しいプロバイダー
class Triangle extends Shape {
 public Triangle() {
 super.type_int = 3;
 }
}
//  
class GraphicEditor {
 public void draw(Shape shape) {
 if (shape.type_int == 1) {
 System.out.println("長方形を描く");
 } else if (shape.type_int == 2) {
 System.out.println("円を描く");
 } else if (shape.type_int == 3) {
 System.out.println("三角形を描く");
 }
 }
}

このように書かれたコードは、シンプルで理解しやすいという利点がある一方で、デザインパターンのオープン・クローズ原則に違反しています。

改善案:描画関数をShapeの親クラスに統合し、そのサブクラスにこのメソッドを実装させます。

改良されたコードは以下の通り:

public class Demo {
 public static void main(String[] args) {
 GraphicEditor graphicEditor = new GraphicEditor();
 graphicEditor.draw(new Rectangle());
 graphicEditor.draw(new Circle());
 graphicEditor.draw(new Triangle());
 }
}
class GraphicEditor {
 public void draw(Shape shape) {
 shape.draw();
 }
}
abstract class Shape {
 int type_int;
 public abstract void draw();
}
class Rectangle extends Shape {
 public Rectangle() {
 super.type_int = 1;
 }
 @Override
 public void draw() {
 System.out.println("長方形を描く");
 }
}
class Circle extends Shape {
 public Circle() {
 super.type_int = 2;
 }
 @Override
 public void draw() {
 System.out.println("円を描く");
 }
}
class Triangle extends Shape {
 public Triangle() {
 super.type_int = 3;
 }
 @Override
 public void draw() {
 System.out.println("三角形を描く");
 }
}

また、新しいグラフィッククラスが追加された場合は、そのクラスが親クラスを継承し、対応する描画メソッドを実装するだけでよく、ユーザーはコードを変更する必要はありません。

ディミトリの法則

基本的な導入

  1. オブジェクトは、他のオブジェクトに関する最小限の知識を保持する必要があります。
  2. クラスとクラスの関係が密であればあるほど、カップリングは大きくなります。
  3. ディミトリの法則は最小知識の原則とも呼ばれ、クラスが依存するクラスについて知っていることは少なければ少ないほど良いということを意味します。つまり、依存するクラスがどんなに複雑であっても、ロジックをクラス内部にカプセル化するようにします。パブリックメソッド以外は、外部に情報を公開しません。
  4. ディミトリの法則にはもっと簡単な説明があります。
  5. 直接の友人:すべてのオブジェクトは他のオブジェクトと結合しており、2つのオブジェクトの間に結合がある限り、これら2つのオブジェクトは友人であると言われています。カップリングには、依存、関連、結合、集約など、さまざまな方法があります。その中で、メンバ変数、メソッドパラメータ、メソッドの戻り値で表されるクラスは直接の友人であり、ローカル変数で表されるクラスは直接の友人ではないと言われています。つまり、馴染みのないクラスは、クラス内部のローカル変数として登場しない方がよいということです。

アプリケーションの例

ある学校があって、その下に本部と各カレッジがあるのですが、本部クラスはカレッジの本部の社員とカレッジの社員の情報をプリントアウトすることが義務付けられています。

第一定義本部スタッフとカレッジスタッフ

// 本部スタッフ
class HeadEmployee{
 private int id;
 @Override
 public String toString() {
 return "HeadEmployee{" +
 "id=" + id +
 '}';
 }
}
// カレッジスタッフ
class CollegeEmployee{
 private int id;
 @Override
 public String toString() {
 return "CollegeEmployee{" +
 "id=" + id +
 '}';
 }
}

次に、本部クラスとカレッジクラスを定義します。

class CollegeManager {
 private List<CollegeEmployee> collegeEmployeeList = new ArrayList<>();
 // 大学職員情報を初期化する
 public CollegeManager() {
 for (int i = 0; i < 5; i++) {
 collegeEmployeeList.add(new CollegeEmployee(i));
 }
 }
 
 public List<CollegeEmployee> getCollegeEmployeeList() {
 return collegeEmployeeList;
 }
}
class HeadQuarterManager {
 private List<HeadEmployee> headEmployeeList = new ArrayList<>();
 // 本社の社員情報を初期化する
 public HeadQuarterManager() {
 for (int i = 0; i < 3; i++) {
 headEmployeeList.add(new HeadEmployee(i));
 }
 }
}

次に、本部の従業員情報と大学の従業員情報を本部のカテゴリに印刷する方法を完了します。

 public void printAllEmployee(CollegeManager collegeManager) {
 List<CollegeEmployee> collegeEmployeeList = collegeManager.getCollegeEmployeeList();
 collegeEmployeeList.forEach(System.out::println);
 headEmployeeList.forEach(System.out::println);
 }

この設計の問題点は、本部クラスのprintAllEmployeeメソッドの中に、List collegeEmployeeListというローカル変数があり、本部クラスの中に、CollegeEmployeeクラスがローカル変数という形で登場することで、非直接の友人関係の結合であり、ディミトリの法則を満たすためです。ディミトリの法則を満たすためには、printAllEmployeeメソッドの中でcollegeManagerだけを呼び出せば、大学職員情報の印刷が完了するので、大学職員情報を印刷するメソッドをcollegeManagerクラスに入れています。

// collegeManager  
 public void printCollegeEmployee() {
 collegeEmployeeList.forEach(System.out::println);
 }
// HeadQuarterManager  
 public void printAllEmployee(CollegeManager collegeManager) {
 collegeManager.printCollegeEmployee();
 headEmployeeList.forEach(System.out::println);
 }

これにより、HeadQuarterManagerクラス自体で大学職員情報の印刷を行う必要がなくなり、代わりにCollegeManagerクラスのオブジェクトに任せることができます。また、論理的にも

合成再利用の原則

基本的な導入

クラスとクラスの関係は、継承ではなく、集約/合成を使用しようとします。

論理的に近い「親子」関係がない場合は、継承を使わないようにしましょう。継承は強い結合をもたらしますが、集約は緩い結合をもたらします。リレーションシップ

設計原則 コア・アイデア

  1. アプリケーション内で変更が可能な箇所を見つけ、変更する必要のないコードと混在させず、分離しておきます。
  2. 実装ではなくインターフェイスのためのプログラミング
  3. 相互作用するオブジェクト間の疎結合設計に向けて
Read next

Javaスクリプトを始める

1 は、ブラウザのスクリプト言語です。 単一行コメントは // で始まり、複数行コメントは / で始まり / で終わります。

Jun 28, 2020 · 3 min read