策略模式的现实世界例子

111
我一直在阅读有关OCP原则及如何使用策略模式实现此目标的内容。
我想向一些人解释,但我唯一能想到的例子是根据“订单”状态使用不同的验证类。
我已经阅读了一些在线文章,但这些文章通常没有描述使用策略的真正原因,例如生成报告/账单/验证等...
您认为有哪些现实世界的例子中策略模式很常见?
19个回答

114

这是怎么样的:

您需要加密一个文件。

对于小文件,您可以使用“内存中”策略,其中完整文件被读取并保存在内存中(假设文件大小小于1 GB)

对于大文件,您可以使用另一种策略,在内存中读取文件的部分并将部分加密结果存储在临时文件中。

这些可能是同一个任务的两种不同策略。

客户端代码看起来相同:

 File file = getFile();
 Cipher c = CipherFactory.getCipher( file.size() );
 c.performAction();



// implementations:
interface  Cipher  {
     public void performAction();
}

class InMemoryCipherStrategy implements Cipher { 
         public void performAction() {
             // load in byte[] ....
         }
}

class SwaptToDiskCipher implements Cipher { 
         public void performAction() {
             // swapt partial results to file.
         }

}
     Cipher c = CipherFactory.getCipher( file.size() );

将为密码返回正确的策略实例。

(我甚至不知道Cipher是否是正确的单词:P)


13
您的示例更像是一个工厂模式,而不是其他模式。另外,我认为它在C#中无法运行。您的“getCipher()”方法是一个静态方法,但在C#中您不能在接口中定义静态方法(我认为在Java中也是如此,但这一点我不确定)。 - FrenchData
14
它们是相互关联的。工厂创建了策略,但策略本身包含算法以执行(基本)相同的操作。该策略也可在运行时更改。关于工厂方法,您是正确的,我已改正它。 - OscarRyz
1
补充Osacars的观点,如果没有工厂,可以创建此对象而无需使用工厂。Cipher C = null; if (file.size() <= 2048) { C = new InMemoryCipherStrategy(); } else { c = SwaptToDiskCipher(); } - Abhijit Mazumder
2
同意@FrenchData的观点。虽然CipherFactory是一个很好的示例,但可能会让不熟悉策略模式的人感到困惑。 - user487772
1
工厂模式是关于创建的,策略模式是关于行为的。它们有一些微小的不同,对吧? - nhoxbypass

78

虽然这是一个老帖子,但仍然会出现在搜索结果中,因此我将添加两个更多的示例(代码使用C#编写)。 我非常喜欢策略模式,因为它在项目经理说:“我们想要应用程序执行'X',但'X'尚不清楚,而且可能会在不久的将来发生变化”时,已经多次拯救了我的后路。

这个视频解释策略模式,以星际争霸为例。

属于此类别的东西:

  • 排序:我们想要对这些数字进行排序,但不知道是否会使用BrickSort、BubbleSort或其他排序方法。

  • 验证:我们需要根据“某个规则”检查项目,但该规则还不清楚,而且我们可能会想到新的规则。

  • 游戏:我们希望玩家在移动时可以步行或奔跑,但也许在未来,他还应该能够游泳、飞行、传送、掘地等。

  • 存储信息:我们希望应用程序将信息存储到数据库中,但以后可能需要能够保存文件或进行Web调用。

  • 输出:我们需要将X作为纯字符串输出,但以后可能是CSV、XML、JSON等。


示例

我有一个项目,用户可以将产品分配给数据库中的人员。 分配产品给人员具有“批准”或“拒绝”的状态,这取决于某些业务规则。 例如:如果用户将产品分配给某个年龄段的人,则其状态应该被拒绝; 如果项目中两个字段之间的差异大于50,则其状态为被拒绝等等。

现在,在开发时,这些业务规则并不都完全清楚,并且随时可能出现新规则。策略模式的威力在于,我制作了一个RuleAgent,它被给予一个IRules列表。

public interface IRule {
    bool IsApproved(Assignment assignment); 
 }

在分配产品给某个人的那一刻,我会创建一个RuleAgent,给它提供一系列规则(这些规则都实现了IRule接口),并要求它验证分配情况。它将遍历所有规则,因为它们都实现了相同的接口,都有IsApproved方法,如果其中任何一个返回false,则返回false。

现在,例如经理突然出现并说,我们还需要拒绝所有针对实习生或所有加班工作人员的分配情况...则您可以创建新的类似以下方式的类:

public OvertimeRule : IRule
{
    public bool IsApproved(Assignment assignment) //Interface method
    {
        if (assignment.Person.Timesheet >= 40)
        {
            return false;
        }
        return true;
    }
}

public InternRule : IRule
{
    public bool IsApproved(Assignment assignment) //Interface method
    {
        if (assignment.Person.Title == "Intern")
        {
            return false;
        }
        return true;
    }
}

你可以看到,不需要不断添加或删除if语句或代码,只需要创建一个实现IRule接口的新规则类,并在需要时切换即可。


另一个很好的例子是Scott Allen在http://www.asp.net/mvc/pluralsight上的视频系列,在应用程序的Unit-test部分使用了策略模式。

他构建了一个网站,其中有个页面根据流行度显示物品。然而,“流行度”可能有很多含义(最多观看次数、最多订阅者、创建日期、最活跃等等),并且如果管理人员还不确定如何排序,可能会在以后尝试不同的排序方式。你可以创建一个接口(IOrderAlgorithm或类似的名称)和一个Orderer对象,将排序委托给IOrderAlgorithm接口的具体实现。你可以创建一个“CommentOrderer”、“ActivityOrderer”等等......在有新需求时只需切换这些对象。


1
我知道这有点超出问题的范围,但接下来该怎么做呢?我们现在有了InternRule,但是我们如何触发OvertimeRule?我们如何确保现在调用OvertimeRule.IsApproved的任何逻辑也会调用InternRule.IsApproved - Spencer Ruport
@SpencerRuport 我假设它会遍历上下文类中的每个策略(具体的IRule),因此如果所有策略都返回 true,则该任务分配将被批准。这可以尽可能复杂,例如为某种类型的任务分配应用特定规则(而不是全部规则)。 - Gerardo Cauich
但这仍然会违反“开闭原则”,我的意思是你必须将新创建的类添加到验证规则的位置,只是一种将某些代码片段抽象到另一个文件中的花哨方式。 - AMunim

25

关键要点:

策略是一种行为设计模式。它用于在算法族之间进行切换。

一个真实的例子是:航空公司在某些月份(7月至12月)提供折扣。您可以拥有一个票价模块,根据月份确定定价选项。

看一个简单的例子。这个例子可以扩展到在线零售应用程序,轻松地在特殊日子/欢乐时光为购物车商品提供折扣。

import java.util.*;

/* Interface for Strategy */
interface OfferStrategy {
    public String getName();
    public double getDiscountPercentage();
}

/* Concrete implementation of base Strategy */
class NoDiscountStrategy implements OfferStrategy {
    public String getName() { 
        return this.getClass().getName();
    }
    public double getDiscountPercentage() {
        return 0;
    }
}

/* Concrete implementation of base Strategy */
class QuarterDiscountStrategy implements OfferStrategy {
    public String getName() {
        return this.getClass().getName();
    }
    public double getDiscountPercentage() {
        return 0.25;
    }
}

/* Context , which is optional can be single interface for client. */
class StrategyContext {

    double price; // price for some item or air ticket etc.
    Map<String, OfferStrategy> strategyContext = new HashMap<String, OfferStrategy>();

    StrategyContext(double price) {
        this.price = price;
        strategyContext.put(NoDiscountStrategy.class.getName(), new NoDiscountStrategy());
        strategyContext.put(QuarterDiscountStrategy.class.getName(), new QuarterDiscountStrategy());
    }

    public void applyStrategy(OfferStrategy strategy) {
        /*
        Currently applyStrategy has simple implementation. You can use Context for populating some more information,
        which is required to call a particular operation
        */
        System.out.println("Price before offer :" + price);
        double finalPrice = price - (price*strategy.getDiscountPercentage());
        System.out.println("Price after offer:" + finalPrice);
    }

    public OfferStrategy getStrategy(int monthNo) {
        /*
            In absence of this Context method, client has to import relevant concrete Strategies everywhere.
            Context acts as single point of contact for the Client to get relevant Strategy
        */
        if (monthNo < 6)  {
            return strategyContext.get(NoDiscountStrategy.class.getName());
        }
        else {
            return strategyContext.get(QuarterDiscountStrategy.class.getName());
        }
    }
}

public class StrategyDemo {
    public static void main(String args[]) {
        StrategyContext context = new StrategyContext(100);
        System.out.println("Enter month number between 1 and 12");
        int month = Integer.parseInt(args[0]);
        System.out.println("Month =" + month);
        OfferStrategy strategy = context.getStrategy(month);
        context.applyStrategy(strategy);
    }
}

输出:

Enter month number between 1 and 12
Month =1
Price before offer :100.0
Price after offer:100.0

Enter month number between 1 and 12
Month =7
Price before offer :100.0
Price after offer:75.0

谢谢,非常清晰易懂。同意 applyStrategy() 是一个实现许多黑魔法的怪兽。此外,在 strategyContext 中保持策略的非静态方式是一流的。 - Arnab Dutta
1
为什么我们不能将价格作为输入传递给applyStrategy?否则,我们需要为每个价格创建一个新对象?为什么会这样?只需设置价格并执行策略即可。 - rahul sharma

12

策略模式的一种常见用途是定义自定义排序策略(在没有高阶函数的语言中),例如在Java中按长度对字符串列表进行排序,通过传递一个匿名内部类(实现策略接口):

List<String> names = Arrays.asList("Anne", "Joe", "Harry");
Collections.sort(names, new Comparator<String>() {
  public int compare(String o1, String o2) {
    return o1.length() - o2.length();
  }
});
Assert.assertEquals(Arrays.asList("Joe", "Anne", "Harry"), names);

同样地,可以使用策略来处理对象数据库中的本地查询,例如在db4o中:

List<Document> set = db.query(new Predicate<Document>() {
  public boolean match(Document candidate) {
    return candidate.getSource().contains(source);
  }
});

12

我可以想到几个相对简单的例子:

  • 对列表进行排序。策略是用于决定列表中哪个项目是“第一个”的比较。
  • 您可能拥有一个应用程序,其中排序算法本身(QuickSort、HeapSort等)可以在运行时选择。
  • Log4NetLog4j中的Appenders、Layouts和Filters。
  • UI工具包中的布局管理器。
  • 数据压缩。您可能有一个ICompressor接口,其唯一方法看起来像这样:

    byte[] compress(byte[] input);

    您的具体压缩类可能是RunLengthCompression、DeflateCompression等。


11
我有一个应用程序,每天会将它的用户基础与我们的企业目录进行同步。根据用户在大学中的状态,他们是否具有资格决定他们是否有资格使用该应用程序。每天,预配程序会确保那些应该有资格的人在应用程序中得到授权,而那些没有资格的人则被去授权(实际上是根据一个优雅的退化算法处理的,但这不是重点)。周六我会进行更彻底的更新,同步每个用户的一些属性,并确保他们具备适当的资格。在月底,我还要根据当月的使用情况进行一些账单处理。
我使用可组合的策略模式来完成这种同步。主程序基本上根据一周中的日期(仅同步更改/全部同步)和学年历史相对于学术日历的时间选择主策略。如果结算周期接近结束,它还会与结算策略进行组合。然后,通过标准接口运行所选的策略。
我不知道这种做法有多常见,但我觉得它非常适合策略模式。

这是一个非常好的例子。它清楚地告诉你命令模式和策略模式之间的区别 - 意图。主程序基本上根据一周中的日期选择主策略。 - Utsav T

9
策略模式的一个很好的例子可以在游戏中找到,我们可以有不同的角色,每个角色可以使用多种武器进行攻击,但一次只能使用一种武器。因此,我们将角色设为上下文,例如国王、指挥官、骑士和士兵,而武器则是策略,在这里attack()可能是方法/算法,它取决于正在使用的武器。因此,如果具体的武器类是Sword、Axe、Crossbow、BowAndArrow等,则它们都将实现attack()方法。我相信进一步的解释是不必要的。

1
我认为被接受的答案应该讨论这个例子 :) - Jeancarlo Fontalvo

