Mô hình Chiến lược có thể được thực hiện mà không cần phân nhánh đáng kể?


14

Mẫu Chiến lược hoạt động tốt để tránh rất lớn nếu ... khác xây dựng và giúp dễ dàng thêm hoặc thay thế chức năng. Tuy nhiên, nó vẫn để lại một lỗ hổng trong quan điểm của tôi. Có vẻ như trong mọi triển khai vẫn cần phải có một cấu trúc phân nhánh. Nó có thể là một nhà máy hoặc một tập tin dữ liệu. Như một ví dụ lấy một hệ thống đặt hàng.

Nhà máy:

// All of these classes implement OrderStrategy
switch (orderType) {
case NEW_ORDER: return new NewOrder();
case CANCELLATION: return new Cancellation();
case RETURN: return new Return();
}

Mã sau này không cần phải lo lắng và hiện tại chỉ có một nơi để thêm loại đơn đặt hàng mới, nhưng phần mã này vẫn không thể mở rộng. Kéo nó ra thành một tệp dữ liệu giúp phần nào dễ đọc (gây tranh cãi, tôi biết):

<strategies>
   <order type="NEW_ORDER">com.company.NewOrder</order>
   <order type="CANCELLATION">com.company.Cancellation</order>
   <order type="RETURN">com.company.Return</order>
</strategies>

Nhưng điều này vẫn bổ sung mã soạn sẵn để xử lý tệp dữ liệu - được cấp, mã dễ kiểm tra đơn vị hơn và tương đối ổn định, nhưng không phức tạp thêm.

Ngoài ra, loại cấu trúc này không kiểm tra tích hợp tốt. Mỗi chiến lược riêng lẻ có thể dễ dàng hơn để kiểm tra ngay bây giờ, nhưng mọi chiến lược mới mà bạn thêm là độ phức tạp bổ sung để kiểm tra. Nó ít hơn bạn sẽ có nếu bạn đã không sử dụng mô hình, nhưng nó vẫn ở đó.

Có cách nào để thực hiện mô hình chiến lược giảm thiểu sự phức tạp này không? Hoặc điều này chỉ đơn giản như nó được, và cố gắng đi xa hơn sẽ chỉ thêm một lớp trừu tượng cho ít để không có lợi?


Hmmmmm .... có thể đơn giản hóa mọi thứ với những thứ như eval... có thể không hoạt động trong Java nhưng có thể bằng các ngôn ngữ khác?
Thất vọngWithFormsDesigner

1
@FrustratedWithFormsDesigner là từ thần kỳ trong java
ratchet freak

2
Bạn vẫn sẽ cần những điều kiện đó ở đâu đó. Bằng cách đẩy họ đến một nhà máy, bạn chỉ cần tuân thủ DRY, vì nếu không thì câu lệnh if hoặc switch có thể đã xuất hiện ở nhiều nơi.
Daniel B

1
Lỗ hổng mà bạn đang đề cập thường được gọi là vi phạm quy tắc đóng mở
k3b

câu trả lời được chấp nhận trong câu hỏi liên quan cho thấy từ điển / bản đồ thay thế cho if-other và switch
gnat

Câu trả lời:


15

Dĩ nhiên là không. Ngay cả khi bạn sử dụng bộ chứa IoC, bạn sẽ phải có điều kiện ở đâu đó, quyết định thực hiện cụ thể để thực hiện. Đây là bản chất của mẫu Chiến lược.

Tôi không thực sự thấy lý do tại sao mọi người nghĩ rằng đây là một vấn đề. Có một số tuyên bố trong một số cuốn sách, như Tái cấu trúc của Fowler , rằng nếu bạn thấy một công tắc / trường hợp hoặc một chuỗi if / elses ở giữa mã khác, bạn nên xem xét mùi đó và tìm cách chuyển nó sang phương thức riêng của mình. Nếu mã trong mỗi trường hợp nhiều hơn một dòng, có thể là hai, thì bạn nên xem xét biến phương thức đó thành Phương thức xuất xưởng, trả về Chiến lược.

Một số người đã coi điều này có nghĩa là một công tắc / trường hợp là xấu. Đây không phải là trường hợp. Nhưng nó nên tự trú, nếu có thể.


1
Tôi cũng không coi đó là một điều xấu. Tuy nhiên, tôi luôn tìm cách để giảm lượng mã duy trì mà nó sẽ chạm vào, điều này đã đặt ra câu hỏi.
Michael K

