Xin chào! Trong các bài giảng trước, chúng ta đã gặp một khái niệm như “mẫu thiết kế”. Trong trường hợp bạn quên, hãy để chúng tôi nhắc bạn: thuật ngữ này biểu thị một giải pháp tiêu chuẩn nhất định cho một vấn đề phổ biến trong lập trình.
Tại JavaRush, chúng tôi thường nói rằng câu trả lời cho hầu hết mọi câu hỏi đều có thể được tìm thấy trên Google. Do đó, ai đó có thể đã giải quyết thành công một vấn đề tương tự như của bạn. Vì vậy, các mẫu là các giải pháp đã được thử nghiệm qua thời gian và thực tế cho các vấn đề hoặc phương pháp phổ biến nhất để giải quyết các tình huống vấn đề. Đây chính là những chiếc “xe đạp” mà trong mọi trường hợp bạn không cần phải tự phát minh ra, nhưng bạn cần biết cách thức và thời điểm áp dụng chúng :) Một nhiệm vụ khác của các mẫu là đưa kiến trúc theo một tiêu chuẩn duy nhất. Đọc mã của người khác không phải là một nhiệm vụ dễ dàng! Mọi người viết nó theo cách khác nhau, bởi vì cùng một vấn đề có thể được giải quyết bằng nhiều cách. Nhưng việc sử dụng các mẫu cho phép các lập trình viên khác nhau hiểu logic của chương trình mà không cần đi sâu vào từng dòng mã (ngay cả khi họ nhìn thấy nó lần đầu tiên!) Hôm nay chúng ta sẽ xem xét một trong những mẫu phổ biến nhất được gọi là “Chiến lược”.
Hãy tưởng tượng rằng chúng ta đang viết một chương trình hoạt động tích cực với đối tượng Xe hơi. Trong trường hợp này, việc chương trình của chúng tôi thực hiện chính xác những gì thậm chí còn không đặc biệt quan trọng. Để làm điều này, chúng tôi đã tạo một hệ thống kế thừa với một lớp cha
Chúng tôi đã tách nhóm thuật toán mà chúng tôi quan tâm (các loại ô tô tiếp nhiên liệu) thành các giao diện riêng biệt với một số cách triển khai. Chúng tôi đã tách chúng ra khỏi bản chất của chiếc xe. Do đó, bây giờ, nếu chúng ta cần thực hiện bất kỳ thay đổi nào đối với quy trình tiếp nhiên liệu này hoặc quy trình tiếp nhiên liệu kia, điều này sẽ không ảnh hưởng đến các loại ô tô của chúng ta dưới bất kỳ hình thức nào. Về khả năng thay thế lẫn nhau, để đạt được điều đó, chúng ta chỉ cần thêm một phương thức setter vào lớp của mình
![Mẫu thiết kế “Chiến lược” - 2](https://cdn.javarush.com/images/article/d36666bd-a280-4878-8d6f-ab5cff873cdd/800.jpeg)
Auto
và ba lớp con: Sedan
, Truck
và F1Car
.
public class Auto {
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
public class Sedan extends Auto {
}
public class Truck extends Auto {
}
public class F1Car extends Auto {
}
Cả ba lớp con đều kế thừa hai phương thức tiêu chuẩn từ lớp cha - gas()
và stop()
chương trình của chúng tôi hoàn toàn đơn giản: ô tô chỉ có thể lái về phía trước và phanh. Tiếp tục công việc của mình, chúng tôi quyết định bổ sung một phương pháp mới cho ô tô - fill()
(tiếp nhiên liệu). Hãy thêm nó vào lớp cha Auto
:
public class Auto {
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
public void fill() {
System.out.println("Заправить бензин!");
}
}
Có vẻ như vấn đề có thể nảy sinh trong một tình huống đơn giản như vậy? Chà, trên thực tế, họ đã phát sinh rồi... ![Mẫu thiết kế “Chiến lược” - 3](https://cdn.javarush.com/images/article/a4a7d554-f812-47f0-8652-6d958446528d/800.jpeg)
public class ChildrenBuggies extends Auto {
public void fill() {
//хм... Это детский багги, его не надо заправлять :/
}
}
Một chiếc ô tô đã xuất hiện trong chương trình của chúng tôi không phù hợp với khái niệm chung - xe buggy dành cho trẻ em. Nó có thể chạy bằng bàn đạp hoặc điều khiển bằng sóng vô tuyến, nhưng có một điều chắc chắn là - không có nơi nào để đổ xăng vào. Sơ đồ kế thừa của chúng tôi đã dẫn đến việc chúng tôi phải loại bỏ các phương thức phổ biến ngay cả đối với các lớp không cần đến chúng. Chúng ta nên làm gì trong tình huống như vậy? Chà, ví dụ, bạn có thể ghi đè phương thức fill()
trong lớp ChildrenBuggies
để khi bạn cố gắng tiếp nhiên liệu cho xe, sẽ không có gì xảy ra:
public class ChildrenBuggies extends Auto {
@Override
public void fill() {
System.out.println("Игрушечную машину нельзя заправить!");
}
}
Nhưng giải pháp này khó có thể gọi là thành công, ít nhất là do trùng lặp mã. Ví dụ, hầu hết các lớp sẽ sử dụng một phương thức từ lớp cha, nhưng các lớp khác sẽ buộc phải ghi đè lên nó. Nếu chúng tôi có 15 lớp và trong lớp 5-6, chúng tôi buộc phải ghi đè hành vi, thì 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ụ: cái này:
public interface Fillable {
public void fill();
}
Chúng ta sẽ tạo một giao diện Fillable
với một phương thức fill()
. Theo đó, những chiếc xe cần đổ xăng sẽ thực hiện giao diện này, còn những chiếc xe khác (ví dụ như xe buggy của chúng tôi) thì không. Nhưng tùy chọn này cũng sẽ không phù hợp với chúng tôi. Hệ thống phân cấp giai cấp của chúng ta có thể tăng lên một số lượng rất lớn trong tương lai (hãy tưởng tượng có bao nhiêu loại ô tô khác nhau trên thế giới). Chúng tôi đã bỏ tùy chọn kế thừa trước đó vì chúng tôi không muốn ghi đè fill()
. Ở đây chúng ta sẽ phải triển khai nó trong mọi lớp! Điều gì sẽ xảy ra nếu chúng ta có 50 người trong số họ? Và nếu những thay đổi thường xuyên được thực hiện đối với chương trình của chúng tôi (và trong các chương trình thực, điều này hầu như luôn xảy ra!), Chúng tôi sẽ phải chạy loanh quanh với cái lưỡi thè ra giữa tất cả 50 lớp và thay đổi hành vi của từng lớp một cách thủ công. Vậy cuối cùng chúng ta nên làm gì? Để giải quyết vấn đề của chúng ta, hãy chọn một con đường khác. Cụ thể, hãy tách hành vi của lớp chúng ta khỏi chính lớp đó. Nó có nghĩa là gì? Như bạn đã biết, bất kỳ đối tượng nào cũng có trạng thái (tập hợp dữ liệu) và hành vi (tập hợp các phương thức). Hành vi của lớp máy của chúng tôi bao gồm ba phương thức - gas()
, stop()
và fill()
. Hai phương pháp đầu tiên đều ổn. Nhưng chúng ta sẽ di chuyển phương thức thứ ba ra ngoài lớp Auto
. Đây sẽ là sự tách biệt hành vi khỏi lớp (chính xác hơn là chúng tôi chỉ tách một phần hành vi - hai phương thức đầu tiên vẫn được giữ nguyên). Chúng ta nên di chuyển phương pháp của mình ở đâu fill()
? Không có gì hiện lên trong đầu tôi ngay lập tức:/ Anh ấy dường như hoàn toàn ở đúng vị trí của mình. 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 giao diện này? Nó đơn giản. Bây giờ chúng ta có thể tạo một số lớp sẽ triển khai giao diện này:
public class HybridFillStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Заправляем бензином or электричеством на выбор!");
}
}
public class F1PitstopStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Заправляем бензин только после всех остальных procedures пит-стопа!");
}
}
public class StandartFillStrategy implements FillStrategy {
@Override
public void fill() {
System.out.println("Просто заправляем бензин!");
}
}
Chúng tôi đã tạo ra ba chiến lược hành vi - dành cho ô tô thông thường, ô tô hybrid và ô tô 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 riêng biệt. Trong trường hợp của chúng tôi, đây chỉ là đầu ra của bảng điều khiển, nhưng có thể có một số logic phức tạp bên trong phương thức. Tiếp theo chúng ta nên làm gì với điều này?
public class Auto {
FillStrategy fillStrategy;
public void fill() {
fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
Chúng tôi sử dụng giao diện của mình FillStrategy
làm trường trong lớp cha Auto
. Xin lưu ý: chúng tôi không chỉ định cách triển khai cụ thể mà chỉ sử dụng giao diện. Và chúng ta sẽ cần triển khai giao diện cụ thể FillStrategy
trong các lớp ô tô con:
public class F1Car extends Auto {
public F1Car() {
this.fillStrategy = new F1PitstopStrategy();
}
}
public class HybridAuto extends Auto {
public HybridAuto() {
this.fillStrategy = new HybridFillStrategy();
}
}
public class Sedan extends Auto {
public Sedan() {
this.fillStrategy = new StandartFillStrategy();
}
}
Hãy xem những gì chúng ta có:
public class Main {
public static void main(String[] args) {
Auto sedan = new Sedan();
Auto hybrid = new HybridAuto();
Auto f1car = new F1Car();
sedan.fill();
hybrid.fill();
f1car.fill();
}
}
Đầu ra của bảng điều khiển:
Просто заправляем бензин!
Заправляем бензином or электричеством на выбор!
Заправляем бензин только после всех остальных procedures пит-стопа!
Tuyệt vời, quá trình tiếp nhiên liệu diễn ra 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 Auto {
private FillStrategy fillStrategy;
public Auto(FillStrategy fillStrategy) {
this.fillStrategy = fillStrategy;
}
public void fill() {
this.fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
}
public class Sedan extends Auto {
public Sedan() {
super(new StandartFillStrategy());
}
}
public class HybridAuto extends Auto {
public HybridAuto() {
super(new HybridFillStrategy());
}
}
public class F1Car extends Auto {
public F1Car() {
super(new F1PitstopStrategy());
}
}
Hãy chạy phương thức của chúng ta main()
(nó không thay đổi) và nhận được kết quả tương tự! Đầu ra của bảng điều khiển:
Просто заправляем бензин!
Заправляем бензином or электричеством на выбор!
Заправляем бензин только после всех остальных procedures пит-стопа!
Mẫu Chiến lược xác định một nhóm thuật toán, đóng gói từng thuật toán và đảm bảo rằng chúng có thể thay thế cho nhau. Nó cho phép bạn sửa đổi các thuật toán bất kể việc sử dụng chúng ở phía máy khách (định nghĩa này được lấy từ cuốn sách “Khám phá các mẫu thiết kế” và đối với tôi có vẻ cực kỳ thành công). ![Mẫu thiết kế “Chiến lược” - 4](https://cdn.javarush.com/images/article/6d9ab390-1860-4470-ae46-384250d4d119/512.jpeg)
Auto
:
public class Auto {
FillStrategy fillStrategy;
public void fill() {
fillStrategy.fill();
}
public void gas() {
System.out.println("Едем вперед");
}
public void stop() {
System.out.println("Тормозим!");
}
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) {
ChildrenBuggies buggies = new ChildrenBuggies();
buggies.setFillStrategy(new StandartFillStrategy());
buggies.fill();
}
}
Nếu đột nhiên xe buggy của trẻ em bắt đầu đổ đầy xăng, chương trình của chúng tôi sẽ sẵn sàng cho tình huống như vậy :) Thực ra chỉ có vậy thôi! Bạn đã học được một mẫu thiết kế khác mà bạn chắc chắn sẽ cần và sẽ giúp bạn nhiều lần khi làm việc trong các dự án thực tế :) Hẹn gặp lại!
GO TO FULL VERSION