CodeGym /Java Blog /ランダム /戦略設計パターン
John Squirrels
レベル 41
San Francisco

戦略設計パターン

ランダム グループに公開済み
やあ!今日のレッスンでは、戦略パターンについて話します。これまでのレッスンで、継承の概念についてはすでに簡単に理解しました。忘れた方のために、この用語は一般的なプログラミング タスクに対する標準ソリューションを指すことを思い出してください。CodeGym では、ほとんどすべての質問の答えを Google で検索できるとよく言います。なぜなら、あなたのタスクは、それが何であれ、おそらく他の誰かによってすでに成功裏に解決されているからです。パターンは、最も一般的なタスクに対する実証済みのソリューション、または問題のある状況を解決するための方法です。これらは「車輪」のようなもので、自分で再発明する必要はありませんが、いつどのように使用するかを知る必要があります:) パターンのもう 1 つの目的は、均一なアーキテクチャを促進することです。他人のコードを読むのは簡単なことではありません。誰もが異なるコードを書きますが、同じタスクをさまざまな方法で解決できるからです。しかし、パターンを使用すると、さまざまなプログラマーがコードの各行を深く掘り下げることなく、プログラミング ロジックを理解するのに役立ちます (初めて見たときでも!)。今日は、「戦略」と呼ばれる最も一般的なデザイン パターンの 1 つを見ていきます。 デザインパターン:戦略-2Conveyance オブジェクトを積極的に操作するプログラムを作成していると想像してください。私たちのプログラムが正確に何をするかはあまり重要ではありません。1 つのConveyance親クラスと 3 つの子クラス ( SedanTruckF1Car )を持つクラス階層を作成しました。

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {
}

public class Truck extends Conveyance {
}

public class F1Car extends Conveyance {
}
3 つの子クラスはすべて、親から 2 つの標準メソッドgo()stop()を継承します。私たちのプログラムは非常にシンプルです。私たちの車は前に進み、ブレーキをかけることしかできません。作業を続けて、車に新しいメソッドfill() (「ガソリンタンクを満タンにする」という意味) を与えることにしました。これをConveyance親クラスに追加しました。

public class Conveyance {

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {

       System.out.println("Braking!");
   }
  
   public void fill() {
       System.out.println("Refueling!");
   }
}
このような単純な状況で実際に問題が発生する可能性がありますか? 実際、彼らはすでに... デザインパターン:戦略-3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
私たちのプログラムには、一般的な概念にうまく適合しない乗り物 (ベビーカー) が含まれています。ペダルやラジコンが付いている可能性もあるが、確かなことは、ガソリンを注入する場所がないということだ。クラス階層により、共通メソッドが必要のないクラスに継承されてしまいます。この状況では何をすべきでしょうか? さて、ストローラークラスのfill()メソッドをオーバーライドして、ベビーカーに燃料を補給しようとしても何も起こらないようにできます。

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
しかし、コードの重複以外の理由がない限り、これは成功した解決策とはほとんど言えません。たとえば、ほとんどのクラスは親クラスのメソッドを使用しますが、残りのクラスはそれをオーバーライドする必要があります。15 個のクラスがあり、そのうち 5 ~ 6 個のクラスの動作をオーバーライドする必要がある場合、コードの重複はかなり広範囲になります。インターフェースが役に立つかも?たとえば、次のようになります。

public interface Fillable {
  
   public void fill();
}
1 つのfill()メソッドを使用してFillableインターフェイス を作成します。次に、燃料を補給する必要がある乗り物にはこのインターフェイスが実装されますが、他の乗り物 (たとえば、ベビーカー) には実装されません。しかし、この選択肢は私たちには適していません。将来的には、クラス階層が非常に大きくなる可能性があります (世界にはさまざまな種類の交通機関がどれほどあるのか想像してみてください)。fill()をオーバーライドしたくないため、継承を伴う以前のバージョンを放棄しました。何度も何度も。次に、それをすべてのクラスに実装する必要があります。50 個ある場合はどうなるでしょうか? また、プログラムに頻繁な変更が加えられる場合 (実際のプログラムではほとんど常に当てはまります!)、50 クラスすべてを急いで調べて、各クラスの動作を手動で変更する必要があります。では、この状況では結局どうすればいいのでしょうか?問題を解決するには、別の方法を選択します。つまり、クラスの動作をクラス自体から分離します。どういう意味ですか?ご存知のとおり、すべてのオブジェクトには状態 (データのセット) と動作 (メソッドのセット) があります。搬送クラスの動作は、go()stop()、およびfill()の 3 つのメソッドで構成されます。最初の 2 つの方法はそのままで問題ありません。ただし、3 番目のメソッドを伝達クラス。これにより、動作がクラスから分離されます (最初の 2 つのメソッドはそのまま残るため、より正確には、動作の一部のみが分離されます)。それでは、 fill()メソッドをどこに配置すればよいでしょうか? 何も思い浮かびません :/ まさにあるべき場所にあるようです。これを別のインターフェイスFillStrategyに移動します。