Các câu lệnh chuyển đổi (và các khối if / if-if dài) là xấu và nên tránh nếu có thể để giữ cho mã của bạn có thể duy trì được. Điều đó nói rằng "nếu có thể" thừa nhận rằng có một số trường hợp công tắc phải tồn tại, và trong những trường hợp đó, hãy cố gắng giữ nó ở một nơi duy nhất và một trường hợp giúp duy trì công việc ít hơn (rất dễ để vô tình bỏ lỡ 1 trong 5 vị trí trong mã mà bạn cần giữ đồng bộ khi bạn không cách ly nó đúng cách).
Shadow Man

10

Mô hình Chiến lược có thể được thực hiện mà không cần phân nhánh đáng kể?

, bằng cách sử dụng hashmap / từ điển nơi mọi đăng ký thực hiện Chiến lược tại. Phương pháp nhà máy sẽ trở thành một cái gì đó như

Class strategyType = allStrategies[orderType];
return runtime.create(strategyType);

Mọi thực hiện chiến lược phải đăng ký một nhà máy với orderType và một số thông tin về cách tạo một lớp.

factory.register(NEW_ORDER, NewOrder.class);

Bạn có thể sử dụng hàm tạo tĩnh để đăng ký, nếu ngôn ngữ của bạn hỗ trợ điều này.

Phương thức đăng ký không làm gì khác hơn là thêm một giá trị mới vào hàm băm:

void register(OrderType orderType, Class class)
{
   allStrategies[orderType] = class;
}

[update 2012-05-04]
Giải pháp này phức tạp hơn nhiều so với "giải pháp chuyển đổi" ban đầu mà tôi thích nhất là hầu hết thời gian.

Tuy nhiên, trong một môi trường mà chiến lược thay đổi thường xuyên (tức là tính giá tùy thuộc vào khách hàng, thời gian, ....), giải pháp hashmap này kết hợp với IoC-Container có thể là giải pháp tốt.


3
Vậy bây giờ bạn đã có toàn bộ Hashmap of Strategies, để tránh chuyển đổi / trường hợp? Làm thế nào chính xác là các hàng factory.register(NEW_ORDER, NewOrder.class);sạch hơn, hoặc ít vi phạm OCP hơn so với các hàng case NEW_ORDER: return new NewOrder();?
pdr

5
Nó không sạch hơn chút nào. Nó là phản trực giác. Nó hy sinh nguyên tắc giữ-nó-kích thích-ngu ngốc để cải thiện đóng mở. Điều tương tự cũng áp dụng cho đảo ngược kiểm soát và tiêm phụ thuộc: những điều này khó hiểu hơn một giải pháp đơn giản. Câu hỏi là "thực hiện ... mà không phân nhánh đáng kể" và không phải "làm thế nào để tạo ra một giải pháp trực quan hơn"
k3b

1
Tôi không thấy rằng bạn đã tránh phân nhánh. Bạn vừa thay đổi cú pháp.
pdr

1
Không có vấn đề gì với giải pháp này trong các ngôn ngữ không tải các lớp cho đến khi chúng cần thiết? Mã tĩnh sẽ không chạy cho đến khi lớp được tải và lớp sẽ không bao giờ được tải vì không có lớp nào khác đề cập đến nó.
kevin cline

2
Cá nhân tôi thích phương pháp này, vì nó cho phép bạn coi các chiến lược của mình là dữ liệu có thể thay đổi , thay vì coi chúng là mã bất biến.
Tacroy

2

"Chiến lược" là về việc phải lựa chọn giữa các thuật toán thay thế ít nhất một lần chứ không phải ít hơn. Ở đâu đó trong chương trình của bạn, ai đó phải đưa ra quyết định - có thể là người dùng hoặc chương trình của bạn. Nếu bạn sử dụng IoC, sự phản chiếu, bộ đánh giá dữ liệu hoặc cấu trúc chuyển đổi / trường hợp để thực hiện điều này sẽ không thay đổi tình hình.


Tôi không đồng ý với tuyên bố của bạn một lần. Các chiến lược có thể được hoán đổi khi chạy. Ví dụ: sau khi không xử lý yêu cầu bằng cách sử dụng một ProcessStrargety tiêu chuẩn, VerboseProcessingStrargety có thể được chọn và chạy lại xử lý.
dmux

@dmux: chắc chắn, đã chỉnh sửa câu trả lời của tôi cho phù hợp.
Doc Brown

1

