Tìm hiểu về Strategy pattern.



Bài viết được tham khảo từ cuốn Design pattern for dummies



1. Giới thiệu về design pattern



Design Pattern là một kỹ thuật trong lập trình hướng đối tượng, nó khá quan trọng và mọi lập trình viên muốn giỏi đều phải biết. Được sử dụng thường xuyên trong các ngôn ngữ OOP. Nó sẽ cung cấp cho bạn các "mẫu thiết kế", giải pháp để giải quyết các vấn đề chung, thường gặp trong lập trình. 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.

Trong Design Pattern có 3 nhóm bao gồm:

  • Creational Pattern (nhóm khởi tạo) gồm: Abstract Factory, Factory Method, Singleton, Builder, Prototype. Nó sẽ giúp bạn trong việt khởi tạo đối tượng, như bạn biết để khởi tạo bạn phải sử dụng từ khóa new, nhóm Creational Pattern sẽ sử dụng một số thủ thuật để khởi tạo đối tượng mà bạn sẽ không nhìn thấy từ khóa này.

  • Structural Pattern (nhóm cấu trúc) gồm: Adapter, Bridge, Composite, Decorator, Facade, Proxy và Flyweight.. Nó dùng để thiết lập, định nghĩa quan hệ giữa các đối tượng.

  • Behavioral Pattern gồm: Interpreter, Template Method, Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy và Visitor. Nhóm này dùng trong thực hiện các hành vi của đối tượng.



2. Tìm hiểu strategy pattern



Strategy pattern là gì?
Strategy pattern (mẫu chiến lược): hiểu một cách đơn giản thì đây là mẫu thiết kế giúp bạn trừu tượng hóa những hành vi (behavior, method, function) của một đối tượng bằng cách đưa ra những cài đặt vào những lớp khác nhau.

Bài toán thực tế.
Bạn nhận được một hợp đồng thiết kế ô tô. Có rất nhiều mẫu ô tô để bạn có thể làm. Bạn nghĩ ngay đến việc sử dụng OOP vào trong thiết kế ô tô của mình. Đầu tiên, bạn tạo ra 1 lớp cơ sở có tên là Vehicle với một phương thức có tên là go, phương thức này xuất hiện lên dòng chữ Now I’m driving.

abstract class Vehicle
{
    public function go()
    {
        echo "Now I'm driving";
    }
}

Sau đó, bạn tạo tiếp 1 lớp mới là lớp StreetRacer thừa kế từ lớp Vehicle như sau:

public class StreetRacer extends Vehicle
{

}

Tới đây, chương trình của bạn vẫn hoàn toàn tốt đẹp. Bạn có thể khai báo 1 đối tượng StreetRacer và gọi tới hàm go:

$streetRacer = new StreetRacer;
$streetRacer->go();

Và kết quả trả về là: Now I’m driving. Kết quả hoàn toàn chính xác.
Nhưng sau đó, bạn nhận thêm 1 hợp đồng sản xuất máy bay trực thăng Helicopter. Bạn nhận thấy máy bay trực thăng thì cũng là 1 phương tiện vận chuyển. Vì vậy bạn quyết định tạo ra 1 lớp Helicopter thừa kế từ lớp Vehicle:

public class Helicopter extends Vehicle
{

}

Nhưng bạn chợt nhận ra vấn đề là khi sử dụng hàm go cho Helicopter, thì kết quả trả về có vẻ không chính xác. Now I’m driving? Tại sao máy bay lại là driving? Máy bay thì phải bay chứ nhỉ? Nhưng nó không phải là vấn đề lớn. Bạn quyết định sẽ override hàm go cho lớp Helicopter như sau:

public class StreetRacer extends Vehicle
{
    public function go()
    {
        echo "Now I'm flying";
    }
}

Có vẻ vấn đề đã được giải quyết. Giờ thì máy bay đã là flying rồi. Nhưng vài tuần sau, khách hàng yêu cầu phải chuyển từ Now, I’m flying sang Now, I’m flying 200mph và nhiều sự thay đổi kế tiếp.
Có một vấn đề nảy sinh ở đây. Đó chưa phải là một vấn đề lớn, nhưng nếu bạn phải xử lý các công việc này một cách khá thường xuyên, thì việc cứ phải chỉnh sửa các lớp con như thế này trờ thành 1 vấn đề bảo trì khá nghiêm trọng.

Bạn bắt đầu suy nghĩ. Có lẽ sự thừa kế không phải là cách giải quyết tốt cho tình huống này. Nơi mà bạn cần phải thay đổi các chức năng thường xuyên ở các lớp con. Và khi có càng nhiều lớp kế thừa liên quan, chúng cũng cần được bảo trì khi có sự thay đổi và khi đó, bạn sẽ phải cập nhất phương thức go nhiều lần.

Vấn đề bạn cần phải giải quyết ở đây là làm sao để tránh được việc thay đổi ở các lớp con, nếu không, bạn sẽ phải thay đổi code ở rất nhiều file để cập nhật được yêu cầu của khách hàng.

Có lẽ là bạn cần một cách khác tốt hơn để xử lý vấn đề này thay vì sử dụng thừa kế. :)



Nắm vững sự thay đổi từ “is-a” sang “has-a”

Với sự kế thừa, lớp cơ sở và các lớp con có một mối quan hệ “is-a”. Ví dụ , lớp Helicopter có quan hệ “is-a” với lớp Vehicle, điều này có nghĩa Helicopter thừa kế mọi thứ từ Vehicle, và nếu bạn phải chỉnh sửa các phương thức này, bạn sẽ gặp phải vấn đề bảo trì nó trong tương lai. Lớp cơ sở xử lý phương thức theo một cách, và lớp kế thừa lại thay đổi nó, và lớp kế tiếp lại thay đổi nó thêm một lần nữa. Và cuối cùng bạn có một lô một lốc các biến thể của cùng 1 phương thức qua các lớp con.

Mặc khác, nếu bạn có thể trích những đoạn code dễ thay đổi và đóng gói chúng vào đối tượng, bạn có thể sử dụng các đối tượng này khi cần. Nhiệm vụ mới là xử lý trên các đối tượng này. Bạn đã không để việc xử lý lây lan qua các lớp con. Làm như vậy sẽ cho phép bạn chỉnh sửa code của bạn bằng việc tạo ra “sự kết hợp” composites các đối tượng. Với composites “kết hợp” này, bạn có thể dễ dàng chọn ra và sử dụng đối tượng cần thiết. Một quan hệ “has-a” mới được tạo ra. Một đối tượng StreetRacer sẽ có một “has-a” cách để di chuyển (go), đã được đóng gói vào đối tượng. Một Helicopter sẽ có một cách riêng để di chuyển (go), và cũng được đóng gói vào đối tượng. Từng đối tượng sẽ thực hiện hành động của riêng nó. Một đối tượng, một nhiệm vụ thường là có ý nghĩa hơn là việc kế thừa các lớp, và tạo ra hàng tá các lớp con. Nói cách khác, chúng ta sắp xếp lại dựa trên nhiệm vụ của lớp, chứ không phải trên sự kế thừa.



Cách tạo thuật toán:

Đầu tiên, bạn tạo 1 giao diện interface cho phương thức go như sau:

public interface goAlgorithm
{
    public function go();
}

Trong giao diện goAlgorithm chỉ có duy nhất 1 phương thức là go. Sau đó bạn sẽ tạo các lớp cụ thể cho từng thuật toán. Đầu tiên là lớp GoByDrivingAlgorithm, thực hiện như sau:

class GoByDrivingAlgorithm implements goAlgorithm

{
    public function go()
    {
        echo "Now I'm driving";
    }
}

Ngoài ra, lớp GoByFlyingAlgorithm sẽ định nghĩa cho các phương tiện có thể bay.

class GoByFlyingAlgorithm implements goAlgorithm
{
    public function go()
    {
        echo "Now I'm flying";
    }
}

Và thậm chí bạn còn có thể định nghĩa phương thức go cho cả máy bay phản lực:

class GoByFlyingFastAlgorithm implements goAlgorithm
{
    public function go()
    {
        echo "Now I'm flying very fast";
    }
}

Vậy là bạn đã tách được các phương thức xử lý ra khỏi các lớp cụ thể như StreetRacer hay Helicopter rồi đấy. Bây giờ bạn đã có thể đưa các thuật toán này vào sử dụng được rồi đấy.



Sử dụng thuật toán



Sau khi bạn tạo một đối tượng từ một thuật toán, bạn cần phải lưu trữ đối tượng ở đâu đó. Vì vậy hãy thêm vào lớp cơ sở Vehicle, một phương thức mới SetGoAlgorithm. Phương thức này sẽ quyết định thuật toán mà bạn muốn sử dụng.

abstract class Vehicle
{
    private $goAlgorithm

    public function setGoAlgorithm(GoAlgorithm $__goAlgorithm )
    {
        $goAlgorithm = $__goAlgorithm;
    }
}

Bây giờ khi bạn muốn sử dụng một thuật toán cụ thể nào đó ở lớp kế thừa, tất cả việc cần làm là gọi phương thức setGoAlgorithm với một đối tượng thuật toán đúng.
Phương thức go trong lớp Vehicle có chút thay đổi.

public function go()
    {
        $goAlgorithm->go();
    }

Bây giờ thì tất cả những gì bạn cần làm là chọn đúng thuật toán cho phương tiện. Ví dụ như StreetRacer thì sẽ là GoByDrivingAlgorithm, cho Helicopter thì sẽ là GoByFlyingAlgorithm,...



3. Tổng kết

Vậy là chúng ta vừa tìm hiểu về Strategy pattern (mẫu chiến lược).Ý nghĩa thực sự của mẫu chiến lược là bạn tách rời phần xử lý một chức năng cụ thể ra khỏi đối tượng của bạn. Sau đó tạo ra một tập hợp các thuật toán để xử lý chức năng đó và lựa chọn thuật toán nào mà bạn thấy đúng đắn nhất khi thực thi chương trình. Mẫu thiết kế này thường được sử dụng để thay thế cho sự kế thừa, khi bạn muốn chấm dứt việc theo dõi và chỉnh sửa một chức năng qua nhiều lớp con.

strategy.gif

Strategy pattern cho thấy đôi khi nó sẽ được áp dụng tốt cho mục đích hướng chức năng. Và nó đặc biệt quan trọng khi bạn muốn thực hiện công việc nâng cấp, bảo trì cho các đoạn mã dễ thay đổi của bạn một cách riêng biệt với toàn bộ mã của chương trình, hoặc khi bạn muốn thay đổi thuật toán sử dụng khi chương trình được thực thi.

Bạn nên sử dụng strategy pattern cho những trường hợp sau:

  • Bạn có một đoạn code dễ thay đổi, và bạn tách chúng ra khỏi chương trình chính để dễ dàng bảo trì
  • Bạn muốn tránh sự rắc rối, khi phải hiện thực một chức năng nào đó qua quá nhiều lớp con.
  • Bạn muốn thay đổi thuật toán sử dụng khi chạy chương trình