設計模式如何提升 vivo 營銷自動化業務擴展性 | 引擎篇01
在《vivo 營銷自動化技術解密 |開篇》中,我們從整體上介紹了vivo營銷自動化平台的業務架構、核心業務模組功能、系統架構和幾大核心技術設計。
本次帶來的是系列文章的第2篇,本文詳細解析設計模式和相關應用如何幫助營銷自動化業務提升系統擴展性,以及實踐過程中的思考和總結。
一、引言
營銷業務本身極具複雜多變性,特別是伴隨著數字化營銷蓬勃發展的趨勢,在市場的不同時期、公司發展的不同階段、面向不同的用戶群體以及持續效果波動迭代,都會產生不同的營銷策略決策。
當面對隨時變化的業務場景時,系統的擴展性就顯得非常重要。而在談到系統設計擴展性的時候,總是首先會想到設計原則和設計模式。但設計模式不是銀彈,並不能解決所有問題,它只是前人提煉總結出來的招式方法,需要開發者根據實際業務場景進行合理的選擇、合適的變通,才能真正去解決實際場景中的問題,並總結形成自己的方法論。
那麼接下來我們看看設計模式是如何幫助我們在營銷策略引擎中提升系統擴展性的。
二、營銷策略引擎
先簡單介紹一下營銷策略引擎:策略引擎是通過搭建可視化流程組件,定義各個流程節點,自動化執行活動業務流程,從而提供不同運營活動能力。其中核心活動業務流程主要包括三大部分:運營活動配置->運營活動審批->運營活動執行。
-
運營活動配置:運營人員在系統後台配置運營活動。包括活動名稱、活動時間、觸發條件、活動用戶和具體推送渠道(如簡訊、微信、push推送等)。
-
運營活動審批:品質/主管人員審批運營活動配置。審批流程涉及了活動審批節點和人員的配置,審批相關的回調操作配置。
-
運營活動執行:系統自動化執行運營活動的過程。即具體的渠道如簡訊、微信、push等推送活動的任務執行下發流程,包括用戶數據準備,數據下發推送和數據效果回收等。
三、設計模式具體應用
3.1 運營活動配置
3.1.1 工廠模式
具體場景
一般情況下,根據不同的用戶和活動場景,運營藉助數據分析會決策出不同的活動策略,比如需要創建簡訊推送策略、微信圖文推送策略、App Push推送策略等。此時我們可以使用工廠模式,統一管理具體推送策略的創建。
模式分析
在GoF《設計模式:可復用面向對象軟體的基礎》中:工廠模式被分成了工廠方法和抽象工廠兩類,而簡單工廠模式(又稱靜態工廠模式)被看作是工廠方法的一種特例。不過由於簡單工廠和工廠方法相對更簡單和易於理解,程式碼可讀性也更強,因此在實際項目中更加常用。
其中簡單工廠的適用場景:
-
a.工廠類負責創建的對象比較少,工廠方法中的創建邏輯簡單。
-
b.客戶端無須關心創建具體對象的細節,僅需知道傳入工廠類的類型參數。
而工廠方法的適用場景:
-
a.工廠類對象創建邏輯相對複雜,需要將工廠實例化延遲到其具體工廠子類中。
-
b.適合需求變更頻繁的場景,可以利用不同的工廠實現類支援新的工廠創建方案,更符合開閉原則,擴展性更好。
典型程式碼示例
//抽象產品類
public abstract class Product {
public abstract void method();
}
//具體的產品類
class ProductA extends Product {
@Override
public void method() {
//具體的執行邏輯
}
}
//抽象工廠模板類
abstract class Factory<T> {
abstract Product createProduct(Class<T> c);
}
//具體工廠實現類
class FactoryA extends Factory{
@Override
Product createProduct(Class c) {
Product product = (Product) Class.forName(c.getName()).newInstance();
return product;
}
}
實際程式碼
/**
* @author chenwangrong
* 活動策略工廠類
*/
@Component
@Slf4j
public class ActivityStrategyFactory {
/**
* 獲得渠道類型對應的策略
*
* @param channelType channelType
* @return OperationServiceStrategy
*/
public static ActivityStrategy getActivityStrategy(ChannelTypeEnum channelType) {
ChannelTypeStrategyEnum channelTypeStrategyEnum = ChannelTypeStrategyEnum.getByChannelType(channelType);
Assert.notNull(channelTypeStrategyEnum , "指定的渠道類型[channelType=" + channelType + "]不存在");
String strategyName= channelTypeStrategyEnum.getHandlerName();
Assert.notNull(strategyName, "指定的渠道類型[channelType=" + channelType + "未配置策略");
return (ActivityStrategy)SpringContextHolder.getBean(handlerName);
}
public enum ChannelTypeStrategyEnum {
/**
* 簡訊渠道
*/
SMS(ChannelTypeEnum.SMS, "smsActivityStrategy"),
/**
* 微信渠道
*/
WX_NEWS(ChannelTypeEnum.WX, "wxActivityStrategy"),
/**
* push渠道
*/
PUSH(ChannelTypeEnum.PUSH, "pushActivityStrategy"),;
private final ChannelTypeEnum channelTypeEnum;
private final String strategyName;
ChannelTypeStrategyEnum (ChannelTypeEnum channelTypeEnum, String strategyName) {
this.channelTypeEnum = channelTypeEnum;
this.strategyName= strategyName;
}
public String getStrategyName() {
return strategyName;
}
public static ChannelTypeStrategyEnum getByChannelType(ChannelTypeEnum channelTypeEnum) {
for (ChannelTypeStrategyEnum channelTypeStrategyEnum : values()) {
if (channelTypeEnum == channelTypeStrategyEnum.channelTypeEnum) {
return channelTypeStrategyEnum ;
}
}
return null;
}
}
}
實踐總結
在實際項目程式碼中我們採用的是簡單工廠模式(靜態工廠模式),實現時利用枚舉(或者映射配置表)來保存渠道類型與具體策略實現類的映射關係,再結合Spring的單例模式,來進行策略類的創建。
相比於工廠方法模式,在滿足業務的前提下,減少了工廠類數量,程式碼更加簡單適用。
3.1.2 模板方法模式
具體場景
在創建不同類型運營活動策略的時候,可以發現除了保存具體活動渠道配置資訊不一樣之外,創建過程中很多操作流程是相同的:比如保存活動基本配置資訊,審計日誌上報,創建活動審批工單,創建完成後消息提醒等。
原有實踐
/**
* 簡訊活動類
*
*/
@Service
public class SmsActivityStrategy{
/**
* 執行渠道發送
*
* @param msgParam msgParam
*/
public ProcessResult createActivity(ActParam param) {
//保存活動基礎資訊
saveActBaseConfig(param);
//保存簡訊活動配置
createSmsActivity(param);
//審計日誌上報 ...
//創建活動審批工單 ...
//消息通知 ...
sendNotification(param);
}
}
/**
* Push活動類
*
*/
@Service
public class PushActivityStrategy{
/**
* 執行渠道發送
*
* @param msgParam msgParam
*/
public ProcessResult createActivity(ActParam param) {
//保存活動基礎資訊
saveActBaseConfig(param);
//保存Push活動配置
createChannelActivity(param);
//審計日誌上報 ...
//創建活動審批工單 ...
//消息通知 ...
sendNotification(param);
}
}
...
對於每種活動策略而言,這些操作都是必需的且操作流程都是固定的,所以可以將這些操作提取成公用的流程,此時就考慮到了模板方法模式。
模式分析
在GoF《設計模式:可復用面向對象軟體的基礎》:模板方法模式是在一個方法中定義一個演算法骨架,並將某些步驟推遲到其子類中實現。模板方法模式允許子類在不改變演算法結構的情況下重新定義演算法的某些步驟。
上面所指的「演算法」,可以理解為業務邏輯,而『』演算法骨架「即是模板,包含『』演算法骨架「的方法就是模板方法,這也是模板方法模式名稱的來源。
模板方法模式適用場景:業務邏輯由確定的步驟組成,這些步驟的順序要是固定不變的,不同的具體業務之間某些方法或者實現可以有所不同。
實現時一般通過抽象類來定義一個邏輯模板和框架,然後將無法確定的部分抽象成抽象方法交由子類來實現,調用邏輯仍在抽象類中完成。
典型程式碼示例
//模板類
public abstract class AbstractTemplate {
//業務邏輯1
protected abstract void doStep1();
//業務邏輯2
protected abstract void doStep2();
//模板方法
public void templateMethod(){
this.doStep1();
//公共邏輯
......
this.doStep2();
}
}
//具體實現類1
public class ConcreteClass1 extends AbstractTemplate {
//實現業務邏輯1
protected void doStep1()
{
//業務邏輯處理
}
//實現業務邏輯2
protected void doStep2()
{
//業務邏輯處理
}
}
//具體實現類2
public class ConcreteClass2 extends AbstractTemplate {
//實現業務邏輯1
protected void doStep1()
{
//業務邏輯處理
}
//實現業務邏輯2
protected void doStep2()
{
//業務邏輯處理
}
}
// 調用類
public class Client {
public static void main(String[] args)
{
AbstractTemplate class1=new ConcreteClass1();
AbstractTemplate class2=new ConcreteClass2();
//調用模板方法
class1.templateMethod();
class2.templateMethod();
}
}
實際程式碼
/**
* 活動創建模板類
*
* @author chenwangrong
*/
@Slf4j
public abstract class AbstractActivityTemplate{
/**
* 保存具體活動配置
*
* @param param 活動參數
* @return ProcessResult 處理結果
*/
protected abstract ProcessResult createChannelActivity(ActParam param);
/**
* 執行活動創建
*
* @param msgParam msgParam
*/
public ProcessResult createActivity(ActParam param) {
//保存活動基礎資訊
saveActBaseConfig(param);
//保存具體渠道配置
createChannelActivity(param);
//審計日誌上報 ...
//消息通知 ...
}
}
/**
* 簡訊活動類
*
*/
@Service
public class SmsActivityStrategy extends AbstractActivityTemplate{
/**
* 創建簡訊渠道活動配置
*
* @param msgParam msgParam
*/
public ProcessResult createChannelActivity(ActParam param) {
//僅需要實現:保存簡訊活動配置
createSmsActivity(param);
}
}
(其他渠道活動類似,此處省略)
// 調用類
public class Client {
public static void main(String[] args)
{
AbstractActivityTemplate smsActivityStrategy=new SmsActivityStrategy();
AbstractActivityTemplate pushActivityStrategy=new PushActivityStrategy();
ActParam param = new ActParam();
//調用具體活動實現類
smsActivityStrategy.createActivity(param);
pushActivityStrategy.createActivity(param);
}
}
實踐總結
模板方法模式有兩大作用:復用和擴展。復用是指所有的子類可以復用父類中提供的模板方法的程式碼。擴展是指框架通過模板模式提供功能擴展點,讓用戶可以在不修改框架源碼的情況下,基於擴展點訂製化框架的功能。
模板方法非常適用於有通用業務邏輯處理流程,同時又在具體流程上存在一定差異的場景,可以通過將流程骨架抽取到模板類中,將可變的差異點設置為抽象方法,達到封裝不變部分,擴展可變部分的目的。
3.1.3 策略模式
具體場景
上述我們通過模板方法模式抽取出了公共流程骨架,但這裡還存在一個問題:調用類仍需要明確知道具體實現類是哪個,實例化後才可進行調用。也就是每一次增加新的渠道活動時,調用方都必須修改調用邏輯,添加新的活動實現類的初始化調用,顯然不利用業務的擴展性。
在創建運營活動過程中,不同類型的活動會對應著不同的創建流程,調用方只需要根據渠道類型來進行區分,而無需理會其中具體的業務邏輯。此時策略模式是一個比較好的選擇。
模式分析
在GoF《設計模式:可復用面向對象軟體的基礎》中:策略模式定義一族演算法類,將每個演算法分別封裝起來,讓它們可以互相替換。策略模式可以使演算法的變化獨立於使用它們的調用方。
典型程式碼示例
//策略介面定義
public interface Strategy {
void doStrategy();
}
//策略具體實現類(多個)
public class StrategyA implements Strategy{
@Override
public void doStrategy() {
}
}
//上下文操作類, 屏蔽高層模組對策略的直接訪問
public class Context {
private Strategy strategy = null;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void doStrategy() {
strategy.doStrategy();
}
}
實際程式碼
/**
* 渠道活動創建策略介面
*
*/
public interface ActivityStrategy {
/**
* 創建渠道活動配置
*
* @param param 活動參數
* @return
*/
void createActivity(ActParam param);
}
/**
* 活動模板類
*
*/
@Slf4j
public abstract class AbstractActivityTemplate implements ActivityStrategy {
/**
* 抽象方法:具體渠道活動創建
*
*/
protected abstract ProcessResult createChannelActivity(ActParam param);
@Override
public ProcessResult createActivity(ActParam param) {
//保存活動基礎資訊
saveActBaseConfig(param);
//保存具體渠道配置
createChannelActivity(param);
//審計日誌上報 ...
//消息通知 ...
}
}
/**
* 簡訊推送策略具體實現類
*
*/
@Component
public class SmsChannelActivityStrategy extends AbstractActivityTemplate {
@Override
public void createChannelActivity(ActParam param) {
//保存簡訊配置數據
}
}
(其他渠道活動類似,此處省略)
/**
* 策略調用入口
*
*/
@Slf4j
@Component
public class ActivityContext {
@Resource
private ActivityStrategyFactory activityStrategyFactory ;
public void create(ActParam param) {
//通過前面的工廠模式的程式碼,獲取具體渠道對應的策略類
ActivityStrategy strategy = activityStrategyFactory.getActivityStrategy(param.ChannelType);
//執行策略
strategy.createActivity(param);
}
}
實際編碼過程中,我們加入了ChannelActivityStrategy作為渠道活動創建策略介面,並用模板類AbstractActivityTemplate實現該介面,同時結合工廠模式創建具體策略,至此將三種模式結合了起來。
實踐總結
策略模式在項目開發過程中經常用於消除複雜的if else複雜邏輯,後續如果有新的渠道活動時,只需要新增對應渠道的活動創建邏輯即可,可以十分便捷地對系統業務進行擴展。
在項目實踐過程,經常會將工廠模式、模板方法模式和策略模式一起結合使用。模板方法模式進行業務流程公共骨架的抽取,策略模式進行具體子流程策略的實現和調用的封裝,而工廠模式可以進行子流程策略的創建。
多種模式的結合使用可以充分發揮出各個模式的優勢,達到真正提升系統設計擴展性的目的。
3.2 運營活動執行
3.2.1 狀態模式
具體場景
在運營活動的執行過程中,會涉及活動狀態的變更,以及變更前的條件檢測和變更後的操作處理。與之相對應地,我們很容易就會想到狀態模式。
模式分析
在 GoF 經典的《設計模式:可復用面向對象軟體的基礎》中:狀態模式允許一個對象在其內部狀態改變的時候改變其行為。
狀態模式的作用就是分離狀態的行為,通過維護狀態的變化,來調用不同狀態對應的不同功能。它們的關係可以描述為:狀態決定行為。由於狀態是在運行期被改變的,因此行為也會在運行期隨著狀態的改變而改變。
典型程式碼示例
/**
* 狀態模式
* 抽象狀態類
* */
interface State {
//狀態對應的處理
void handle()
}
//具體狀態關現類
public class ConcreteStateA implements State {
@Override
public void handle() {
}
}
public class ConcreteStateB implements State {
@Override
public void handle() {
}
}
//環境類Context,訪問入口
public class Context {
//持有一個State類型的對象實例
private State state;
public void setState(State state) {
this.state = state;
}
public void request() {
//轉調state來處理
state.handle();
}
}
public class Client {
public static void main(String[] args){
//創建狀態
State state = new ConcreteStateB();
//創建環境
Context context = new Context();
//將狀態設置到環境中
context.setState(state);
//請求
context.request();
}
}
實踐總結
在實際軟體項目開發中,業務狀態不多且狀態轉移簡單的場景, 可使用狀態模式來實現;但如果是涉及的業務流程狀態轉移繁雜時,使用狀態模式會引入非常多的狀態類和方法,當狀態邏輯有變更時,程式碼也會變得難以維護,此時使用狀態模式並不十分適合。
而當流程狀態繁多,事件校驗和觸發執行動作包含的業務邏輯比較複雜時,如何去實現呢?
這裡我們必須停下來思考:使用設計模式只是解決實際問題的一種手段,但設計模式不是一把「萬能的」鎚子,需要清楚地了解到它的優勢和不足。而這種問題場景下,業界已經有一個更通用的方案——有限狀態機,通過更高層的封裝,提供給業務更便捷的應用。
3.2.2 狀態模式的應用——有限狀態機
有限狀態機(Finite-State Machine , 縮寫:FSM),業界簡稱狀態機。它亦是由事件、狀態、動作 三大部分組成,三者的關係是:事件觸髮狀態的轉移,狀態的轉移觸發後續動作的執行。狀態機可以基於傳統的狀態模式硬編碼來實現,也可以通過資料庫/文件配置或者DSL的方式來保存狀態及轉移配置來實現(推薦)。
業界中也已湧現出了不少開源狀態機的框架,比較常用的有Spring-statemachine(Spring官方提供) 、squirrel statemachine和阿里開源的cola-statemachine。
實際應用
在實際項目開發中,我們針對自身業務的特點:業務流程狀態多,但是事件觸發和狀態變更動作相對簡單,故而選擇了無狀態、更加輕量級的解決方案——基於開源的狀態機實現思想進行開發。(關於狀態機的實現和使用選型會在後續的文章中做進一步的分析,感興趣的童鞋可以訪問官網先做了解)。
實踐程式碼
/**
* 狀態機工廠類
*/
public class StatusMachineEngine {
private StatusMachineEngine() {
}
private static final Map<OrderTypeEnum, String> STATUS_MACHINE_MAP = new HashMap();
static {
//簡訊推送狀態
STATUS_MACHINE_MAP.put(ChannelTypeEnum.SMS, "smsStateMachine");
//PUSH推送狀態
STATUS_MACHINE_MAP.put(ChannelTypeEnum.PUSH, "pushStateMachine");
//......
}
public static String getMachineEngine(ChannelTypeEnum channelTypeEnum) {
return STATUS_MACHINE_MAP.get(channelTypeEnum);
}
/**
* 觸髮狀態轉移
* @param channelTypeEnum
* @param status 當前狀態
* @param eventType 觸發事件
* @param context 上下文參數
*/
public static void fire(ChannelTypeEnum channelTypeEnum, String status, EventType eventType, Context context) {
StateMachine orderStateMachine = StateMachineFactory.get(STATUS_MACHINE_MAP.get(channelTypeEnum));
//推動狀態機進行流轉,具體介紹本期先省略
orderStateMachine.fireEvent(status, eventType, context);
}
/**
* 簡訊推送活動狀態機初始化
*/
@Component
public class SmsStateMachine implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private StatusAction smsStatusAction;
@Autowired
private StatusCondition smsStatusCondition;
//基於DSL構建狀態配置,觸發事件轉移和後續的動作
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
StateMachineBuilder<String, EventType, Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(INIT)
.to(NOT_START)
.on(EventType.TIME_BEGIN)
.when(smsStatusAction.checkNotifyCondition())
.perform(smsStatusAction.doNotifyAction());
builder.externalTransition()
.from(NOT_START)
.to(DATA_PREPARING)
.on(EventType.CAL_DATA)
.when(smsStatusCondition.doNotifyAction())
.perform(smsStatusAction.doNotifyAction());
builder.externalTransition()
.from(DATA_PREPARING)
.to(DATA_PREPARED)
.on(EventType.PREPARED_DATA)
.when(smsStatusCondition.doNotifyAction())
.perform(smsStatusAction.doNotifyAction());
...(省略其他狀態)
builder.build(StatusMachineEngine.getMachineEngine(ChannelTypeEnum.SMS));
}
//調用端
public class Client {
public static void main(String[] args){
//構建活動上下文
Context context = new Context(...);
// 觸髮狀態流轉
StatusMachineEngine.fire(ChannelTypeEnum.SMS, INIT, EventType.SUBMIT, context);
}
}
}
通過預定義狀態轉換流程的方式,實現ApplicationListener介面,在應用啟動時將事件、狀態轉移條件和觸發操作的流程載入到狀態機工作記憶體中,由事件觸發驅動狀態機進行自動流轉。
實踐總結
實際場景中,不必強行套用設計模式,而是應當充分結合業務的特點,同時針對設計模式的優劣勢,進行更加合適的選型或者進一步擴展。
3.3 自動化運營活動審批
3.3.1 設計模式的綜合應用——工作流引擎
具體場景
為了做好品質和風險管控,活動創建需要加入審批環節,把控運營活動的發布執行,同時對於不同類型的運營活動,可能涉及的業務領域和部門各不相同,審批管控人員也不一樣,需要配置相對應的審批關係。
此時需要做到:
-
a.審批流程全配置化,易修改和添加;
-
b.業務流程節點可自由編排,組件公用化;
-
c.流程數據持久化,審批過程數據需要進行操作監控。
針對這方面的需求,業界有一套通用的業務工具——工作流引擎。工作流引擎顯然並不屬於具體某一種設計模式的實現,它是涵蓋了多種設計模式的組件應用。
不僅僅是審批功能,其實前面自動化營銷流程引擎設計也同樣是使用工作流引擎搭建流程組件:
狀態機 VS 工作流引擎
工作流引擎和狀態機似乎存在非常多的相似之處,都可以通過定義流程的節點、轉移條件和相應觸發的操作來完成業務流程。如果只從適用場景的複雜性上看,狀態機更適用於單維度的業務問題,能夠清晰地描繪出所有可能的狀態以及導致轉換的事件,更加靈活輕便;而工作流引擎則更適合業務流程管理,解決如大型CRM複雜度更高的流程自動化問題,可以改善整體業務流程的效率。
在業界的工作流引擎中,比較著名的有Activiti和JBPM等。(關於狀態機和工作流引擎的對比、開源工作流引擎的具體介紹和選型,以及如何自行開發構建一款基本的工作流引擎組件,同樣是會在後續的文章中做進一步分析,本文由於主題和篇幅的原因暫不做詳細介紹。)
在實際開發過程中,我們是基於開源的Activiti工作流引擎自研了一套簡易版的工作流引擎,精簡了許多相關的配置,只留下了核心流程操作和數據記錄。
工作流引擎流程圖:
實踐總結
工作流引擎是涵蓋了多種設計模式的應用組件,只有在複雜多變的業務場景中才需要應用,需要結合業務進行仔細評估。在合適的場景使用合適的解決方案,遵循系統架構設計的簡單、合適、可演化原則,不過度設計。
四、總結
本文基於自動化營銷的業務實踐,分析介紹了工廠方法模式、模板方法模式、策略模式以及狀態模式這四種模式在項目開發中的具體實現過程。也在單純的模式之外介紹了狀態機和工作流引擎這些涵蓋了多種設計模式系統組件,並分享了過程中的選擇和思考。
面對業務複雜多變的需求,需要時刻關注系統設計的復用性和可擴展性,而設計原則和設計模式可以在系統設計實現時給予我們方向性的指導,同時更需要根據實際業務場景進行合理的選擇,合適的變通,不斷完善自己的方法論。
後續我們將帶來系列專題文章的其他內容,每一篇文章都會對裡面的技術實踐進行詳盡解析,敬請期待。
作者:vivo互聯網伺服器團隊-Chen Wangrong