Java >> Java チュートリアル >  >> Java

Java におけるオブジェクト指向設計の原則

はじめに

設計原則 設計を選択する際の経験則として使用される、一般化されたアドバイスまたは実証済みの優れたコーディング プラクティスです。

これらはデザイン パターンと似た概念ですが、主な違いは、デザインの原則がより抽象的で一般化されていることです。それらはハイレベルです 多くの場合、多くの異なるプログラミング言語や異なるパラダイムにさえ適用できるアドバイスです。

デザイン パターンも抽象化または一般化された優れたプラクティスですが、より具体的で実用的な 低レベル を提供します。 一般化されたコーディング手法だけでなく、問題のクラス全体に関連しています。

この記事には、オブジェクト指向パラダイムにおける最も重要な設計原則のいくつかがリストされていますが、これは決して網羅的なリストではありません。

  • 同じことを繰り返さない (DRY) 原則
  • Keep It Simple and Stupid (KISS) 原則
  • 単一責任の原則 (SRP)
  • オープン/クローズの原則
  • リスコフ置換原理 (LSP)
  • インターフェース分離の原則 (ISP)
  • 依存性逆転の原則 (DIP)
  • 継承より合成の原則

SRP、LSP、Open/Closed、DIP の原則はまとめてまとめられ、SOLID と呼ばれます。

同じことを繰り返さない (DRY) 原則

Don't Repeat Yourself (DRY) 原則は、プログラミング パラダイム全体で共通の原則ですが、OOP では特に重要です。原則によると:

すべての知識またはロジックは、システム内で単一の明確な表現を持つ必要があります .

OOP に関して言えば、これは抽象クラス、インターフェイス、およびパブリック定数を利用することを意味します。クラス間で共通の機能がある場合は常に、それらを共通の親クラスに抽象化するか、インターフェースを使用してそれらの機能を結合することが理にかなっています:

public class Animal {
    public void eatFood() {
        System.out.println("Eating food...");
    }
}

public class Cat extends Animal {
    public void meow() {
        System.out.println("Meow! *purrs*");
    }
}

public class Dog extends Animal {
    public void woof() {
        System.out.println("Woof! *wags tail*");
    }
}

両方とも Cat そして Dog 食べ物を食べる必要がありますが、話し方が異なります。食べ物を食べることは彼らにとって共通の機能であるため、 Animal のような親クラスに抽象化できます。 そしてクラスを拡張してもらいます。

現在、両方のクラスが食べ物を食べるという同じ機能を実装する代わりに、それぞれ独自のロジックに集中できます。

Cat cat = new Cat();
cat.eatFood();
cat.meow();

Dog dog = new Dog();
dog.eatFood();
dog.woof();

出力は次のようになります:

Eating food...
Meow! *purrs*
Eating food...
Woof! *wags tail*

複数回使用される定数がある場合は常に、それをパブリック定数として定義することをお勧めします:

static final int GENERATION_SIZE = 5000;
static final int REPRODUCTION_SIZE = 200;
static final int MAX_ITERATIONS = 1000;
static final float MUTATION_SIZE = 0.1f;
static final int TOURNAMENT_SIZE = 40;

たとえば、これらの定数を数回使用し、最終的には手動で値を変更して遺伝的アルゴリズムを最適化します。これらの各値を複数の場所で更新する必要があると、間違いを犯しやすくなります。

また、間違いを犯して実行中にこれらの値をプログラムで変更したくないため、final も導入しています。

注: Java の命名規則により、これらはアンダースコア ("_") で区切られた単語で大文字にする必要があります。

この原則の目的は、メンテナンスを容易にすることです 機能や定数が変更された場合、コードを 1 か所だけ編集する必要があるためです。これにより、作業が簡単になるだけでなく、今後間違いが起こらないようになります。複数の場所でコードを編集するのを忘れたり、プロジェクトに精通していない他の誰かがあなたがコードを繰り返したことを知らず、1 か所だけで編集してしまう可能性があります。