Mẫu chiến lược được sử dụng khi bạn chỉ định các hành vi có thể và được sử dụng tốt nhất khi chỉ định một trình xử lý khi khởi động. Chỉ định trường hợp nào của chiến lược sẽ sử dụng có thể được thực hiện thông qua Người hòa giải, bộ chứa IoC tiêu chuẩn của bạn, một số Nhà máy như bạn mô tả hoặc đơn giản bằng cách sử dụng đúng chiến lược dựa trên ngữ cảnh (vì thông thường, chiến lược được cung cấp như một phần của việc sử dụng rộng rãi hơn của lớp có chứa nó).

Đối với loại hành vi này trong đó các phương thức khác nhau cần được gọi dựa trên dữ liệu, thì tôi khuyên bạn nên sử dụng các cấu trúc đa hình được cung cấp bởi ngôn ngữ trong tay; không phải là mô hình chiến lược.


1

Khi nói chuyện với các nhà phát triển khác nơi tôi làm việc, tôi đã tìm thấy một giải pháp thú vị khác - cụ thể cho Java, nhưng tôi chắc chắn ý tưởng này hoạt động với các ngôn ngữ khác. Xây dựng một enum (hoặc bản đồ, nó không quá quan trọng) bằng cách sử dụng các tham chiếu lớp và khởi tạo bằng phản xạ.

enum FactoryType {
   Type1(Type1.class),
   Type2(Type2.class);

   private Class<? extends Type> clazz;

   private FactoryType(Class<? extends Type> clazz) {
      this.clazz = clazz;
   }

   public Class<? extends Type> getTypeClass() {
      return clazz;
   }
}

Điều này làm giảm đáng kể mã nhà máy:

public Type create(FactoryType type) throws Exception {
   return type.getTypeClass().newInstance();
}

(Vui lòng bỏ qua việc xử lý lỗi kém - mã ví dụ :))

Nó không phải là linh hoạt nhất , vì một bản dựng vẫn được yêu cầu ... nhưng nó làm giảm sự thay đổi mã thành một dòng. Tôi thích rằng dữ liệu được tách ra khỏi mã nhà máy. Bạn thậm chí có thể làm những việc như cài đặt tham số bằng cách sử dụng các lớp / giao diện trừu tượng để cung cấp một phương thức chung hoặc tạo các chú thích thời gian biên dịch để buộc các chữ ký của hàm tạo cụ thể.


Không chắc chắn điều gì đã khiến điều này quay trở lại trang chủ nhưng đây là một câu trả lời hay và trong Java 8 bây giờ bạn có thể tạo các tham chiếu của Trình xây dựng mà không cần phản ánh.
JimmyJames

1

Khả năng mở rộng có thể được cải thiện bằng cách mỗi lớp xác định loại đơn đặt hàng riêng. Sau đó, nhà máy của bạn chọn một trong những phù hợp.

Ví dụ:

public interface IOrderStrategy
{
    OrderType OrderType { get; }
}

public class NewOrder : IOrderStrategy
{
    public OrderType OrderType { get; } = OrderType.NewOrder;
}

public class OrderFactory
{
    private IEnumerable<IOrderStrategy> _strategies;

    public OrderFactory(IEnumerable<IOrderStrategy> strategies) // Injected by IoC container
    {
        _strategies = strategies;
    }

    public IOrderStrategy Create(OrderType orderType)
    {
        IOrderStrategy strategy = _strategies.FirstOrDefault(s => s.OrderType == orderType);

        if (strategy == null)
            throw new ArgumentException("Invalid order type.", nameof(orderType));

        return strategy;
    }
}

1

Tôi nghĩ nên có một giới hạn về số lượng chi nhánh được chấp nhận.

Ví dụ: nếu tôi có nhiều hơn tám trường hợp bên trong câu lệnh chuyển đổi của mình, thì tôi sẽ đánh giá lại mã của mình và tìm kiếm những gì tôi có thể tính lại. Tôi thường thấy rằng một số trường hợp nhất định có thể được nhóm vào một nhà máy riêng biệt. Giả định của tôi ở đây là có một nhà máy xây dựng chiến lược.

Dù bằng cách nào, bạn không thể tránh điều này và ở đâu đó bạn sẽ phải kiểm tra trạng thái hoặc loại đối tượng bạn đang làm việc. Sau đó, bạn sẽ xây dựng một chiến lược cho điều đó. Tốt cũ tách mối quan tâm.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.