8
我知道这是一个老问题,但我认为我最近实现了另一个有趣的例子。这是策略模式在文档传递系统中使用的非常实用的例子。我有一个PDF传递系统,它接收包含许多文档和一些元数据的存档。根据元数据,它决定将文档放在哪里;例如,根据数据,我可以将文档存储在A、B或C存储系统中,或者三个系统的混合体。不同的客户使用此系统,并且他们在出现错误时具有不同的回滚/错误处理要求:其中一个希望交付系统在第一个错误时停止,保留已经交付到其存储位置的所有文档,但停止进程并不再交付任何东西;另一个希望在将文档存储在C中出现错误时从B回滚,但保留已经交付到A的任何内容。很容易想象第三个或第四个人也将有不同的需求。
为了解决这个问题,我创建了一个基本的交付类,其中包含交付逻辑以及回滚所有存储的方法。在出现错误时,交付系统不会直接调用这些方法。相反,该类使用依赖注入来接收一个“回滚/错误处理策略”类(根据客户使用系统的情况),如果适用于该策略,则在出现错误时调用该类,该策略再调用回滚方法。
交付类本身向策略类报告正在发生的情况(将文档传递给哪些存储,以及发生了什么故障),并且每当出现错误时,它会询问策略是否继续。如果策略说“停止”,则该类调用策略的“cleanUp”方法,该方法使用先前报告的信息来决定从交付类中调用哪些回滚方法,或者什么也不做。
rollbackStrategy.reportSuccessA(...);
rollbackStrategy.reportFailureB(...);

if (rollbackStrategy.mustAbort()) {
    rollbackStrategy.rollback(); // rollback whatever is needed based on reports
    return false;
}

所以现在我有两种不同的策略:一种是QuitterStrategy(遇到第一个错误就退出,不清理任何东西),另一种是MaximizeDeliveryToAStrategy(尽可能地不中止进程并且从不回滚存储器A中交付的内容,但如果交付给C失败,则回滚B中的内容)。
据我所知,这是策略模式的一个例子。如果你(是的,你读者)认为我错了,请在下面评论让我知道。我很好奇什么构成了“纯”的策略模式,我的实现违反了哪些定义。我认为它看起来有点奇怪,因为策略接口有点臃肿。到目前为止,我看到的所有示例都只使用了一个方法,但我仍然认为这封装了一个算法(如果业务逻辑可以被视为算法,我认为它确实是)。
由于策略还会在交付执行期间收到事件通知,因此它也可以被视为Observer,但那是另外一个故事。
从一些研究来看,这似乎是一种“组合模式”(像MVC一样,在特定方式下使用多个设计模式的模式)称为Advisor。它是关于是否应该继续传递的顾问,但它也是主动的错误处理程序,因为当被要求时它可以回滚一些东西。
无论如何,这是一个相当复杂的例子,可能会让你感觉到策略模式的用法都太简单/愚蠢了。当与其他模式一起使用时,它可以变得非常复杂甚至更加实用。

5

策略模式是最常用的模式,特别适用于验证和排序算法。

让我用一个简单的实际例子来解释一下。

enum Speed {
  SLOW, MEDIUM, FAST;
}

class Sorter {
 public void sort(int[] input, Speed speed) {
    SortStrategy strategy = null;
    switch (speed) {
    case SLOW:
        strategy = new SlowBubbleSortStrategy();
        break;
    case MEDIUM:
        strategy = new MediumInsertationSortStrategy();
        break;

    case FAST:
        strategy = new FastQuickSortStrategy();
        break;
    default:
        strategy = new MediumInsertationSortStrategy();
    }
    strategy.sort(input);
 }

}

interface SortStrategy {

    public void sort(int[] input);
}

class SlowBubbleSortStrategy implements SortStrategy {

   public void sort(int[] input) {
    for (int i = 0; i < input.length; i++) {
        for (int j = i + 1; j < input.length; j++) {
            if (input[i] > input[j]) {
                int tmp = input[i];
                input[i] = input[j];
                input[j] = tmp;
            }
        }
    }
    System.out.println("Slow sorting is done and the result is :");
    for (int i : input) {
        System.out.print(i + ",");
    }
  }

 }

class MediumInsertationSortStrategy implements SortStrategy {

public void sort(int[] input) {
    for (int i = 0; i < input.length - 1; i++) {
        int k = i + 1;
        int nxtVal = input[k];
        while (input[k - 1] > nxtVal) {
            input[k] = input[k - 1];
            k--;
            if (k == 0)
                break;
        }
        input[k] = nxtVal;
    }
    System.out.println("Medium sorting is done and the result is :");
    for (int i : input) {
        System.out.print(i + ",");
    }

 }

}

class FastQuickSortStrategy implements SortStrategy {

public void sort(int[] input) {
    sort(input, 0, input.length-1);
    System.out.println("Fast sorting is done and the result is :");
    for (int i : input) {
        System.out.print(i + ",");
    }
}

private void sort(int[] input, int startIndx, int endIndx) {
    int endIndexOrig = endIndx;
    int startIndexOrig = startIndx;
    if( startIndx >= endIndx)
        return;
    int pavitVal = input[endIndx];
    while (startIndx <= endIndx) {
        while (input[startIndx] < pavitVal)
            startIndx++;
        while (input[endIndx] > pavitVal)
            endIndx--;
        if( startIndx <= endIndx){
            int tmp = input[startIndx];
            input[startIndx] = input[endIndx];
            input[endIndx] = tmp;
            startIndx++;
            endIndx--;
        }
    }
    sort(input, startIndexOrig, endIndx);
    sort(input, startIndx, endIndexOrig);
 }

}  

这里是对应的测试代码:

public class StrategyPattern {
  public static void main(String[] args) {
    Sorter sorter = new Sorter();
    int[] input = new int[] {7,1,23,22,22,11,0,21,1,2,334,45,6,11,2};
    System.out.print("Input is : ");
    for (int i : input) {
        System.out.print(i + ",");
    }
    System.out.println();
    sorter.sort(input, Speed.SLOW);
 }

}

这个例子来源于http://coder2design.com/strategy-pattern/

该网站介绍了策略模式的应用,它是一种常见的软件设计模式,主要用于在运行时根据不同的情况选择不同的算法或行为。通过使用策略模式,我们可以避免大量的if-else语句,并使代码更加清晰和易于维护。

策略模式的不同用途:验证:当代码需要进行大量的验证时。 不同算法:特别是当可以使用不同的排序算法时,例如冒泡排序或快速排序。
存储信息:当我们可能在不同的位置储存信息,例如数据库或文件系统。
解析:在解析时,我们可以针对不同的输入使用不同的策略。
过滤策略。
布局策略。
- Jatinder Pal

4
public class StrategyDemo {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        Item item1 = new Item("1234", 10);
        Item item2 = new Item("5678", 40);

        cart.addItem(item1);
        cart.addItem(item2);

        // pay by paypal
        cart.pay(new PaypalStrategy("myemail@example.com", "mypwd"));

        // pay by credit card
        cart.pay(new CreditCardStrategy("Pankaj Kumar", "1234567890123456", "786", "12/15"));
    }
}

interface PaymentStrategy {
    public void pay(int amount);
}

class CreditCardStrategy implements PaymentStrategy {

    private String name;
    private String cardNumber;
    private String cvv;
    private String dateOfExpiry;

    public CreditCardStrategy(String nm, String ccNum, String cvv, String expiryDate) {
        this.name = nm;
        this.cardNumber = ccNum;
        this.cvv = cvv;
        this.dateOfExpiry = expiryDate;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid with credit/debit card");
    }

}

class PaypalStrategy implements PaymentStrategy {

    private String emailId;
    private String password;

    public PaypalStrategy(String email, String pwd) {
        this.emailId = email;
        this.password = pwd;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid using Paypal.");
    }

}

class Item {

    private String upcCode;
    private int price;

    public Item(String upc, int cost) {
        this.upcCode = upc;
        this.price = cost;
    }

    public String getUpcCode() {
        return upcCode;
    }

    public int getPrice() {
        return price;
    }

}

class ShoppingCart {

    // List of items
    List<Item> items;

    public ShoppingCart() {
        this.items = new ArrayList<Item>();
    }

    public void addItem(Item item) {
        this.items.add(item);
    }

    public void removeItem(Item item) {
        this.items.remove(item);
    }

    public int calculateTotal() {
        int sum = 0;
        for (Item item : items) {
            sum += item.getPrice();
        }
        return sum;
    }

    public void pay(PaymentStrategy paymentMethod) {
        int amount = calculateTotal();
        paymentMethod.pay(amount);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接