ただし、この原則を使用するときは、常識を適用することが重要です。最初に同じコードを使用して 2 つの異なることを行ったとしても、それら 2 つのことを常に同じ方法で処理する必要があるとは限りません。

これは通常、構造を処理するために同じコードが使用されているにもかかわらず、構造が実際には類似していない場合に発生します。メソッドが無関係で理解できない場所から呼び出されるため、コードが「過度に乾燥」して本質的に判読不能になることもあります。

優れたアーキテクチャはこれを償却できますが、実際には問題が発生する可能性があります.

DRY原則の違反

DRY 原則の違反は、しばしば WET ソリューションと呼ばれます。 WET は複数のものの省略形になることがあります:

  • タイピングを楽しんでいます
  • みんなの時間を無駄にする
  • 毎回書く
  • すべてを 2 回書く

WET ソリューションが必ずしも悪いわけではありません。本質的に異なるクラスで繰り返しが推奨される場合や、コードを読みやすくしたり、相互依存を少なくしたりするために、繰り返しが推奨されることがあります。

Keep It Simple and Stupid (KISS) 原則

Keep it Simple and Stupid (KISS) の原則は、コードをシンプルで人間にとって読みやすいものにすることを思い出させるものです。メソッドが複数のユース ケースを処理する場合は、それらを小さな関数に分割します。複数の機能を実行する場合は、代わりに複数のメソッドを作成してください。

この原則の核心は、ほとんどの ただし、効率が非常にでない限り 重大なことですが、別のスタック呼び出しがプログラムのパフォーマンスに深刻な影響を与えることはありません。実際、一部のコンパイラやランタイム環境では、メソッド呼び出しをインライン実行に単純化することさえあります。

一方、判読不能で長いメソッドは、人間のプログラマーにとって維持するのが非常に難しくなり、バグを見つけるのが難しくなり、DRY に違反していることに気付くかもしれません。そのうちの 1 つだけを行うので、別の方法を作成します。

全体として、独自のコードに悩まされ、各部分が何をするかわからない場合は、再評価の時期です。

読みやすくするためにデザインを微調整できることはほぼ確実です。また、まだ記憶に新しいうちに設計者として苦労している場合は、将来初めてそれを見た人がどのように機能するかを考えてください。

単一責任の原則 (SRP)

単一責任の原則 (SRP) は、1 つのクラスに 2 つの機能が存在してはならないと述べています。時々、次のように言い換えられます:

「クラスには、変更する理由が 1 つだけある必要があります。」

「変更する理由」がクラスの責任である場合。複数の責任がある場合、ある時点でそのクラスを変更する理由が他にもあります。

これは、機能の更新が必要な場合に、影響を受ける可能性のある同じクラスに複数の個別の機能があってはならないことを意味します。

この原則により、バグへの対処、相互依存関係を混乱させることなく変更を実装すること、およびクラスが必要としないメソッドを実装または継承することなくクラスから継承することが容易になります。

これにより、依存関係に大きく依存するようになるように見えるかもしれませんが、この種のモジュール性ははるかに重要です。クラス間のある程度の依存関係は避けられないため、それに対処するための原則とパターンも用意されています。

たとえば、アプリケーションがデータベースから製品情報を取得し、それを処理して、最終的にエンド ユーザーに表示する必要があるとします。

単一のクラスを使用して、データベース呼び出しを処理し、情報を処理し、情報をプレゼンテーション層にプッシュできます。ただし、これらの機能をバンドルすると、コードが判読できなくなり、非論理的になります。

代わりに、ProductService などのクラスを定義します。 データベースから製品を取得する ProductController 情報を処理し、それをプレゼンテーション レイヤー (HTML ページまたは別のクラス/GUI) に表示します。

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

オープン/クローズ 原則では、クラスまたはオブジェクトとメソッドは拡張に対してオープンである必要がありますが、変更に対してはクローズする必要があると述べています。

