天天看点

策略模式解锁:动态切换算法的高效编程技巧

在软件开发中,如何应对不断变化的业务需求和技术挑战?策略模式或许是一种解决方案。本文将介绍策略模式的概念和应用场景,以及如何在实际项目中应用策略模式来提高代码的可维护性、可扩展性和可测试性。如果您也关心如何更好地应对变化的业务需求,那么请继续阅读下去。”
策略模式解锁:动态切换算法的高效编程技巧

为什么要使用策略模式

策略模式的背景来源于面向对象设计的SOLID原则,特别是开放/封闭原则(OCP)。开放/封闭原则指出,软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。换句话说,当需要添加新功能时,应该通过添加新代码来扩展现有实体的行为,而不是通过修改现有代码来改变其行为。

SOLID原则

S(单一职责原则):一个类应该只有一个单一的功能,并且该功能应该由这个类完全封装起来。 O(开放/封闭原则):一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。 L(里氏替换原则):子类应该可以替换它们的父类并且仍然保持原有的行为。 I(接口隔离原则):客户端不应该依赖它不需要的接口。 D(依赖反转原则):高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

在实际应用中,经常会遇到需要在程序运行时根据不同情况选择不同的算法或处理方式的需求。如果直接在代码中硬编码不同的算法或处理方式,就会破坏开放/封闭原则,使得代码难以扩展和维护。

策略模式就是为了解决这个问题而诞生的。策略模式将不同的算法或处理方式封装成独立的策略对象,客户端可以根据不同的需求选择不同的策略对象来完成相应的操作。这样可以避免直接在代码中硬编码不同的算法或处理方式,使得程序更加灵活、可扩展和易于维护。

什么是策略模式

官方文档:

策略模式定义了一系列算法,将每个算法都封装起来,使得它们可以互相替换。策略模式让算法的变化独立于使用算法的客户端。

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

官方文档强调策略模式的两个重要特点:一是将算法封装成独立的策略对象,使得客户端可以根据不同的需求动态地选择相应的算法;二是将算法的变化独立于客户端,使得算法的修改不会影响到客户端的代码。这两个特点使得策略模式具有高度的灵活性、可扩展性和易于维护性,同时还可以提高代码的复用性和可测试性。

策略模式的组成成分:

  1. 策略(Strategy):定义所有支持的算法的公共接口。
  2. 具体策略(Concrete Strategy):实现具体的算法,并且提供了算法的实现细节。
  3. 环境(Context):用一个具体策略对象来配置,并维护对策略对象的引用。它需要知道所有的具体策略,并根据需要选择合适的算法。

举一个生活中的例子:

以交通出行方式的选择为例,假设现在有一个需要选择交通工具的场景,客户端需要根据不同的出行需求选择不同的交通方式,如到公司要赶时间,选择出租车;周末出去玩,选择自行车等。这里,选择交通工具的过程就可以看做是策略模式中的算法选择。

在策略模式中,算法的选择过程通常是由客户端来完成的。客户端可以根据不同的需求来选择合适的算法,同时客户端不需要关心具体算法的实现细节。这里的客户端就可以是需要选择交通方式的人。

代码实现

策略(Strategy): 公共接口

java复制代码public interface TravelStrategy {
//    旅行方式
    public void travel();
}
           

具体策略(Concrete Strategy):实现具体的算法,并且提供了算法的实现细节

java复制代码//公交出行
public class BusStrategy implements TravelStrategy{
    @Override
    public void travel() {
        System.out.println("乘坐公交车旅行");
    }
}
           
java复制代码//自行车出行
public class BikeStrategy implements TravelStrategy{
    @Override
    public void travel() {
        System.out.println("骑自行车旅行");
    }
}
           
java复制代码//步行
public class WalkStrategy implements TravelStrategy{
    @Override
    public void travel() {
        System.out.println("步行旅行");
    }
}
           

环境(Context):用一个具体策略对象来配置,并维护对策略对象的引用

java复制代码public class Transportation {
    //维护的策略
    private TravelStrategy travelStrategy;

    public Transportation(TravelStrategy travelStrategy) {
        this.travelStrategy = travelStrategy;
    }

    public void travel() {
        travelStrategy.travel();
    }

    public void setTravelStrategy(TravelStrategy travelStrategy) {
        this.travelStrategy = travelStrategy;
    }
}
           

Client : 决定使用哪个策略

java复制代码public class Client {
    public static void main(String[] args) {
        Transportation transportation = new Transportation(new BikeStrategy()); // 选择骑自行车旅行
        transportation.travel(); // 骑自行车旅行
        transportation.setTravelStrategy(new WalkStrategy()); // 选择步行旅行
        transportation.travel(); // 步行旅行
    }
}
           

在策略模式中,通常是将不同的算法实现封装到不同的策略类中,客户端根据需求选择合适的策略类来完成相应的功能。如果需要根据不同的条件选择不同的算法实现,可以通过工厂模式或者配置文件等方式来动态创建相应的策略类,从而避免使用if-else语句。

