Design Pattern - Strategy Pattern

Lời mở đầu

Xin chào các bạn, hẳn là trong số các lập trình viên, bất cứ ai cũng đã ít nhất một lần nghe qua về Design Pattern. "Design Pattern là cái gì vậy?". Các bạn có thể dễ dàng "gu-gờ sợt" và tìm được khá nhiều bài viết về Design Pattern, cả tiếng Anh lẫn tiếng Việt.
"Design Pattern là các giải pháp đã được tối ưu hóa, được tái sử dụng cho các vấn đề lập trình mà chúng ta gặp phải hàng ngày. Nó là một khuôn mẫu đã được suy nghĩ, giải quyết trong tình huống cụ thể rồi.
Các vấn đề mà bạn gặp phải có thể bạn sẽ tự nghĩ ra cách giải quyết nhưng có thể nó chưa phải là tối ưu. Design Pattern giúp bạn giải quyết vấn đề một cách tối ưu nhất, cung cấp cho bạn các giải pháp trong lập trình OOP.
Nó không phải là ngôn ngữ cụ thể nào cả. Design Pattern có thể thực hiện được ở phần lớn các ngôn ngữ lập trình.
"
Và hôm nay, mình xin giới thiệu với các bạn về một Design Pattern: Strategy Pattern.

Đặt vấn đề

Quinncy làm việc cho một công ty game, sản phẩm của họ có một game nổi tiếng đó là “The adventure into duck land”. Trong game có mô phỏng rất nhiều loại vịt khác nhau. Ban đầu, thiết kế của hệ thống vịt như sau:
alt

Sau một vài lần thảo luận, team thiết kế nhận thấy phải cho thêm lũ vịt biết bay vào để chuẩn bị cho phần game sắp ra. Quinncy nghĩ: "Chỉ cần thêm một phương thức fly() vào lớp Duck và sau đó tất cả lũ vịt lại kế thừa nó -> so easy". Lúc này, biểu đồ UML của lũ vịt sẽ là:
alt
Tuy nhiên, khi Quinncy thiết kế như thế này, anh ta gặp phải một số vấn đề như sau:

  • Mỗi một lớp vịt kế thừa từ lớp Duck đều phải override lại phương thức "fly" -> "Don't repeat your self".
  • Không phải tất cả lớp con của lớp Duck sẽ đều có thể "fly": vịt gỗ - DecoyDuck, vịt cao su - RubberDuck đâu thể bay được.
  • RubberDuck - vịt cao su sẽ không kêu quác quác mà "beep beep"
  • Nếu giờ có thêm lớp vịt gỗ - DecoyDuck, nó không thể "fly" hay “quack” được. Nếu theo thiết kế như trên thì khi tạo ra lớp DecoyDuck, chúng ta sẽ phải override phương thức fly()quack() của lớp Duck và để hai phương thức đó thực hiện "không thực hiện gì cả".
    Có cách nào khác giải quyết tình huống này nữa không???
    Sau một hồi suy nghĩ, Quinncy nghĩ ra được một cách rất “xịn” như sau:
    alt
    Các bạn nghĩ sao về thiết kế này???

Giải quyết bài toán

1. Xác định vấn đề

Nếu bạn đã là một lập trình viên thì bạn sẽ biết rằng thứ duy nhất không thay đổi trong việc phát triển phần mềm là “sự thay đổi”. Vậy làm sao để giải quyết được những vấn đề mà luôn thay đổi như vậy??? Câu trả lời là: Tách rời chúng ra khỏi những thành phần còn lại.
Đó cũng chính là một nguyên lý thiết kế.

Identify the aspects of your application that vary and separate them from what stays the same

“Xác định những khía cạnh trong ứng dụng mà có khả năng thay đổi và tách chúng khỏi phần còn lại.”

Có thể hiểu như là tách những phần mà có khả năng thay đổi và đóng gói chúng lại, để bạn có thể dễ dàng thay thế và mở rộng phần đó mà không làm ảnh hưởng đến những phần còn lại.
Vây, chúng ta sẽ bắt đầu như thế nào đây???
Quay lại bài toán trên, dễ dàng nhận thấy thứ thay đổi ở đây là hành vi “fly” và “quack” của từng loại vịt. Để tách rời những hành vi này ra khỏi Duck class, chúng ta sẽ tạo ra một tập các class mà đại diện cho mỗi hành vi “fly”, “quack”. Sau khi đã xác định được vấn đề là tách được những thứ thay đổi rồi, ta sẽ thiết kế lớp cho các hành vi “fly” và “quack” như sau: alt
Với cách thiết kế này, những kiểu đối tượng khác có thể tái sử dụng hành vi “fly” và “quack” của chúng ta bởi vì những hành vi này không còn dính vào Duck class nữa.
Hơn nữa, chúng ta có thể thêm những hành vi khác mà không phải thay đổi bất cứ class nào đã tồn tại hoặc phải chỉnh sửa sourcecode.
Tiếp theo, chúng ta cần tích hợp phần thay đổi vào thiết kế của chúng ta.

  1. Tạo ra 2 biến instance variables cho lớp Duck gọi là flyBehaviorquackBehavior. Bỏ 2 phương thức fly() và quack() vì chúng ta đã tách nó ra và đưa vào FlyBehavior và QuackBehavior. Thay thế phương thức fly() và quack() bằng 2 phương thức khác gọi là: performFly() và performQuack(). alt
  2. Implements performQuack() (tương tự với performFly())
