CodeGym /Blog Java /Ngẫu nhiên /Mẫu thiết kế chiến lược

Mẫu thiết kế chiến lược

Xuất bản trong nhóm
CHÀO! Trong bài học hôm nay, chúng ta sẽ nói về Strategy pattern. Trong các bài học trước, chúng ta đã làm quen sơ qua với khái niệm kế thừa. Trong trường hợp bạn quên, tôi xin nhắc bạn rằng thuật ngữ này đề cập đến một giải pháp tiêu chuẩn cho một nhiệm vụ lập trình thông thường. Tại CodeGym, chúng tôi thường nói rằng bạn có thể google câu trả lời cho hầu hết mọi câu hỏi. Điều này là do nhiệm vụ của bạn, bất kể đó là gì, có thể đã được người khác giải quyết thành công. Các mẫu là các giải pháp đã được thử và đúng cho các nhiệm vụ phổ biến nhất hoặc các phương pháp để giải quyết các tình huống có vấn đề. Chúng giống như những "bánh xe" mà bạn không cần phải tự phát minh lại, nhưng bạn cần biết cách thức và thời điểm sử dụng chúng :) Một mục đích khác của các mẫu là thúc đẩy kiến ​​trúc thống nhất. Đọc mã của người khác không phải là nhiệm vụ dễ dàng! Mọi người viết mã khác nhau, bởi vì cùng một nhiệm vụ có thể được giải quyết theo nhiều cách. Nhưng việc sử dụng các mẫu giúp các lập trình viên khác nhau hiểu logic lập trình mà không cần đi sâu vào từng dòng mã (ngay cả khi nhìn thấy nó lần đầu tiên!). Hôm nay chúng ta xem xét một trong những mẫu thiết kế phổ biến nhất có tên là "Chiến lược". Design pattern: Strategy - 2Hãy tưởng tượng rằng chúng ta đang viết một chương trình sẽ hoạt động tích cực với các đối tượng Conveyance. Nó không thực sự quan trọng chính xác những gì chương trình của chúng tôi làm. Chúng tôi đã tạo một hệ thống phân cấp lớp với một lớp cha Conveyance và ba lớp con: Sedan , TruckF1Car .

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 {
}
Cả ba lớp con đều kế thừa hai phương thức chuẩn từ lớp cha: go()stop() . Chương trình của chúng tôi rất đơn giản: ô tô của chúng tôi chỉ có thể di chuyển về phía trước và đạp phanh. Tiếp tục công việc của mình, chúng tôi quyết định cung cấp cho ô tô một phương thức mới: fill() (có nghĩa là "đổ đầy bình xăng"). Chúng tôi đã thêm nó vào lớp cha 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!");
   }
}
Vấn đề có thể thực sự phát sinh trong một tình huống đơn giản như vậy không? Trên thực tế, họ đã có... Design pattern: Strategy - 3

public class Stroller extends Conveyance {

   public void fill() {
      
       // Hmm... This is a stroller for children. It doesn't need to be refueled :/
   }
}
Chương trình của chúng tôi hiện có phương tiện vận chuyển (xe đẩy em bé) không phù hợp lắm với khái niệm chung. Nó có thể có bàn đạp hoặc được điều khiển bằng radio, nhưng có một điều chắc chắn là nó sẽ không có chỗ để đổ xăng. Hệ thống phân cấp lớp của chúng tôi đã khiến các phương thức phổ biến được kế thừa bởi các lớp không cần chúng. Chúng ta nên làm gì trong tình huống này? Chà, chúng ta có thể ghi đè phương thức fill() trong lớp Stroller để không có gì xảy ra khi bạn cố gắng tiếp nhiên liệu cho xe đẩy:

public class Stroller extends Conveyance {

   @Override
   public void fill() {
       System.out.println("A stroller cannot be refueled!");
   }
}
Nhưng điều này khó có thể được gọi là một giải pháp thành công nếu không có lý do nào khác ngoài mã trùng lặp. Ví dụ, hầu hết các lớp sẽ sử dụng phương thức của lớp cha, nhưng phần còn lại sẽ buộc phải ghi đè lên nó. Nếu chúng ta có 15 lớp và chúng ta phải ghi đè hành vi trong 5-6 lớp trong số đó, việc sao chép mã sẽ trở nên khá rộng rãi. Có lẽ giao diện có thể giúp chúng tôi? Ví dụ, như thế này:

public interface Fillable {
  