java复制代码class Transportation {
    private TravelStrategy strategy;
    //使用map存储
    private Map<String, TravelStrategy> strategies = new HashMap<>();
    
    public Transportation() {
        // 初始化策略映射
        strategies.put("walk", new WalkStrategy());
        strategies.put("bus", new BusStrategy());
        strategies.put("bike", new BikeStrategy());
    }
    
    public void setStrategy(String key) {
        this.strategy = strategies.get(key);
    }
    
    public void travel() {
        strategy.travel();
    }
}
           

Client

java复制代码public class Client {
    public static void main(String[] args) {
        Transportation trans = new Transportation();
        
        // 根据不同的条件选择出行方式
        trans.setStrategy("bus");
        trans.travel();
        
        trans.setStrategy("walk");
        trans.travel();
        
        trans.setStrategy("bike");
        trans.travel();
    }
}
           

Java源码中的体现

  • Java中的集合框架中的排序(Comparator)接口就是一个典型的策略模式的应用。用户可以通过传入不同的比较器对象,实现不同的排序策略。
java复制代码// 定义一个Person类
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
           

比较器

java复制代码// 定义一个按姓名排序的比较器
public class NameComparator implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return o1.getName().compareTo(o2.getName());
    }
}

// 定义一个按年龄排序的比较器
public class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person o1, Person o2) {
        return o1.getAge() - o2.getAge();
    }
}
           

Client

java复制代码// 使用示例
public class Main {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 25));
        people.add(new Person("Bob", 20));
        people.add(new Person("Charlie", 30));

        // 使用按姓名排序的比较器进行排序
        Collections.sort(people, new NameComparator());
        for (Person p : people) {
            System.out.println(p.getName() + " " + p.getAge());
        }

        // 使用按年龄排序的比较器进行排序
        Collections.sort(people, new AgeComparator());
        for (Person p : people) {
            System.out.println(p.getName() + " " + p.getAge());
        }
    }
}
           
Comparator是策略(Strategy) NameComparator和AgeComparator是具体策略(Concrete Strategy) Collections类是充当了环境类的角色

在Collections.sort()方法中,我们需要传入一个List对象和一个Comparator对象作为参数,其中Comparator对象就是具体策略对象,它的具体实现决定了排序的策略。List对象则是需要排序的数据集合。在排序时,Collections类根据传入的Comparator对象的具体实现来进行排序,并将排序结果应用到原有的List对象中,完成排序的过程。

Spring中的体现

  1. Resource接口: Spring的Resource接口是一个典型的策略模式。Resource接口定义了资源访问的统一抽象,针对不同来源的资源,Spring提供了如ClassPathResource、FileSystemResource、UrlResource等不同的实现。
  2. Bean生命周期管理: Spring框架中,Bean的初始化和销毁过程有多种策略可选,如:InitializingBean和DisposableBean接口、@PostConstruct和@PreDestroy注解、以及通过XML或Java配置指定init-method和destroy-method。
  3. Spring AOP: 在Spring的AOP(面向切面编程)中,使用策略模式来定义切点(Pointcut)和通知(Advice),以及不同的代理创建策略(如JDK动态代理和CGLIB代理)。
  4. Spring事务管理: Spring事务管理器(PlatformTransactionManager)接口定义了事务管理的策略,针对不同的数据访问技术,Spring提供了如JdbcTransactionManager、HibernateTransactionManager、JpaTransactionManager等不同实现。
  5. Spring MVC中的处理器映射(HandlerMapping)和处理器适配器(HandlerAdapter): 处理器映射和处理器适配器都采用策略模式。例如,Spring MVC提供了基于注解的RequestMappingHandlerMapping和基于XML配置的BeanNameUrlHandlerMapping等不同映射策略;处理器适配器方面,如RequestMappingHandlerAdapter和SimpleControllerHandlerAdapter等。
  6. Spring中的缓存抽象: Spring提供了Cache接口和CacheManager接口来实现缓存策略的抽象,针对不同的缓存技术,Spring提供了如EhCache、Redis等不同实现。
抽象策略类 具体策略类 环境类 解释
Resource ClassPathResource, FileSystemResource, UrlResource ApplicationContext Spring中的资源访问统一抽象。不同的实现允许访问不同来源的资源,例如类路径资源、文件系统资源、URL资源等。
- InitializingBean, DisposableBean, @PostConstruct, @PreDestroy, init-method, destroy-method BeanFactory, ApplicationContext Spring框架中,Bean的生命周期管理提供多种初始化和销毁策略,使得开发者可以根据需求选择适当的策略来管理Bean的创建和销毁。
Pointcut, Advice RegexpMethodPointcut, AspectJExpressionPointcut, BeforeAdvice, AfterAdvice ProxyFactoryBean, AspectJ Spring AOP中,通过切点和通知策略定义横切关注点。切点确定应用通知的位置,通知定义在切点处执行的操作。这使得代码更加模块化和易于维护。
PlatformTransactionManager DataSourceTransactionManager, HibernateTransactionManager, JpaTransactionManager TransactionTemplate Spring事务管理策略。为不同的数据访问技术提供适当的事务管理器实现,如JDBC、Hibernate、JPA等。这使得事务管理与数据访问技术解耦。
HandlerMapping, HandlerAdapter RequestMappingHandlerMapping, BeanNameUrlHandlerMapping, RequestMappingHandlerAdapter, SimpleControllerHandlerAdapter DispatcherServlet Spring MVC中,处理器映射和处理器适配器策略用于处理HTTP请求。不同的映射策略将请求映射到相应的处理器,适配器负责执行请求处理逻辑。这使得请求处理更加灵活和可扩展。
Cache, CacheManager ConcurrentMapCache, EhCacheCacheManager, RedisCacheManager CacheInterceptor Spring中的缓存抽象策略。为不同的缓存技术提供实现,如ConcurrentMap、EhCache、Redis等。这使得缓存策略与缓存技术解耦,便于更换和扩展。

策略模式的优缺点

优点:

  1. 提高代码的复用性和可维护性:策略模式将算法封装在独立的策略类中,使得它们可以在不影响客户端的情况下进行修改和扩展。这有助于降低维护成本和提高代码的复用性。
  2. 简化客户端:策略模式将复杂的算法逻辑封装在策略类中,使得客户端代码变得简洁,客户端只需引用策略类即可使用对应的算法,无需关心算法的具体实现。
  3. 遵循开闭原则:策略模式遵循了开闭原则,即软件实体应该对扩展开放,对修改关闭。在策略模式中,可以通过添加新的策略类来扩展算法,而不需要修改现有代码。
  4. 提高测试性:由于策略类通常是独立的可替换的组件,这使得对这些组件的测试变得更加容易。每个策略类可以单独进行测试,而不会影响其他策略类的测试。

缺点:

  1. 增加类的数量:策略模式为每个算法都提供了一个单独的策略类,这可能导致系统中类的数量增加,增加了系统的复杂性。
  2. 客户端需要了解策略类:虽然策略模式简化了客户端代码,但客户端仍需要了解各个策略类以便正确使用它们。这可能会增加客户端代码的复杂性,尤其是在策略类较多时。
  3. 增加运行时开销:由于策略模式通过委托的方式调用策略类,这可能会导致运行时开销的增加。不过,这种开销通常可以接受,因为策略模式带来的好处远大于这点额外的开销。

如何权衡策略模式的优缺点

在使用策略模式时,我们需要权衡其优缺点,根据实际项目需求和场景来决定是否采用策略模式。以下是一些建议,可以帮助您在实际开发中做出决策:

  1. 分析项目的需求和场景:首先,需要分析项目的需求和场景,了解是否存在多个互相替换的算法或策略。如果存在这种情况,策略模式可能是一个合适的选择。
  2. 评估算法的复杂性:考虑算法的复杂性以及它们之间的关系。如果算法之间有很多共享的逻辑,策略模式可能不是最佳选择,因为它会导致很多重复代码。在这种情况下,可以考虑其他设计模式,如模板方法模式。
  3. 项目的可扩展性需求:如果项目中的算法需要频繁地扩展或修改,策略模式可以帮助遵循开闭原则,使得扩展变得更加容易。
  4. 考虑运行时性能开销:虽然策略模式可能会带来一定的运行时性能开销,但通常这种开销是可以接受的。如果性能是关键因素,可以在实现策略模式时采用一些优化措施,例如使用享元模式来减少策略对象的创建。
  5. 评估客户端的复杂性:在使用策略模式时,需要权衡客户端代码的复杂性。如果策略类较多,客户端可能需要了解这些策略类以便正确使用它们。在这种情况下,可以考虑使用简单工厂模式或抽象工厂模式来降低客户端对策略类的依赖。

策略模式的应用场景

  1. 多个算法互相替换:当一个问题可以用多种算法或策略来解决时,可以考虑使用策略模式。将不同的算法封装在独立的策略类中,使它们可以相互替换。
  2. 算法的使用者与实现者需要解耦:如果希望将算法的使用者与算法的具体实现进行解耦,可以使用策略模式。这样,算法的使用者只需要关注策略接口,而不需要知道具体的实现。
  3. 需要根据运行时条件选择不同算法:当在运行时需要根据不同的条件来选择不同的算法时,可以使用策略模式。通过将算法封装在策略类中,可以在运行时根据条件动态地选择合适的策略。
  4. 算法需要方便地扩展和修改:如果希望能够方便地扩展和修改算法,可以考虑使用策略模式。通过将算法封装在独立的策略类中,可以在不影响客户端代码的情况下对算法进行扩展和修改。
  5. 遵循开闭原则:当希望遵循开闭原则来设计一个软件系统时,可以考虑使用策略模式。策略模式可以让算法对扩展开放,对修改关闭,这符合开闭原则的要求。
原文链接:https://juejin.cn/post/7222626198672605240

继续阅读