起因

最近在实习的时候一直在思考怎么提高代码健壮性与可复用性。 工程实践这块确实存在一些欠缺,应当与设计模式相结合,努力提高代码的健壮性的同时,也提高大佬们复用我逻辑的可能性。

设计模式

设计模式应当是最有用的,和本科期间写的玩具项目不同,没有多人维护,生命周期也可能只在一个月,甚至更少。

设计模式原则

面向对象的设计模式有七大基本原则:

  • 开闭原则(Open Closed Principle,OCP)
  • 单一职责原则(Single Responsibility Principle, SRP)
  • 里氏代换原则(Liskov Substitution Principle,LSP)
  • 依赖倒转原则(Dependency Inversion Principle,DIP)
  • 接口隔离原则(Interface Segregation Principle,ISP)
  • 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
  • 最少知识原则(Least Knowledge Principle,LKP)或者迪米特法则(Law of Demeter,LOD)

简单理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改关闭; 单一职责原则指导我们实现类要职责单一;里氏替换原则指导我们不要破坏继承体系; 依赖倒置原则指导我们要面向接口编程; 接口隔离原则指导我们在设计接口的时候要精简单一; 迪米特法则指导我们要降低耦合。

设计模式就是通过这七个原则,来指导我们如何做一个好的设计。但是设计模式不是一套“奇技淫巧”,它是一套方法论,一种高内聚、低耦合的设计思想。我们可以在此基础上自由的发挥,甚至设计出自己的一套设计模式。

当然,学习设计模式或者是在工程中实践设计模式,必须深入到某一个特定的业务场景中去,再结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。如果脱离具体的业务逻辑去学习或者使用设计模式,那是极其空洞的。接下来我们将通过外卖营销业务的实践,来探讨如何用设计模式来实现可重用、易维护的代码。

几个case初探设计模式

言而总之,掌握一个方法论,高内聚、低耦合。

奖励发放逻辑 — 从 if 分支到策略 + 工厂模式

原先的逻辑是RewardService,后续维护起来实在太过繁琐,过多的if else读的头都晕了,维护起来也不方便。

一是不符合开闭原则,可以预见,如果后续新增品类券的话,需要直接修改主干代码,而我们提倡代码应该是对修改封闭的; 二是不符合迪米特法则,发奖逻辑和各个下游接口高度耦合,这导致接口的改变将直接影响到代码的组织,使得代码的可维护性降低。

// 奖励服务
class RewardService {
    // 外部服务
    private WaimaiService waimaiService;
    private HotelService hotelService;
    private FoodService foodService;
    // 使用对入参的条件判断进行发奖
    public void issueReward(String rewardType, Object ... params) {
        if ("Waimai".equals(rewardType)) {
            WaimaiRequest request = new WaimaiRequest();
            // 构建入参
            request.setWaimaiReq(params);
            waimaiService.issueWaimai(request);
        } else if ("Hotel".equals(rewardType)) {
            HotelRequest request = new HotelRequest();
            request.addHotelReq(params);
            hotelService.sendPrize(request);
        } else if ("Food".equals(rewardType)) {
            FoodRequest request = new FoodRequest(params);
            foodService.getCoupon(request);
        } else {
          	throw new IllegalArgumentException("rewardType error!");
        }
    }
}

将各个同下游接口交互的功能抽象成单独的服务,封装其参数组装及异常处理,使得发奖主逻辑与其解耦 用策略模式和适配器模式

  1. 【策略模式】将多个“奖励发放”策略封装成统一接口,以便可以动态选择要使用的策略,实现行为的可扩展与可替换。
  2. 【适配器模式】将原本不同的服务类(WaimaiService, HotelService, FoodService)统一“适配”为符合 Strategy 接口的策略类,从而避免客户端直接依赖这些不一致的服务接口。

先实现策略模式,将多个发放策略统一为一个接口