これが本質的に意味することは、可能な将来の更新を念頭に置いてクラスとモジュールを設計する必要があるということです。そのため、それらの動作を拡張するためにクラス自体を変更する必要のない汎用的な設計にする必要があります。

フィールドやメソッドを追加することはできますが、古いメソッドを書き直す必要がないように、古いフィールドを削除し、古いコードを変更して再び機能させるようにします。事前に考えることで、要件の更新の前後に安定したコードを書くことができます。

この原則は、下位互換性を確保し、リグレッション (更新後にプログラムの機能や効率が低下したときに発生するバグ) を防ぐために重要です。

リスコフ置換原理 (LSP)

Liskov Substitution Principle によると (LSP)、派生クラスは、コードの動作を変更せずに基本クラスを置き換えることができる必要があります。

この原則は、インターフェース分離の原則と密接に関連しています。 単一責任の原則 つまり、これらのいずれかに違反すると、LSP にも違反する可能性があります (または違反になる可能性があります)。これは、クラスが複数のことを行う場合、それを拡張するサブクラスがそれらの 2 つ以上の機能を有意義に実装する可能性が低いためです。

オブジェクトの関係について人々が考える一般的な方法 (少し誤解を招く場合があります) は、is 関係 が必要であるというものです。 クラスの間。

例:

  • Car Vehicle です
  • TeachingAssistaint CollegeEmployee です

これらの関係は両方向には進まないことに注意することが重要です。 Car という事実 Vehicle です Vehicle という意味ではないかもしれません Car です - Motorcycle の場合もあります 、 BicycleTruck ...

これが誤解を招く可能性がある理由は、自然言語で考えるときに人々が犯すよくある間違いです。たとえば、Square かどうかを尋ねた場合 Rectangle と「関係」があります 、あなたは自動的に「はい」と答えるかもしれません。

結局、私たちは幾何学から正方形が であることを知っています 長方形の特殊なケース。ただし、構造の実装方法によっては、そうではない場合があります:

public class Rectangle {
    protected double a;
    protected double b;

    public Rectangle(double a, double b) {
        this.a = a;
        this.b = b;
    }

    public void setA(double a) {
        this.a = a;
    }

    public void setB(double b) {
        this.b = b;
    }

    public double calculateArea() {
        return a*b;
    }
}

Square を継承してみましょう。 同じパッケージ内:

public class Square extends Rectangle {
    public Square(double a) {
        super(a, a);
    }

    @Override
    public void setA(double a) {
        this.a = a;
        this.b = a;
    }

    @Override
    public void setB(double b) {
        this.a = b;
        this.b = b;
    }
}

ここでセッターが実際に両方の a を設定していることに気付くでしょう。 と b .あなたの中には、すでに問題を推測しているかもしれません。 Square を初期化したとしましょう ポリモーフィズムを適用して Rectangle 内に含めます 変数:

Rectangle rec = new Square(5);

そして、プログラムの後半で、おそらく完全に別の関数で、これらのクラスの実装とは関係のない別のプログラマーが、四角形のサイズを変更したいと判断したとしましょう。彼らは次のようなことを試みるかもしれません:

rec.setA(6);
rec.setB(3);

まったく予期しない動作が発生し、問題の原因を突き止めるのが困難になる場合があります。

rec.calculateArea() を使用しようとした場合 結果は 18 になりません 一辺の長さが 6 の長方形から予想されるように と 3 .

結果は代わりに 9 になります それらの長方形は実際には正方形で、長さ 3 の 2 つの等しい辺があるためです。 .

正方形はそのように機能するため、これはまさにあなたが望んでいた動作であると言うかもしれませんが、それにもかかわらず、それは長方形から期待される動作ではありません.

したがって、継承するときは、動作を念頭に置く必要があります それらは本当に機能的に互換性がありますか プログラムでの使用のコンテキストの外で概念が似ているだけでなく、コード内で。

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