public class Duck {
    QuackBehavior quackBehavior;
    
    public void performQuack() {
        quackBehavior.quack();
    }
}
  1. Làm sao để instance variable flyBehavior và quackBehavior được khởi tạo??? Chúng ta làm như sau:
public class MallardDuck extends Duck {
    public MallardDuck() {
        quackBehavior = new Quack();
        flyBehavior = new FlyWithWings();
    }
    // other methods
}
  1. Một vấn đề nữa, làm sao chúng ta có thể “set” hành vi cho lũ vịt một cách “dynamically”??? Chúng ta chỉ cần thêm hai phương thức sau đến lớp Duck.
public void setFlyBehavior (FlyBehavior fb) {
    flyBehavior = fb;
}

public void setQuackBehavior (QuackBehavior qb) {
    quackBehavior = qb;
}

Biểu đồ UML sau khi phân tích và giải quyết vấn đề. alt

2. "Play with duck"

Giờ chúng ta sẽ viết code minh họa cho vấn đề trên.

  1. Xây dựng Duck class
public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    public abstract void display();
    
    public void setFlyBehavior (FlyBehavior fb) {
        flyBehavior = fb;
    }
    public void setQuackBehavior (QuackBehavior qb) {
        quackBehavior = qb;
    }
    public void performFly() {
        flyBehavior.fly();
    }
    public void performQuack() {
        quackBehavior.quack();
    }
    public void swim() {
        System.out.println("All ducks float, even decoys!");
    }
   }
  1. Xây dựng FlyBehavior interface và các class liên quan
public interface FlyBehavior {
    public void fly();
}

public class FlyNoWay implements FlyBehavior {
    public void fly() {
        System.out.println("I can't fly"):
    }
}

public class FlyRocketPowered implements FlyBehavior {
    public void fly() {
        System.out.println("I'm flying with a rocket!");
    }
}
  1. Xây dựng QuackBehavior interface và các class liên quan
public interface QuackBehavior {
    public void quack();
}

public class Quack implements QuackBehavior {
    public void quack() {
        System.out.println("Quack");
    }
}

public class MuteQuack implements QuackBehavior {
    public void quack() {
        System.out.println("<< Silence >>");
    }
}

public class Squeak implements QuackBehavior {
    public void quack() {
        System.out.println("Squeak");
    }
}
  1. Chạy thử chương trình.
public static MiniDuckSimulator {
    public static void main(String[] args) {
        Duck mallard = new MallardDuck();
        mallard.performQuack();
        mallard.performFly();
    }
}

Trong trường hợp bạn muốn “set behavior dynamically”.

  1. Thêm hai phương thức sau tới Duck class.
public void setFlyBehavior (FlyBehavior fb) {
    flyBehavior = fb;
}
public void setQuackBehavior (QuackBehavior qb) {
    quackBehavior = qb;
}
  1. Tạo ra một class mới, giả sử như sau.
public class ModelDuck extends Duck {
    public ModelDuck() {
        flyBehavior = new FlyNoWay();
        quackBehavior = new Quack():
    }
    
    public void display() {
        System.out.println("I'm a model duck");
    }
}
  1. Tạo ra một hành vi “fly” mới.
public class FlyRocketPowered implements FlyBehavior {
    public void fly() {
        System.out.println("I'm flying with a rocket!");
    }
}
  1. Chạy thử nào.
public class MiniDuckSimulator {
    public static void main(String[] args) {
        Duck model = new ModelDuck();
        model.performFly();
        model.setFlyBehavior(new FlyRocketPowered());
        model.performFly();
    }
}

Tổng kết

Trong quá trình giải quyết tình huống trên, chúng ta đã sử dụng một Design Pattern gọi là Strategy Pattern. Biểu đồ UML cho pattern này như sau.
alt
Tài liệu tham khảo:
Head First Design Pattern
http://blog.duyet.net/2015/02/oop-design-patterns-la-gi.html#.Wozil-eYPDc
Các bạn có câu hỏi, thắc mắc, hoặc muốn thảo luận về vấn đề nào thì có thể comment ở dưới.
Cảm ơn các bạn đã đọc bài.

All Rights Reserved