// 策略接口
interface Strategy {
    void issue(Object ... params);
}
// 外卖策略
class Waimai implements Strategy {
  	private WaimaiService waimaiService;
    @Override
    public void issue(Object... params) {
        WaimaiRequest request = new WaimaiRequest();
        // 构建入参
        request.setWaimaiReq(params);
        waimaiService.issueWaimai(request);
    }
}
// 酒旅策略
class Hotel implements Strategy {
  	private HotelService hotelService;
    @Override
    public void issue(Object... params) {
        HotelRequest request = new HotelRequest();
        request.addHotelReq(params);
        hotelService.sendPrize(request);
    }
}
// 美食策略
class Food implements Strategy {
  	private FoodService foodService;
    @Override
    public void issue(Object... params) {
        FoodRequest request = new FoodRequest(params);
        foodService.payCoupon(request);
    }
}

接下来设计策略模式的环境类,同样用了适配器的思想(把不同的类适配成一个共有类,避免我们过度依赖下游的接口)

// 使用分支判断获取的策略上下文
class StrategyContext {
    public static Strategy getStrategy(String rewardType) {
        switch (rewardType) {
            case "Waimai":
                return new Waimai();
            case "Hotel":
                return new Hotel();
            case "Food":
                return new Food();
            default:
                throw new IllegalArgumentException("rewardType error!");
        }
    }
}
// 优化后的策略服务
class RewardService {
    public void issueReward(String rewardType, Object ... params) {
        Strategy strategy = StrategyContext.getStrategy(rewardType);
        strategy.issue(params);
    }
}

优雅,太优雅了,但很显然这些类没必要用有状态的,他们本身并不具有有状态的特质。

实现单例模式,并通过注册表来实现自动注册。

// 策略上下文,用于管理策略的注册和获取
class StrategyContext {
    private static final Map<String, Strategy> registerMap = new HashMap<>();
    // 注册策略
    public static void registerStrategy(String rewardType, Strategy strategy) {
        registerMap.putIfAbsent(rewardType, strategy);
    }
    // 获取策略
    public static Strategy getStrategy(String rewardType) {
        return registerMap.get(rewardType);
    }
}
// 抽象策略类
abstract class AbstractStrategy implements Strategy {
    // 类注册方法
    public void register() {
        StrategyContext.registerStrategy(getClass().getSimpleName(), this);
    }
}
// 单例外卖策略
class Waimai extends AbstractStrategy implements Strategy {
    private static final Waimai instance = new Waimai();
  	private WaimaiService waimaiService;
    private Waimai() {
        register();
    }
    public static Waimai getInstance() {
        return instance;
    }
    @Override
    public void issue(Object... params) {
        WaimaiRequest request = new WaimaiRequest();
        // 构建入参
        request.setWaimaiReq(params);
        waimaiService.issueWaimai(request);
    }
}
// 单例酒旅策略
class Hotel extends AbstractStrategy implements Strategy {
  	private static final Hotel instance = new Hotel();
  	private HotelService hotelService;
    private Hotel() {
        register();
    }
    public static Hotel getInstance() {
        return instance;
    }
    @Override
    public void issue(Object... params) {
        HotelRequest request = new HotelRequest();
        request.addHotelReq(params);
        hotelService.sendPrize(request);
    }
}
// 单例美食策略
class Food extends AbstractStrategy implements Strategy {
  	private static final Food instance = new Food();
  	private FoodService foodService;
    private Food() {
        register();
    }
    public static Food getInstance() {
        return instance;
    }
    @Override
    public void issue(Object... params) {
        FoodRequest request = new FoodRequest(params);
        foodService.payCoupon(request);
    }
}

奖励发放策略_类图

如果用@Component注解会更简单

  • 实战一下 业务逻辑图

通过策略模式和适配器模式,抽象出一个Stratgy接口,新用户普通奖励、新用户梯度、老用户普通一、老用户普通二实现这个接口。 通过单例Bean注入到一个Map中,然后根据key来选择不同的策略

任务模型的设计 - 状态模式 + 发布-订阅

状态的流转我一般是习惯用DAG来实现,使用状态模式直觉上不太像。