public interface FillStrategy {

   public void fill();
}
なぜそのようなインターフェースが必要なのでしょうか? それはすべて簡単です。これで、このインターフェースを実装するいくつかのクラスを作成できます。

public class HybridFillStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas or electricity — your choice!");
   }
}

public class F1PitstopStrategy implements FillStrategy {
  
   @Override
   public void fill() {
       System.out.println("Refuel with gas only after all other pit stop procedures are complete!");
   }
}

public class StandardFillStrategy implements FillStrategy {
   @Override
   public void fill() {
       System.out.println("Just refuel with gas!");
   }
}
私たちは 3 つの行動戦略を作成しました。1 つは普通車用、もう 1 つはハイブリッド車用、もう 1 つは F1 レースカー用です。各戦略は異なる給油アルゴリズムを実装します。この例では、コンソールに文字列を表示するだけですが、各メソッドには複雑なロジックが含まれる可能性があります。次は何をするの?

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
  
}
FillStrategyインターフェイスをConveyance親クラスのフィールドとして 使用します。特定の実装を示しているわけではないことに注意してください。インターフェイスを使用しています。車のクラスには、FillStrategyインターフェイスの特定の実装が必要です。

public class F1Car extends Conveyance {

   public F1Car() {
       this.fillStrategy = new F1PitstopStrategy();
   }
}

public class HybridCar extends Conveyance {

   public HybridCar() {
       this.fillStrategy = new HybridFillStrategy();
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       this.fillStrategy = new StandardFillStrategy();
   }
}

何が得られたか見てみましょう!

public class Main {

   public static void main(String[] args) {

       Conveyance sedan = new Sedan();
       Conveyance hybrid = new HybridCar();
       Conveyance f1car = new F1Car();

       sedan.fill();
       hybrid.fill();
       f1car.fill();
   }
}
コンソール出力:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
素晴らしい!給油プロセスは正常に機能します。ちなみに、ストラテジをコンストラクターのパラメーターとして使用することを妨げるものはありません。たとえば、次のようになります。

public class Conveyance {

   private FillStrategy fillStrategy;

   public Conveyance(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }

   public void fill() {
       this.fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }
}

public class Sedan extends Conveyance {

   public Sedan() {
       super(new StandardFillStrategy());
   }
}



public class HybridCar extends Conveyance {

   public HybridCar() {
       super(new HybridFillStrategy());
   }
}

public class F1Car extends Conveyance {

   public F1Car() {
       super(new F1PitstopStrategy());
   }
}
main()メソッド を実行してみましょう(変更されていないままです)。同じ結果が得られます。コンソール出力:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
戦略設計パターンは、アルゴリズムのファミリーを定義し、それぞれをカプセル化し、それらが交換可能であることを保証します。これにより、クライアントがアルゴリズムをどのように使用するかに関係なく、アルゴリズムを変更できます (この定義は、書籍「Head First Design Patterns」から抜粋したもので、私には優れていると思います)。 デザインパターン:戦略-4私たちは、関心のあるアルゴリズムのファミリー (車に燃料を補給する方法) を、異なる実装を備えた個別のインターフェイスですでに指定しました。それらを車自体から切り離しました。特定の給油アルゴリズムに変更を加える必要がある場合でも、車のクラスにはまったく影響しません。互換性を実現するには、単一のセッター メソッドをConveyanceクラスに追加するだけです。

public class Conveyance {

   FillStrategy fillStrategy;

   public void fill() {
       fillStrategy.fill();
   }

   public void go() {
       System.out.println("Moving forward");
   }

   public void stop() {
       System.out.println("Braking!");
   }

   public void setFillStrategy(FillStrategy fillStrategy) {
       this.fillStrategy = fillStrategy;
   }
}
これで、その場で戦略を変更できるようになりました。

public class Main {

   public static void main(String[] args) {

       Stroller stroller= new Stroller();
       stroller.setFillStrategy(new StandardFillStrategy());

       stroller.fill();
   }
}
ベビーカーが突然ガソリンで動き始めたとしても、私たちのプログラムはこのシナリオに対処する準備ができています :) 以上です。これで、実際のプロジェクトに取り組むときに間違いなく不可欠で役立つデザイン パターンが 1 つ学べました :) 次回まで!
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION