Xin hướng dẫn áp dụng Strategy pattern

Chào các bạn, mình có đọc bài Design Pattern Strategy của anh Hải thấy khá dễ hiểu và bổ ích, quan trọng hơn là có thể áp dụng cho project đang mở rộng của mình. Tuy nhiên mình gặp một vấn đề khó giải quyết khi thử áp dụng nó, mong các bạn góp ý giúp. Mình sẽ mô tả luôn dựa trên ví dụ trong bài viết:

Theo bài viết về Strategy, ta có class Vehicle là class cơ sở, Các class con là StreetRacer, Helicopter, Jet kế thừa từ Vehicle. Dể tránh sự phụ thuộc của các class con vào class cha khi viết method go(), ta tạo ra interface GoAlgorithm chứa phương thức go() và trong Vehicle sẽ có 1 đối tượng GoAlgorithm và có setter để các class con set Algorithm nào cần thiết, các class “Strategy” là GoByDrivingAlgorithm, GoByFlying, GoByFlyingFast impliment GoAlgorithm. bây giờ đã tách biệt khi các class con chỉ cần set GoAlgorithm tương ứng cần dùng. Ok đến đây mình chưa thấy vấn đề gì cả.

Tuy nhiên khi triển khai, class Jet sẽ dùng đối tượng của GoByFlyingFast để thực hiện method go() tuy nhiên để thực hiện được method go() này trong project của mình, nó lại cần dữ liệu từ chính class Jet. Như vậy class GoByFlyingFast hiện tại đang phụ thuộc vào class Jet, và cần có thông tin nào đó hoặc method nào đó của Jet để hỗ trợ cho method go() mà GoByFlyingFast đang thực hiện.

Để giải quyết cái này mình phải truyền this (object Jet) vào GoByFlyingFast khi khởi tạo:

class Jet extends Vehicle {
    public Jet() {
        setGoAlgorithm(new GoByFlyingFast(this));
    }
}

Tuy nhiên tiếp theo mình phải thực hiện 1 trong 2 cách để GoByFlyingFast có thể dùng được các thuộc tính và phương thức của Jet:

  • C1: Các thuộc tính của của Jet mà GoByFlyingFast dùng sẽ phải có setter, getter, các method hỗ trợ cho go() cũng phải public để GoByFlyingFast dùng. => Không ổn vì phá vỡ class Jet mất rồi.
  • C2: Đưa các thuộc tính hoặc method hỗ trợ cho go() của Jet vào trong GoByFlyingFast => Cái này cũng không ổn vì vừa phá vỡ class Jet lại làm cho Jet bị thừa thãi.
2 Likes

Chào bạn, trước tiên trong bài viết của a Hải có quá nhiều kiến thức, không focus 100% vào strategy nên khiến bạn có chút hiểu lầm.
Về cơ bản, strategy đơn giản hơn 1 chút:
image
Trong bài viết : Vehicle là Strategy, các loại xe là concrete.<phần này không rõ ràng>
Vehicle là Context, GoAlgorithm là Strategy, GoByDrivingAlgorithmGoByFlyingAlgorithm là Concrete.

Quay lại với vấn đề của bạn,

GoByFlyingFast dùng các thuộc tính của Jet.

Bạn có thể tham khảo 1 trong 2 ngu kiến sau:

  1. Bạn có thể sử dụng GoByFlyingFast dưới dạng provider tức là chỉ cung cấp tài nguyên cho hàm go của Vehicle xử lý.
class Jet extends Vehicle {
    public void go() {
        speed = goAlgorithm.getSpeed(); // 120
        type= goAlgorithm.getType(); // fly
        println("I'm " + type + "  in speed = " + speed + " km/h"); 
    }
}
  1. Sử dụng kết hợp với Decorator
4 Likes

nếu phải dùng mỗi class mỗi thuộc tính khác nhau thì phải sử dụng Interface rồi, ko sử dụng Strategy pattern này được nữa.

ví dụ class Shape có 2 loại RectangleShape và CircleShape, có phương thức chung GetArea(). Hình chữ nhật thì cần chiều dài và chiều rộng, hình tròn chỉ cần bán kính, ko thể sử dụng Shape như 1 Context có Strategy là AreaStrategy được mà cứ xài thẳng Interface Shape hay abstract class Shape :V Strategy pattern là để 1 object có thể thay đổi phương thức nó đang xài được, mà ko cần tạo subclass mới chỉ để thay đổi 1 phương thức. Ở đây tính diện tích hình chữ nhật chỉ có 1 phương án duy nhất là dài * rộng, cần gì phải đổi phương án mà đi xài Strategy pattern, đổi phương án tính diện tích thành phương án tính diện tích của hình tròn à? :V Buộc phải xài đa hình thôi :V

tóm lại có lẽ là nếu khác phương thức, nhưng cùng thuộc tính thì xài strategy pattern, tránh xài đa hình, còn khác thuộc tính thì phải xài đa hình.

5 Likes

Cảm ơn bạn, cách này cũng ổn nhưng nó làm dư thừa ở interface và đôi khi các class con lại có các thuộc tính khác. Nên có lẽ lời giải thích “Strategy” không phù hợp ở trường hợp này của bạn @tntxtnt là hợp lý.

Bạn @tntxtnt và mọi người cũng cho mình hỏi thêm, giả sử như bây giờ method go() của mình không phụ thuộc vào thuộc tính hay method khác của Jet nữa tuy nhiên sau khi xử lý go() ở GoByFlyingFast, muốn update thông tin đã xử lý về cho Jet thì nên dùng cách nào? có nên dùng interface callback lại Jet ko?

2 Likes

Bạn tham khảo bài viết này xem có dễ hiểu hơn không: https://gpcoder.com/4796-huong-dan-java-design-pattern-strategy/

4 Likes

Ví dụ như Ứng dụng MD5 Checker của mình có rất nhiều thuật toán băm.
Mỗi thuật toán là một class độc lập với attribute và logic riêng, code bên trong có thể rối như dây điện nhưng đều đóng gói vào 1 interface giống nhau chỉ để lòi ra 2 jack cắm là:

  1. bỏ vào chuỗi hoặc file.
  2. nhận lại chuỗi băm.

Mỗi khi người dùng chọn 1 loại hàm băm thì phải gọi đến 1 class tương ứng, vậy có đến 81 hàm băm thì không lẽ phải if else 81 lần hay switch case?
Lúc này strategy pattern mới có giá trị, như comments của bạn Giang Phan, mình có thể gọi bất kỳ class tương ứng thông qua params, và params đó lấy dữ liệu trực tiếp từ select box luôn.
Giả sử class đó không tồn tại, ta có thể dùng try catch bắt lại, do kết nối này lỏng lẻo, nên dù cập nhật lỗi hoặc xóa đại một class băm nào đó cũng sẽ không ảnh hưởng tới các chức năng và hàm băm khác.

4 Likes

Trước khi mình hỏi cũng đọc qua bài này và một vài bài khác rồi nhưng không giải quyết được vấn đề của mình ở trên. Giờ thì ổn vì mình thấy nó chưa phù hợp cho cái của mình.

@TyE cái của bạn nó không phụ thuộc như của mình, của bạn giống vs ví dụ của tác giả.

1 Like

thÌ trong Jet/Racer/Helicopter base class Vehicle phải cung cấp cho hàm go() 1 biến nào đó có thể thay đổi được (cho tiện), ví dụ chạy hao xăng thì có thể gọi

go(FuelTank tank)

trong đó FuelTank có thể đơn giản chỉ là 1 wrapper class cho số thực :V

class FuelTank {
    private double capacity;
    private double currentFuel;
    public FuelTank(double capacity, double currentFuel) ...
    public bool useFuel(double amount) {
        if (currentFuel  < amount) {
            currentFuel = 0;
            return false;
        }
        currentFuel -= amount;
        return true;
    }
}
...
public void go(FuelTank tank) {
    double fuelAmount = // mỗi strategy mỗi khác, ví dụ bay tốn nhiều xăng hơn
    if (!tank.useFuel(fuelAmount))
        // hết xăng...
    else
        // chạy bình thường...
}

abstract class Vehicle {
    private FuelTank fuelTank;
    ...
    public void go() {
        goStrategy.go(fuelTank);
    }
}

hay xử lý theo logic nào đó :V Có thể trong GoStrategy (abstract class hay Java mới có cho interface có default method gì đó) lo phần hết xăng/còn xăng, có thể FuelTank có thêm phương thức kiểm tra còn xăng hay ko, v.v… :V

nếu ko thì có thể xài tuple trả về các giá trị thay đổi, rồi ngoài việc gọi goAlgorithm.go() có thể thêm mắm muối vào đây để xử lý các giá trị mà go() trả về, mà có lẽ ko ổn lắm vì logic của go() thì ở go strategy nó xử lý chứ class chứa go strategy thì ko cần biết.

ví dụ phương thức thanh toán tiền thì có lẽ hợp lý hơn. Class Giỏ hàng sẽ có phương thức thanh toán với nhiều strategy khác nhau: CreditCard, DebitCard, Paypal, Bitcoin, v.v… Khách mua muốn thanh toán Giỏ hàng bằng phương thức nào thì đổi phương thức đó và thanh toán, ko cần tạo subclass GiỏHàngCreditCard, GiỏHàngBitcoin v.v… :V Logic sẽ được từng loại thanh toán đó xử lý, chỉ cần truyền vào bao nhiêu tiền, hóa đơn name+id, trả cho tài khoản nào, rồi trả về 1 boolean hoàn tất hoặc có vấn đề là được, nếu kĩ hơn thì trả về cái error code :V cho biết bị lỗi gì

5 Likes

Ok mình hình dung ra rồi. Cảm ơn bạn.

1 Like

Em không hiểu chỗ này ạ, anh nói rõ hơn có được không ? tạo class con để thay đổi method là sao nhỉ ?

trong OOP có nguyên tắc SOLID gì đó :V trong đó chữ O là open for extension but close for modification nghĩa là class 1 khi đã viết ra rồi bạn ko nên thay đổi các phương thức của nó nữa :V muốn thay đổi phương thức f() thì bạn phải extend class đó, nghĩa là tạo class con kế thừa class đó rồi thay đổi phương thức đó.

nhưng cũng có thể gặp trường hợp 1 đối tượng có thể cần thay đổi phương thức của nó “on the fly” nghĩa là chính nó tự thay đổi phương thức của nó :V Ví dụ 1 đối tượng Customer c có thể thay đổi phương thức thanh toán từ debit card sang bitcoin, như vậy c cần đổi phương thức thanhToan() của chính nó :V thay đổi toàn bộ phương thức thanhToan() luôn nha chứ ko phải chỉ đổi cái thẻ từ ngân hàng A sang ngân hàng B, vì debit/credit card khác hoàn toàn với bitcoin. Nhưng c đã là đối tượng rồi làm sao extend nó được, nó đâu phải là class. Vì vậy mới có cái gọi là Strategy pattern này :V Khi xài pattern này thì c có thể gọi changePaymentStrategy(bitcoinStrategy) để đổi phương thức thanhToan() từ debit thành bitcoin dễ dàng.

để dễ hình dung cho thêm ví dụ game nữa =] ví dụ CSGO có đổi vũ khí từ súng sang dao chẳng hạn. Trong OOP ta có thể tạo class cha Player, rồi để thay đổi phương thức attack() cho dao và súng của Player ta có thể tạo class con PlayerWithMeleeWeaponPlayerWithRangedWeapon, rồi tạo player = new PlayerWithMeleeWeapon() hoặc player = new PlayerWithRangedWeapon() mỗi khi người chơi đổi vũ khí là dao hoặc súng. Cách này khá vô lý =] vì móc dao/rút súng là ta phải hủy diệt người chơi cũ tạo người chơi mới từ cái xác cũ =] Nếu ta xài “strategy” pattern này thì ta tạo thêm 1 class strategy gọi là Weapon có phương thức attack(). Player sẽ có thêm 13 thuộc tính Weapon meleeWeapon, rangedWeapon, activeWeapon. Ta tạo thêm 2 subclass MeleeWeaponRangedWeapon cho strategy Weapon này. Mỗi khi swap vũ khí ta chỉ cần gán activeWeapon cho melee hoặc ranged là được. Đó gọi là đổi chiến thuật (strategy) cho phương thức attack :V Phương thức attack của Player chỉ cần gọi activeWeapon.attack() là xong, nhẹ nhàng. Và cách này giống y như thực tế vậy :V Có khi bạn xài rồi mà ko biết nó gọi là strategy pattern đó :V

5 Likes

Nôm na là: Bạn có thể ăn mì xào bằng đũa hay nĩa :smiley: thì không cần class bạn-cầm-đũa hay bạn-cầm-nĩa làm gì. Vấn đề là lâu lâu bạn lại thích ăn bằng nĩa cơ.

Phương thức cố định thì cũng tương tự như bạn chỉ có thể ăn mì xào bằng đũa.

4 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?