而状态变更的通知,可以用观察者模式或发布-订阅

  • 观察者模式

  • 每个想要跟踪状态流转过程的人,需要自己注册到一个ObserverList中,然后在每次状态流转的时候,事件会主动遍历ObserverList,并主动通知ObserverList。

  • 这个过程显然没有解耦。

  • 发布-订阅

  • 对于每个状态的流转,我们都会将其发布一个主题,然后业务方如果想要了解到事件流转状态的话,可以主动订阅这个主题。

  • 这个过程显然解耦的很好。

  • 实战一下  返奖流程与设计模式实践

这个也很显然了,无论你是构建一个DAG,还是通过状态模式都能很好的将状态流转保持下去。 实现一个State接口,里面有update方法,对于每个状态都实现这个接口,

责任链

要擅用stream,用stream来实现一个责任链。 一个责任链可以用策略模式来封装,提高代码的可拓展性。

  • 实战一下 投放业务流程图 业务逻辑图

要实现两个地方,一个是上面的链是否成功,下面的链是否要走

//定义一个抽象的规则
public abstract class BasicRule<CORE_ITEM, T extends RuleContext<CORE_ITEM>>{
    //有两个方法,evaluate用于判断是否经过规则执行,execute用于执行具体的规则内容。
    public abstract boolean evaluate(T context);
    public abstract void execute(T context) {
}
​
//定义所有的规则具体实现
//规则1:判断服务可用性
public class ServiceAvailableRule extends BasicRule<UserPortrait, UserPortraitRuleContext> {
    @Override
    public boolean evaluate(UserPortraitRuleContext context) {
        TakeawayUserPortraitBasicInfo basicInfo = context.getBasicInfo();
        if (basicInfo.isServiceFail()) {
              return false;
        }
        return true;
    }
  
    @Override
    public void execute(UserPortraitRuleContext context) {}
​
}
//规则2:判断当前用户属性是否符合当前资源位投放的用户属性要求
public class UserGroupRule extends BasicRule<UserPortrait, UserPortraitRuleContext> {
    @Override
    public boolean evaluate(UserPortraitRuleContext context) {}
  
    @Override
    public void execute(UserPortraitRuleContext context) {
        UserPortrait userPortraitPO = context.getData();
        if(userPortraitPO.getUserGroup() == context.getBasicInfo().getUserGroup().code) {
          context.setValid(true);
        } else {
          context.setValid(false);
        }
    }
}
  
//规则3:判断当前用户是否在投放城市,具体逻辑省略
public class CityInfoRule extends BasicRule<UserPortrait, UserPortraitRuleContext> {}
//规则4:根据用户的活跃度进行资源过滤,具体逻辑省略
public class UserPortraitRule extends BasicRule<UserPortrait, UserPortraitRuleContext> {} 
​
//我们通过spring将这些规则串起来组成一个一个请求链
    <bean name="serviceAvailableRule" class="com.dianping.takeaway.ServiceAvailableRule"/>
    <bean name="userGroupValidRule" class="com.dianping.takeaway.UserGroupRule"/>
    <bean name="cityInfoValidRule" class="com.dianping.takeaway.CityInfoRule"/>
    <bean name="userPortraitRule" class="com.dianping.takeaway.UserPortraitRule"/>
      
    <util:list id="userPortraitRuleChain" value-type="com.dianping.takeaway.Rule">
        <ref bean="serviceAvailableRule"/>
        <ref bean="userGroupValidRule"/>
        <ref bean="cityInfoValidRule"/>
        <ref bean="userPortraitRule"/>
    </util:list>
      
//规则执行
public class DefaultRuleEngine{
    @Autowired
    List<BasicRule> userPortraitRuleChain;
​
    public void invokeAll(RuleContext ruleContext) {
        for(Rule rule : userPortraitRuleChain) {
            rule.evaluate(ruleContext)
        }
    }
}

参考

设计模式二三事 Java设计模式:23种设计模式全面解析(超级详细) 设计模式在外卖营销业务中的实践