   public void fill();
}
Chúng ta sẽ tạo giao diện Có thể điền bằng một phương thức fill() . Sau đó, những phương tiện vận chuyển cần được tiếp nhiên liệu sẽ thực hiện giao diện này, trong khi các phương tiện vận chuyển khác (ví dụ: xe đẩy trẻ em của chúng tôi) thì không. Nhưng tùy chọn này không phù hợp với chúng tôi. Trong tương lai, hệ thống phân cấp theo lớp của chúng ta có thể phát triển trở nên rất lớn (hãy tưởng tượng có bao nhiêu loại phương tiện vận chuyển khác nhau trên thế giới). Chúng tôi đã bỏ phiên bản trước liên quan đến thừa kế, vì chúng tôi không muốn ghi đè lên fill()phương pháp nhiều, nhiều lần. Bây giờ chúng ta phải thực hiện nó trong mọi lớp học! Và nếu chúng ta có 50 thì sao? Và nếu các thay đổi thường xuyên được thực hiện trong chương trình của chúng ta (và điều này hầu như luôn đúng với các chương trình thực tế!), thì chúng ta sẽ phải lướt qua tất cả 50 lớp và thay đổi hành vi của từng lớp theo cách thủ công. Vậy cuối cùng, chúng ta nên làm gì trong tình huống này? Để giải quyết vấn đề của chúng tôi, chúng tôi sẽ chọn một cách khác. Cụ thể, chúng ta sẽ tách biệt hành vi của lớp với chính lớp đó. Điều đó nghĩa là gì? Như bạn đã biết, mọi đối tượng đều có trạng thái (một tập hợp dữ liệu) và hành vi (một tập hợp các phương thức). Hành vi của lớp vận chuyển của chúng tôi bao gồm ba phương thức: go() , stop()fill() . Hai phương pháp đầu tiên là tốt như họ đang có. Nhưng chúng tôi sẽ chuyển phương pháp thứ ba ra khỏiLớp vận tải . Điều này sẽ tách hành vi khỏi lớp (chính xác hơn, nó sẽ chỉ tách một phần của hành vi, vì hai phương thức đầu tiên sẽ giữ nguyên vị trí của chúng). Vậy chúng ta nên đặt phương thức fill() ở đâu ? Không có gì xuất hiện trong tâm trí :/ Có vẻ như nó chính xác là nơi nó nên ở. Chúng tôi sẽ chuyển nó sang một giao diện riêng: FillStrategy !

public interface FillStrategy {

   public void fill();
}
Tại sao chúng ta cần một giao diện như vậy? Tất cả đều đơn giản. Bây giờ chúng ta có thể tạo một số lớp triển khai giao diện này:

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!");
   }
}
Chúng tôi đã tạo ra ba chiến lược hành vi: một cho ô tô thông thường, một cho xe hybrid và một cho xe đua Công thức 1. Mỗi chiến lược thực hiện một thuật toán tiếp nhiên liệu khác nhau. Trong trường hợp của chúng tôi, chúng tôi chỉ hiển thị một chuỗi trên bảng điều khiển, nhưng mỗi phương thức có thể chứa một số logic phức tạp. Chúng ta làm gì tiếp theo?

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!");
   }
  
}
Chúng tôi sử dụng giao diện FillStrategy của mình làm trường trong lớp cha Conveyance . Lưu ý rằng chúng tôi không chỉ ra cách triển khai cụ thể — chúng tôi đang sử dụng một giao diện. Các lớp ô tô sẽ cần triển khai cụ thể giao diện 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();
   }
}

Hãy nhìn vào những gì chúng ta có!

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();
   }
}
Đầu ra bảng điều khiển:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Tuyệt vời! Quá trình tiếp nhiên liệu hoạt động như bình thường! Nhân tiện, không có gì ngăn cản chúng ta sử dụng chiến lược làm tham số trong hàm tạo! Ví dụ, như thế này:

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());
   }
}
Hãy chạy phương thức main() của chúng ta (không thay đổi). Chúng tôi nhận được kết quả tương tự! Đầu ra bảng điều khiển:

Just refuel with gas! 
Refuel with gas or electricity — your choice! 
Refuel with gas only after all other pit stop procedures are complete!
Mẫu thiết kế chiến lược xác định một nhóm thuật toán, gói gọn từng thuật toán và đảm bảo rằng chúng có thể hoán đổi cho nhau. Nó cho phép bạn sửa đổi các thuật toán bất kể chúng được khách hàng sử dụng như thế nào (định nghĩa này, được lấy từ cuốn sách "Các mẫu thiết kế đầu tiên", có vẻ tuyệt vời đối với tôi). Design pattern: Strategy - 4Chúng tôi đã chỉ định nhóm thuật toán mà chúng tôi quan tâm (các cách tiếp nhiên liệu cho ô tô) trong các giao diện riêng biệt với các triển khai khác nhau. Chúng tôi tách chúng ra khỏi chính chiếc xe. Bây giờ nếu chúng ta cần thực hiện bất kỳ thay đổi nào đối với một thuật toán tiếp nhiên liệu cụ thể, nó sẽ không ảnh hưởng đến các lớp ô tô của chúng ta theo bất kỳ cách nào. Và để đạt được khả năng hoán đổi cho nhau, chúng ta chỉ cần thêm một phương thức setter duy nhất vào lớp Conveyance của mình:

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;
   }
}
Bây giờ chúng ta có thể thay đổi chiến lược một cách nhanh chóng:

public class Main {

   public static void main(String[] args) {

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

       stroller.fill();
   }
}
Nếu xe đẩy trẻ em đột nhiên chạy bằng xăng, chương trình của chúng tôi sẽ sẵn sàng xử lý tình huống này :) Và đó là về nó! Bạn đã học thêm một mẫu thiết kế chắc chắn sẽ rất cần thiết và hữu ích khi làm việc trong các dự án thực tế :) Cho đến lần sau!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION