Mẫu Strategy
Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 95 trên 104.
Mẫu Strategy pattern định nghĩa một nhóm thuật toán, đóng gói từng thuật toán và cho phép thay thế chúng cho nhau. Nhờ đó, bạn có thể thay đổi hành vi của một đối tượng trong lúc chương trình chạy mà không sửa code của đối tượng đó. Thuật toán thay đổi độc lập với các thành phần sử dụng nó.
Mẫu này gồm ba phần: giao diện Strategy khai báo phương thức thuật toán, các chiến lược cụ thể triển khai những biến thể khác nhau và ngữ cảnh sử dụng một chiến lược:
#include <iostream>
#include <memory>
// Giao diện chiến lược
class PaymentStrategy {
public:
virtual void pay(int amount) = 0;
virtual ~PaymentStrategy() = default;
};
// Các chiến lược cụ thể
class CreditCardPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via Credit Card\n";
}
};
class PayPalPayment : public PaymentStrategy {
public:
void pay(int amount) override {
std::cout << "Paid " << amount << " via PayPal\n";
}
};
// Ngữ cảnh
class ShoppingCart {
std::unique_ptr<PaymentStrategy> strategy;
public:
void setPaymentMethod(std::unique_ptr<PaymentStrategy> s) {
strategy = std::move(s);
}
void checkout(int total) {
if (strategy) strategy->pay(total);
}
};
int main() {
ShoppingCart cart;
cart.setPaymentMethod(std::make_unique<CreditCardPayment>());
cart.checkout(100);
cart.setPaymentMethod(std::make_unique<PayPalPayment>());
cart.checkout(50);
}ShoppingCart không biết mình đang dùng phương thức thanh toán nào, nó chỉ gọi pay() trên chiến lược đã được thiết lập. Bạn có thể thay đổi chiến lược trong khi chương trình đang chạy bằng setPaymentMethod(), giúp hệ thống linh hoạt và dễ mở rộng với các lựa chọn thanh toán mới.
Dùng Strategy khi bạn có nhiều thuật toán cho một tác vụ cụ thể và muốn chuyển đổi linh hoạt giữa chúng, hoặc khi muốn tránh dùng câu lệnh điều kiện (conditional statement) để chọn hành vi.
Thử thách
DễHãy xây dựng một trình tính phí vận chuyển sử dụng mẫu Strategy để tính chi phí giao hàng theo các phương thức vận chuyển khác nhau. Đây là tình huống thực tế cần thay đổi thuật toán trong thời gian chạy: cùng một kiện hàng có thể được vận chuyển bằng đường bộ, đường hàng không hoặc chuyển phát nhanh, mỗi phương thức có cách tính giá riêng.
Bạn sẽ sắp xếp code trong ba tệp:
ShippingStrategy.h: Định nghĩa giao diện chiến lược và các chiến lược vận chuyển cụ thể.Tạo lớp trừu tượng
ShippingStrategyvới phương thức ảo thuần túycalculateCost(double weight), trả về chi phí vận chuyển dưới dạng số thực, cùng với hàm hủy ảo.Sau đó, triển khai ba chiến lược cụ thể:
GroundShipping: chi phí1.5cho mỗi đơn vị trọng lượng (weight * 1.5)AirShipping: chi phí4.0cho mỗi đơn vị trọng lượng (weight * 4.0)ExpressShipping: chi phí6.5cho mỗi đơn vị trọng lượng, cộng thêm phí cố định10.0(weight * 6.5 + 10.0)
ShippingService.h: Tạo lớp ngữ cảnh sử dụng một chiến lược vận chuyển.Lớp
ShippingServicecần lưu mộtstd::unique_ptr<ShippingStrategy>làm thành viên riêng tư. Triển khai:- Phương thức
setStrategy(std::unique_ptr<ShippingStrategy> strategy)để thay đổi phương thức vận chuyển - Phương thức
calculateShipping(double weight)sử dụng chiến lược hiện tại để tính và trả về chi phí
Nếu chưa đặt chiến lược khi gọi
calculateShipping, hãy trả về0.0.- Phương thức
main.cpp: Minh họa việc thay đổi chiến lược trong thời gian chạy.Đọc hai đầu vào:
- Trọng lượng kiện hàng (số thực)
- Phương thức vận chuyển:
ground,airhoặcexpress
Tạo một
ShippingServicevà đặt chiến lược phù hợp dựa trên phương thức đầu vào. Tính và in ra chi phí vận chuyển.Sau đó, chuyển sang một chiến lược khác (dùng
airnếu đầu vào không phảiair, nếu không thì dùngground) và tính lại chi phí với cùng trọng lượng. Qua đó, bạn sẽ thấy sức mạnh của việc thay đổi chiến lược trong thời gian chạy.In mỗi chi phí trên một dòng riêng, với chính xác một chữ số thập phân và có tên phương thức ở phía trước:
[Method]: $[cost]
Ví dụ, với đầu vào 5.0 và ground:
Ground: $7.5
Air: $20.0Với đầu vào 3.0 và express:
Express: $29.5
Air: $12.0Với đầu vào 10.0 và air:
Air: $40.0
Ground: $15.0Lưu ý rằng ShippingService không cần biết chi tiết của từng thuật toán tính giá. Lớp này chỉ chuyển việc tính toán cho chiến lược hiện được đặt. Bạn có thể dễ dàng thêm phương thức vận chuyển mới (như giao hàng bằng drone hoặc giao trong ngày) mà không cần sửa đổi lớp dịch vụ.
Tự làm thử
#include <iostream>
#include <string>
#include <iomanip>
#include <memory>
#include "ShippingStrategy.h"
#include "ShippingService.h"
int main() {
double weight;
std::string method;
std::cin >> weight;
std::cin >> method;
// Đặt đầu ra có 1 chữ số thập phân
std::cout << std::fixed << std::setprecision(1);
ShippingService service;
// TODO: Dựa vào phương thức đầu vào ("ground", "air" hoặc "express"):
// 1. Thiết lập chiến lược phù hợp cho service
// 2. Tính và in chi phí theo định dạng: "[Method]: $[cost]"
// TODO: Chuyển sang chiến lược khác:
// Nếu đầu vào là "air", chuyển sang GroundShipping
// Nếu không, chuyển sang AirShipping
// Tính và in chi phí mới
return 0;
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng về OOP
Tệp bên ngoàiBiên dịch và tạo bản dựng C++Tệp header và tệp nguồnNamespace và phạm viGiới thiệu về OOP trong C++Lớp và đối tượngCon trỏ thisPhương thức (hàm thành viên)Thuộc tính (dữ liệu thành viên)Cơ bản về hàm khởi tạo và hàm hủyÔn tập: máy tính đơn giản4Thuộc tính của lớp
Thành viên đối tượng và thành viên tĩnhHàm truy xuất và hàm thiết lậpHàm thành viên hằngTừ khóa mutablePhương thức và biến tĩnhHàm và lớp bạnÔn tập: Trình quản lý tài khoản ngân hàng7Kế thừa
Kế thừa cơ bảnMức truy cập trong kế thừaThứ tự gọi hàm khởi tạo và hàm hủyGhi đè phương thứcHàm ảo và VTableĐa kế thừaKế thừa ảoÔn tập: hệ phân cấp nhân viên10Tổng quan về STL
Tổng quan và triết lý của STLCác vùng chứa của STLBộ lặpCác thuật toán của STLHàm tử và biểu thức lambdaÔn tập: tần suất từ13Mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonFactory và Abstract FactoryMẫu BuilderMẫu ObserverMẫu Strategy2Quản lý bộ nhớ
Bộ nhớ ngăn xếp và heapCon trỏ và tham chiếuBộ nhớ động (new/delete)Con trỏ thông minh trong C++RAII trong C++Ôn tập: Trình quản lý mảng động5Tính đóng gói
Phạm vi truy cập trong C++Tìm hiểu sâu về phạm vi truy cậpẨn thông tinStruct và lớpLớp lồng nhau và lớp bên trongÔn tập: hệ thống hồ sơ sinh viên8Đa hình
Đa hình lúc biên dịch và khi chạyNạp chồng hàmÔn lại hàm ảoHàm ảo thuần túyLớp trừu tượngThiết kế interface trong C++Ép kiểu động và RTTIÔn tập: Máy tính hình học3Hàm khởi tạo và hàm hủy
Hàm khởi tạo mặc địnhHàm khởi tạo có tham sốHàm khởi tạo sao chépHàm khởi tạo di chuyểnDanh sách khởi tạoHàm khởi tạo ủy quyềnTìm hiểu sâu về hàm hủyQuy tắc 3 / 5 / 0Ôn tập: lớp chuỗi6Nạp chồng toán tử
Giới thiệu về nạp chồng toán tửNạp chồng toán tử số họcNạp chồng toán tử so sánhToán tử luồngNạp chồng toán tử gánNạp chồng toán tử [] và ()Toán tử chuyển đổi kiểuÔn tập: lớp ma trận9Mẫu
Mẫu hàmMẫu lớpChuyên biệt hóa mẫuMẫu biến tham sốCơ bản về SFINAE và đặc điểm kiểuÔn tập: vùng chứa tổng quátTự luyện tập: Trình biên dịch C++ online