インターフェース分離の原則 (ISP) は、クライアントが完全に使用していないインターフェイスに依存することを強制されるべきではないと述べています。これは、インターフェイスが保証する機能に必要な最小限のメソッドのセットを持つ必要があり、1 つの機能のみに制限する必要があることを意味します。

たとえば、Pizza addPepperoni() を実装するためにインターフェイスを必要とするべきではありません これは、すべての種類のピザで利用できる必要はないためです。このチュートリアルでは、すべてのピザにソースがあり、焼く必要があり、例外は 1 つもないと仮定しましょう。

これは、インターフェイスを定義できるときです:

public interface Pizza {
    void addSauce();
    void bake();
}

次に、いくつかのクラスを使用してこれを実装しましょう:

public class VegetarianPizza implements Pizza {
    public void addMushrooms() {System.out.println("Adding mushrooms");}

    @Override
    public void addSauce() {System.out.println("Adding sauce");}

    @Override
    public void bake() {System.out.println("Baking the vegetarian pizza");}
}

public class PepperoniPizza implements Pizza {
    public void addPepperoni() {System.out.println("Adding pepperoni");}

    @Override
    public void addSauce() {System.out.println("Adding sauce");}

    @Override
    public void bake() {System.out.println("Baking the pepperoni pizza");}
}

VegetarianPizza PepperoniPizza にはキノコがあります ペパロニがあります。もちろん、両方ともソースが必要であり、焼く必要があります。これもインターフェースで定義されています。

addMushrooms() の場合 または addPepperoni() メソッドがインターフェイスに配置されていた場合、両方を必要とせず、それぞれ 1 つしか必要としない場合でも、両方のクラスがそれらを実装する必要があります。

絶対に必要な機能を除いて、すべてのインターフェイスを削除する必要があります。

依存性逆転の原則 (DIP)

依存性逆転の原則によると (DIP)、高レベル モジュールと低レベル モジュールは、低レベル モジュールの変更 (または交換) が高レベル モジュールの (多くの) やり直しを必要としないような方法で分離する必要があります。そのため、低レベル モジュールと高レベル モジュールの両方が相互に依存するのではなく、インターフェイスなどの抽象化に依存する必要があります。

DIP が述べているもう 1 つの重要なことは次のとおりです。

抽象化は詳細に依存すべきではありません。詳細 (具体的な実装) は、抽象化に依存する必要があります。

この原則は重要です。モジュールを切り離すことで、システムの複雑さが軽減され、保守と更新が容易になり、テストが容易になり、再利用が容易になるからです。特に単体テストと再利用性に関して、これがどれほどゲームチェンジャーであるかを十分に強調することはできません。コードが十分に汎用的に書かれている場合、別のプロジェクトでアプリケーションを簡単に見つけることができますが、あまりにも具体的で元のプロジェクトの他のモジュールと相互依存しているコードは、元のプロジェクトから分離するのが難しくなります.

この原則は、DIP の実際の実装または目標である依存性注入と密接に関連しています。 DI は要約すると、2 つのクラスが依存している場合、それらの機能を抽象化し、両方とも相互ではなく抽象化に依存する必要があります。これにより、基本的に、機能を維持しながら実装の詳細を変更できるようになります。

依存性逆転の原則 および制御の反転 (IoC) は、技術的には正しくありませんが、同じ意味で使用される人もいます。

依存関係の逆転は、デカップリングへと導きます 依存性注入を使用して Inversion of Control Container を介して . IoC コンテナーの別の名前は、依存性注入コンテナー である可能性が非常に高いです 、古い名前が残っていますが。

構成より継承の原則

多くの場合、構成を優先する必要があります 継承以上 システムを設計するとき。 Java では、これは インターフェース をより頻繁に定義する必要があることを意味します クラスを定義するのではなく、それらを実装します

