天天看點

政策模式解鎖:動态切換算法的高效程式設計技巧

作者:程式猿阿嘴
在軟體開發中,如何應對不斷變化的業務需求和技術挑戰?政策模式或許是一種解決方案。本文将介紹政策模式的概念和應用場景,以及如何在實際項目中應用政策模式來提高代碼的可維護性、可擴充性和可測試性。如果您也關心如何更好地應對變化的業務需求,那麼請繼續閱讀下去。”
政策模式解鎖:動态切換算法的高效程式設計技巧

為什麼要使用政策模式

政策模式的背景來源于面向對象設計的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

繼續閱讀