策略模式
如何避免冗长的if-else、switch分支判断代码?
原理与实现
-
定义一族算法类,将每个算法分别封装起来,让他们可以相互替换。策略模式可以使算法独立于使用它们的客户端(使用算法的代码)。
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use them.
-
工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式也是解耦,他解耦的是策略的定义,创建和使用这三部分。
策略的定义
- 策略的定义包括一个策略接口和一组实现这个接口的策略类。
- 客户端基于接口而非实现编程, 可以灵活替换策略。
public interface Strategy { void algorithmInterface(); } public class ConcreteStrategyA implements Strategy { @Override public void algorithmInterface() { //具体的算法... } } public class ConcreteStrategyB implements Strategy { @Override public void algorithmInterface() { //具体的算法... } }
策略的创建
- 一般通过类型来判断创建哪个策略。
- 这个地方可以将创建逻辑封装->工厂模式。
public class StrategyFactory { private static final Map<String, Strategy> strategies = new HashMap<>(); static { strategies.put("A", new ConcreteStrategyA()); strategies.put("B", new ConcreteStrategyB()); } public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } return strategies.get(type); } }
- 一般来说策略类如果是无状态的,不包含成员变量,只是单纯的算法实现,这样的策略对象是可以被共享的。
- 相反如果是有状态的,可以根据业务的需要,每次调用时都是创建新的对象。
public class StrategyFactory { public static Strategy getStrategy(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } if (type.equals("A")) { return new ConcreteStrategyA(); } else if (type.equals("B")) { return new ConcreteStrategyB(); } return null; } }
策略的使用
- 典型的场景就是运行时动态确定使用哪种策略。
- 动态是指我们在程序运行期间根据配置、用户输入、计算结果等不确定因素来确定使用哪种策略。
// 策略接口:EvictionStrategy // 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy... // 策略工厂:EvictionStrategyFactory public class UserCache { private Map<String, User> cacheData = new HashMap<>(); private EvictionStrategy eviction; public UserCache(EvictionStrategy eviction) { this.eviction = eviction; } //... } // 运行时动态确定,根据配置文件的配置决定使用哪种策略 public class Application { public static void main(String[] args) throws Exception { EvictionStrategy evictionStrategy = null; Properties props = new Properties(); props.load(new FileInputStream("./config.properties")); String type = props.getProperty("eviction_type"); evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type); UserCache userCache = new UserCache(evictionStrategy); //... } } // 非运行时动态确定,在代码中指定使用哪种策略 public class Application { public static void main(String[] args) { //... EvictionStrategy evictionStrategy = new LruEvictionStrategy(); UserCache userCache = new UserCache(evictionStrategy); //... } }
- 如果不是在运行时判断确定策略,这样就没有发挥策略模式的优势,而是利用了基于接口编程。
如何避免分支判断代码?
public class OrderService { public double discount(Order order) { double discount = 0.0; OrderType type = order.getType(); if (type.equals(OrderType.NORMAL)) { // 普通订单 //...省略折扣计算算法代码 } else if (type.equals(OrderType.GROUPON)) { // 团购订单 //...省略折扣计算算法代码 } else if (type.equals(OrderType.PROMOTION)) { // 促销订单 //...省略折扣计算算法代码 } return discount; } }
上面代码可以利用策略模式优化,将不同类型的订单的计算算法和类型通过工厂类封装。
// 策略的定义 public interface DiscountStrategy { double calDiscount(Order order); } // 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码... // 策略的创建 public class DiscountStrategyFactory { private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>(); static { strategies.put(OrderType.NORMAL, new NormalDiscountStrategy()); strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy()); strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy()); } public static DiscountStrategy getDiscountStrategy(OrderType type) { return strategies.get(type); } } // 策略的使用 public class OrderService { public double discount(Order order) { OrderType type = order.getType(); DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type); return discountStrategy.calDiscount(order); } }
重构之后就没有if else了。本质上是借助了工厂类中的map, 通过数据结构本身的表的查询替换了我们手动的判断分支查询。
如果业务场景需要每次创建不同的策略对象,我们就需要另外一种工厂类的实现:
public class DiscountStrategyFactory { public static DiscountStrategy getDiscountStrategy(OrderType type) { if (type == null) { throw new IllegalArgumentException("Type should not be null."); } if (type.equals(OrderType.NORMAL)) { return new NormalDiscountStrategy(); } else if (type.equals(OrderType.GROUPON)) { return new GrouponDiscountStrategy(); } else if (type.equals(OrderType.PROMOTION)) { return new PromotionDiscountStrategy(); } return null; } }
这里只是把分支挪进了工厂类。并没有真正移除。
结合例子:实现给不同文件大小文件排序的小程序
问题和解决思路
- 写一个程序实现对一个文件中内容进行排序的功能。文件中只包含整型数,通过逗号相连。
- 简单思路:确定文件大小,
- 如果相对较小,读取内容不需要特别多时间,可以简单的使用内存解决。将数字读取到内存并且使用排序算法排序。
- 如果相对较大,内存有限没办法一次性加载。就可能需要一些外部排序算法。
- 如果文件更大,可以利用多核的优势,在外排序的基础上优化加入多线程并发排序。
- 如果文件超大,单机多线程也很慢的时候,引入多机器,使用例如MapReduce框架解决。
代码实现和分析
public class Sorter { private static final long GB = 1000 * 1000 * 1000; public void sortFile(String filePath) { // 省略校验逻辑 File file = new File(filePath); long fileSize = file.length(); if (fileSize < 6 * GB) { // [0, 6GB) quickSort(filePath); } else if (fileSize < 10 * GB) { // [6GB, 10GB) externalSort(filePath); } else if (fileSize < 100 * GB) { // [10GB, 100GB) concurrentExternalSort(filePath); } else { // [100GB, ~) mapreduceSort(filePath); } } private void quickSort(String filePath) { // 快速排序 } private void externalSort(String filePath) { // 外部排序 } private void concurrentExternalSort(String filePath) { // 多线程外部排序 } private void mapreduceSort(String filePath) { // 利用MapReduce多机排序 } } public class SortingTool { public static void main(String[] args) { Sorter sorter = new Sorter(); sorter.sortFile(args[0]); } }
每种排序算法实现逻辑都比较复杂实际上,代码也比较多。这样可读性和可维护性都有点差。所有的排序算法设计成Sorter的私有函数也会影响代码的可复用性。
优化的重构
两个点:第一个点是排序算法可以被拆分出来一个新的类,易于维护和复用;另外一个就是前文说的,多个分支可以通过基于工厂类创建的策略模式来优化,让排序策略的定义、创建和使用解耦。
public interface ISortAlg { void sort(String filePath); } public class QuickSort implements ISortAlg { @Override public void sort(String filePath) { //... } } public class ExternalSort implements ISortAlg { @Override public void sort(String filePath) { //... } } public class ConcurrentExternalSort implements ISortAlg { @Override public void sort(String filePath) { //... } } public class MapReduceSort implements ISortAlg { @Override public void sort(String filePath) { //... } } public class Sorter { private static final long GB = 1000 * 1000 * 1000; public void sortFile(String filePath) { // 省略校验逻辑 File file = new File(filePath); long fileSize = file.length(); ISortAlg sortAlg; if (fileSize < 6 * GB) { // [0, 6GB) sortAlg = new QuickSort(); } else if (fileSize < 10 * GB) { // [6GB, 10GB) sortAlg = new ExternalSort(); } else if (fileSize < 100 * GB) { // [10GB, 100GB) sortAlg = new ConcurrentExternalSort(); } else { // [100GB, ~) sortAlg = new MapReduceSort(); } sortAlg.sort(filePath); } }
然后Sorter中的sortFile可以进一步使用策略模式优化。因为他们是无状态的排序算法,可以对每个算法策略类只创建一个对象重复使用。
public class SortAlgFactory { private static final Map<String, ISortAlg> algs = new HashMap<>(); static { algs.put("QuickSort", new QuickSort()); algs.put("ExternalSort", new ExternalSort()); algs.put("ConcurrentExternalSort", new ConcurrentExternalSort()); algs.put("MapReduceSort", new MapReduceSort()); } public static ISortAlg getSortAlg(String type) { if (type == null || type.isEmpty()) { throw new IllegalArgumentException("type should not be empty."); } return algs.get(type); } } public class Sorter { private static final long GB = 1000 * 1000 * 1000; public void sortFile(String filePath) { // 省略校验逻辑 File file = new File(filePath); long fileSize = file.length(); ISortAlg sortAlg; if (fileSize < 6 * GB) { // [0, 6GB) sortAlg = SortAlgFactory.getSortAlg("QuickSort"); } else if (fileSize < 10 * GB) { // [6GB, 10GB) sortAlg = SortAlgFactory.getSortAlg("ExternalSort"); } else if (fileSize < 100 * GB) { // [10GB, 100GB) sortAlg = SortAlgFactory.getSortAlg("ConcurrentExternalSort"); } else { // [100GB, ~) sortAlg = SortAlgFactory.getSortAlg("MapReduceSort"); } sortAlg.sort(filePath); } }
最后如果精益求精,sortFile中的if判断也可以去掉,通用可以借助策略模式中根据type表查找的思想,优化:
public class Sorter { private static final long GB = 1000 * 1000 * 1000; private static final List<AlgRange> algs = new ArrayList<>(); static { algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort"))); algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort"))); algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort"))); algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort"))); } public void sortFile(String filePath) { // 省略校验逻辑 File file = new File(filePath); long fileSize = file.length(); ISortAlg sortAlg = null; for (AlgRange algRange : algs) { if (algRange.inRange(fileSize)) { sortAlg = algRange.getAlg(); break; } } sortAlg.sort(filePath); } private static class AlgRange { private long start; private long end; private ISortAlg alg; public AlgRange(long start, long end, ISortAlg alg) { this.start = start; this.end = end; this.alg = alg; } public ISortAlg getAlg() { return alg; } public boolean inRange(long size) { return size >= start && size < end; } } }
但是这里还有问题就是每次新加代码都要回去修改策略工厂类,违反OCP开闭原则。
对于 Java 语言来说,我们可以通过反射来避免对策略工厂类的修改。具体是这么做的:我们通过一个配置文件或者自定义的 annotation 来标注都有哪些策略类;策略工厂类读取配置文件或者搜索被 annotation 标注的策略类,然后通过反射动态地加载这些策略类、创建策略对象;当我们新添加一个策略的时候,只需要将这个新添加的策略类添加到配置文件或者用 annotation 标注即可。还记得上一节课的课堂讨论题吗?我们也可以用这种方法来解决。
对于Sorter,通过将文件大小区间和算法之间的对应关系放到配置文件中。当添加新的排序算法时,我们只需要改动配置文件即可,不需要改动代码。