Car については既に説明しました Vehicle です クラスが互いに継承するかどうかを決定するために人々が使用する一般的な指針として.

考えるのは難しく、リスコフの置換原則に違反する傾向がありますが、この考え方は、開発の後半でコードを再利用および転用する場合に非常に問題になります。

ここでの問題を次の例で説明します:

SpaceshipAirplane 抽象クラス FlyingVehicle を拡張します 、 Car の間 と Truck GroundVehicle を拡張 .それぞれに車両の種類に適したそれぞれの方法があり、これらの用語で考えると、自然にそれらを抽象化してグループ化します。

この継承構造は、オブジェクトが何であるかという観点からオブジェクトについて考えることに基づいています。 彼らがすることの代わりに .

これに関する問題は、新しい要件が階層全体のバランスを崩す可能性があることです。この例で、上司が突然やってきて、クライアントが今すぐ空飛ぶ車を欲しがっていることを知らせてきたらどうしますか? FlyingVehicle から継承する場合 、 drive() を実装する必要があります 同じ機能が既に存在しているにもかかわらず、DRY 原則に違反しており、その逆も同様です。

public class FlyingVehicle {
    public void fly() {}
    public void land() {}
}

public class GroundVehicle {
    public void drive() {}
}

public class FlyingCar extends FlyingVehicle {

    @Override
    public void fly() {}

    @Override
    public void land() {}

    public void drive() {}
}

public class FlyingCar2 extends GroundVehicle {

    @Override
    public void drive() {}

    public void fly() {}
    public void land() {}
}

Java を含むほとんどの言語では多重継承が許可されていないため、これらのクラスのいずれかを拡張することを選択できます。ただし、どちらの場合も、もう一方の機能を継承することはできず、書き直す必要があります。

この新しい FlyingCar に合わせてアーキテクチャ全体を変更する方法を見つけられるかもしれません クラスですが、開発の深さによっては、コストのかかるプロセスになる可能性があります。

この問題を考えると、共通の機能に基づいて一般化することで、この混乱全体を回避することができます。 固有の類似性の代わりに .これは、多くの組み込み Java メカニズムが開発された方法です。

クラスがすべての機能を実装し、子クラスを親クラスの代わりに使用できる場合は、継承 を使用します。 .

クラスで特定の機能を実装する場合は、composition を使用します .

Runnable を使用します 、 Comparable など。よりクリーンであるため、メソッドを実装するいくつかの抽象クラスを使用する代わりに、コードをより再利用可能にし、以前に作成された機能を使用するために必要なものに準拠する新しいクラスを簡単に作成できるようにします。

これにより、依存関係が重要な機能を破壊し、コード全体で連鎖反応を引き起こすという問題も解決されます。コードを新しいタイプのものに対応させる必要があるときに大きな問題を抱える代わりに、その新しいものを以前に設定された標準に準拠させ、古いものと同じように機能させることができます。

この車両の例では、インターフェース Flyable を実装するだけで済みます。 および Drivable 抽象化と継承を導入する代わりに。

私たちの Airplane および Spaceship Flyable を実装できます 、私たちの Car および Truck Drivable を実装できます 、そして新しい FlyingCar 両方を実装できます .

クラス構造の変更は必要なく、主要な DRY 違反も、同僚の混乱もありません。 まったく同じ必要がある場合 複数のクラスで機能を実装する場合、インターフェイスのデフォルト メソッドを使用して実装し、DRY に違反しないようにすることができます。

結論

設計原則は開発者のツールキットの重要な部分であり、ソフトウェアを設計する際により意識的な選択を行うことは、慎重で将来を見据えた設計のニュアンスを明確にするのに役立ちます.

ほとんどの開発者は理論ではなく経験を通じてこれらを真に学びますが、理論は新しい視点を与えてくれ、特に について、より思慮深い設計の習慣に向けることができます。 そのでのインタビュー これらの原則に基づいてシステム全体を構築した会社


Java タグ