暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

设计模式的修炼法则

程序员星宇 2021-06-09
597


前言


    关于设计模式,我的理解更多的其实是停留再理论基础之上的,并没有真正体会到一个好的设计模式有多重要,事实上在项目中恰到好处的设计模式往往能起到四两拨千斤的效果。下面就先复习一下设计模式的理论概念,在结合一些使用场景体会一下设计模式带来的代码上的简洁和业务实现、拓展的高效。这里的场景主要是参考美团技术团队的文章,将美团的一些业务和设计模式关联起来学习,一方面更容易理解,另一方面毕竟有些东西是人家技术团队做的,技术方面还是有一定参考价值的。


一、问题引入

    业务策略多变导致需求多变,是业界很多技术团队面临的最具挑战的问题之一。那么如何设计一套易于扩展和维护的营销系统呢?

美团外面技术团队分享了从领域模型到代码工程之间的转化,从DDD引出了设计模式,并详细介绍了工厂方法模式、策略模式、责任链模式以及状态模式这四种模式在美团营销业务中的具体实现,将理论与实践进行了一次深度结合。

二、设计模式和领域驱动设计

    美团在设计营销系统的时候,通常的做法就是采用自顶向下的方式来解构业务,因此引入了DDD概念(Domain-Driver Design),DDD能够指导完成从问题空间到解决方案的剖析,将业务需求映射为领域上下文以及上下文间的映射关系。从战术层面上,DDD能够细化领域上下文,并形成有效的、细化的领域模型来指导工程实践。建立领域模型的一个关键意义在于,能够确保不断扩展和变化的需求在领域模型内不断地演进和发展,而不至于出现模型的腐化和领域逻辑的外溢。

    而设计模式天然具备成为领域模型到工程代码之间的桥梁。

无论是领域驱动模式还是设计模式,本质上都是“模式”,只是解决的问题不一样。站在业务建模的立场上,DDD的模式解决的是如何进行领域建模。而站在代码实践的立场上,设计模式主要关注于代码的设计与实现。既然本质都是模式,那么它们天然就具有一定的共通之处。

DDD分为战略设计和战术设计。在战略设计中,我们讲求的是子域和限界上下文(Bounded Context,BC)的划分,以及各个限界上下文之间的上下游关系。当前如此火热的“在微服务中使用DDD”这个命题,究其最初的逻辑无外乎是“DDD中的限界上下文可以用于指导微服务中的服务划分”。

滕云,公众号:ThoughtWorks洞见后端开发实践系列——领域驱动设计(DDD)编码实践


三、设计模式在外卖营销业务中的具体案例

美团技术团队在项目中是如何实践领域模型的

文彬 子维,公众号:美团点评技术团队大家一直在谈的领域驱动设计(DDD),我们在互联网业务系统是这么实践的

3.1、设计模式原则

  • 开闭原则原则:对扩展开发,对修改关闭;

软件开发过程中,需求是不断变化的,因为变化、升级和维护等原因需要对原有的软件代码进行修改,而一旦对原有的代码进行修改,就有可能影响到原有的模块,引起bug,因此,在软件开发过程中,我们应该尽可能通过扩展的方式实现变化,而不是修改原有的代码


假如有类Class1完成职责T1,T2,当职责T1或T2有变更需要修改时,有可能影响到该类的另外一个职责正常工作。


  • 单一职责原则:实现类的职责单一;

如果发现一个类承担了太多的功能,这个时候就要考虑将某些功能划分到其他类中去处理,具体的划分细节要平开发者的个人经验。

  • 里氏替换原则:不要破坏继承体系;

所有引用基类的地方都必须能够透明的使用其子类


只要父类能出现的地方子类就能出现。反之,父类则未必能胜任。

  • 依赖倒置原则:面向接口编程;

依赖倒置原则指的是高层次模块不应该依赖于低层次模块的具体实现,两者都应该依赖其抽象.


类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。因此将类A修改为依赖接口interface,类B和类C各自实现接口interface,类A通过接口interface间接与类B或者类C发生联系,则会大大降低修改类A的几率。

  • 接口隔离原则:设计接口要精简单一

接口定义要小,但是要有限度,对接口细化可以增加灵活性,但是过度细化则会使设计复杂化

  • 合成/聚合复用原则:

组合/聚合和继承是实现复用的两个基本途径。合成复用原则是指尽量使用组合/聚合,而不是使用继承。子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分“Has-A”和“Is-A”.只有“Is-A”关系才符合继承关系,“Has-A”关系应当使用聚合来描述。

  • 最少知识原则:降低耦合

一个对象应该对其他对象有最小的了解,也称作最小知道原则,即类和类直接应该建立在最小的依赖之上,一个类应该对其依赖的类有最小的了解,即只需要知道其所需要的方法即可,至于其内部具体是如何实现的则不用关心。


快速理解设计模式六大原则(偏理论)

https://www.jianshu.com/p/807bc228dbc2

理解设计模式(以代码为例子)

https://www.cnblogs.com/shijingjing07/p/6227728.html


3.2、“邀请下单”业务中设计模式的实践

“邀请下单”是美团外卖用户邀请其他用户下单后给予奖励的平台。这个业务的主要后端技术包含以下两个点:

  • 返奖金额的计算,涉及到不同的计算规则。

  • 从邀请开始到返奖结束的整个流程。

3.2.1、业务建模

从模型中可以看出来虽然返奖的金额计算规则不同的用户是不同的,但是无论是何种用户,对于整体返奖流程是不变的,唯一变化的是返奖规则。此处,我们可参考开闭原则,对于返奖流程保持封闭,对于可能扩展的返奖规则进行开放。我们将返奖规则抽象为返奖策略,即针对不同用户类型的不同返奖方案,我们视为不同的返奖策略,不同的返奖策略会产生不同的返奖金额结果。

    通过分析,在我们的领域模型中,返奖策略是一个值对象,我们可以通过工厂的方式针对不同的用户奖励策略生成值对象。

设计模式:工厂模式

工厂模式又细分为工厂方法模式和抽象工厂模式。

工厂方法模式定义:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法是一个类的实例化延迟到其子类。

    //抽象的产品
    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;
    }
    }

    设计模式:策略模式

    模式定义:定义一系列算法,将每个算法都封装起来,并且它们可以互换。策略模式是一种对象行为模式。

      //定义一个策略接口
      public interface Strategy {
      void strategyImplementation();
      }


      //具体的策略实现(可以定义多个具体的策略实现)
      public class StrategyA implements Strategy{
      @Override
      public void strategyImplementation() {
      System.out.println("正在执行策略A");
      }
      }


      //封装策略,屏蔽高层模块对策略、算法的直接访问,屏蔽可能存在的策略变化
      public class Context {
      private Strategy strategy = null;


      public Context(Strategy strategy) {
      this.strategy = strategy;
      }


      public void doStrategy() {
      strategy.strategyImplementation();
      }
      }

      项目实践 

          返奖的主流程就是选择不同的返奖策略的过程,每个返奖策略都包括返奖金额计算、更新用户奖金信息、以及结算这三个步骤。我们可以使用工厂模式生产出不同的策略,同时使用策略模式来进行不同的策略执行。


      1.    首先确定我们需要生成出n种不同的返奖策略

        //抽象策略
        public abstract class RewardStrategy {
        public abstract void reward(long userId);


        public void insertRewardAndSettlement(long userId, int reward) {} ; 更新用户信息以及结算
        }
        //新用户返奖具体策略A
        public class newUserRewardStrategyA extends RewardStrategy {
        @Override
        public void reward(long userId) {} 具体的计算逻辑,...
        }


        //老用户返奖具体策略A
        public class OldUserRewardStrategyA extends RewardStrategy {
        @Override
        public void reward(long userId) {} 具体的计算逻辑,...
        }


        //抽象工厂
        public abstract class StrategyFactory<T> {
        abstract RewardStrategy createStrategy(Class<T> c);
        }


        //具体工厂创建具体的策略
        public class FactorRewardStrategyFactory extends StrategyFactory {
        @Override
        RewardStrategy createStrategy(Class c) {
        RewardStrategy product = null;
        try {
        product = (RewardStrategy) Class.forName(c.getName()).newInstance();
        } catch (Exception e) {}
        return product;
        }
        }

        2.    通过工厂模式生产出具体的策略之后,根据我们之前的介绍,很容易就可以想到使用策略模式来执行我们的策略。

          public class RewardContext {
          private RewardStrategy strategy;


          public RewardContext(RewardStrategy strategy) {
          this.strategy = strategy;
          }


          public void doStrategy(long userId) {
          int rewardMoney = strategy.reward(userId);
          insertRewardAndSettlement(long userId, int reward) {
          insertReward(userId, rewardMoney);
          settlement(userId);
          }
          }
          }

          3.    接下来我们将工厂模式和策略模式结合在一起,就完成了整个返奖的过程

            public class InviteRewardImpl {
            返奖主流程
            public void sendReward(long userId) {
            FactorRewardStrategyFactory strategyFactory = new FactorRewardStrategyFactory(); 创建工厂
            Invitee invitee = getInviteeByUserId(userId); 根据用户id查询用户信息
            if (invitee.userType == UserTypeEnum.NEW_USER) { 新用户返奖策略
            NewUserBasicReward newUserBasicReward = (NewUserBasicReward) strategyFactory.createStrategy(NewUserBasicReward.class);
            RewardContext rewardContext = new RewardContext(newUserBasicReward);
            rewardContext.doStrategy(userId); 执行返奖策略
            }if(invitee.userType == UserTypeEnum.OLD_USER){} 老用户返奖策略,...
            }
            }


            工厂方法模式帮助我们直接产生一个具体的策略对象,策略模式帮助我们保证这些策略对象可以自由地切换而不需要改动其他逻辑,从而达到解耦的目的。通过这两个模式的组合,当我们系统需要增加一种返奖策略时,只需要实现RewardStrategy接口即可,无需考虑其他的改动。当我们需要改变策略时,只要修改策略的类名即可。不仅增强了系统的可扩展性,避免了大量的条件判断,而且从真正意义上达到了高内聚、低耦合的目的。


            返奖流程与设计模式实践

            领域建模:

            1. 收到订单消息后,用户进入待校验状态。

            2. 在校验后,若校验通过,用户进入预返奖状态,并放入延迟队列。若校验未通过,用户进入不返奖状态,结束流程;

            3. T+N天后,处理延迟消息,若用户未退款,进入待返奖状态。若用户退款,进入失败状态,结束流程;

            4. 执行返奖,若返奖成功,进入完成状态,结束流程。若返奖不成功,进入待补偿状态;

            5. 待补偿状态的用户会由任务定期触发补偿机制,直至返奖成功,进入完成状态,保障流程结束。

            设计模式:状态模式

            当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。

            对比策略模式的类型会发现和状态模式的类图很类似,但实际上有很大的区别,具体体现在concrete class上。策略模式通过Context产生唯一一个ConcreteStrategy作用于代码中,而状态模式则是通过context组织多个ConcreteState形成一个状态转换图来实现业务逻辑。接下来,我们通过一段通用代码来解释怎么使用状态模式:

              //定义一个抽象的状态类
              public abstract class State {
              Context context;
              public void setContext(Context context) {
              this.context = context;
              }
              public abstract void handle1();
              public abstract void handle2();
              }
              //定义状态A
              public class ConcreteStateA extends State {
              @Override
              public void handle1() {} 本状态下必须要处理的事情


              @Override
              public void handle2() {
              super.context.setCurrentState(Context.contreteStateB); 切换到状态B
              super.context.handle2(); 执行状态B的任务
              }
              }
              //定义状态B
              public class ConcreteStateB extends State {
              @Override
              public void handle2() {} 本状态下必须要处理的事情,...


              @Override
              public void handle1() {
              super.context.setCurrentState(Context.contreteStateA); 切换到状态A
              super.context.handle1(); 执行状态A的任务
              }
              }
              //定义一个上下文管理环境
              public class Context {
              public final static ConcreteStateA contreteStateA = new ConcreteStateA();
              public final static ConcreteStateB contreteStateB = new ConcreteStateB();


              private State CurrentState;
              public State getCurrentState() {return CurrentState;}


              public void setCurrentState(State currentState) {
              this.CurrentState = currentState;
              this.CurrentState.setContext(this);
              }


              public void handle1() {this.CurrentState.handle1();}
              public void handle2() {this.CurrentState.handle2();}
              }
              //定义client执行
              public class client {
              public static void main(String[] args) {
              Context context = new Context();
              context.setCurrentState(new ContreteStateA());
              context.handle1();
              context.handle2();
              }
              }


              工程实践

              一个状态的下游不会涉及特别多的状态装换,所以我们简化了状态模式。当前的状态只负责当前状态要处理的事情,状态的流转则由第三方类负责。其实践代码如下:

                //返奖状态执行的上下文
                public class RewardStateContext {


                private RewardState rewardState;


                public void setRewardState(RewardState currentState) {this.rewardState = currentState;}
                public RewardState getRewardState() {return rewardState;}
                public void echo(RewardStateContext context, Request request) {
                rewardState.doReward(context, request);
                }
                }


                public abstract class RewardState {
                abstract void doReward(RewardStateContext context, Request request);
                }


                //待校验状态
                public class OrderCheckState extends RewardState {
                @Override
                public void doReward(RewardStateContext context, Request request) {
                orderCheck(context, request); //对进来的订单进行校验,判断是否用券,是否满足优惠条件等等
                }
                }


                //待补偿状态
                public class CompensateRewardState extends RewardState {
                @Override
                public void doReward(RewardStateContext context, Request request) {
                compensateReward(context, request); //返奖失败,需要对用户进行返奖补偿
                }
                }


                //预返奖状态,待返奖状态,成功状态,失败状态(此处逻辑省略)
                //..


                public class InviteRewardServiceImpl {
                public boolean sendRewardForInvtee(long userId, long orderId) {
                Request request = new Request(userId, orderId);
                RewardStateContext rewardContext = new RewardStateContext();
                rewardContext.setRewardState(new OrderCheckState());
                rewardContext.echo(rewardContext, request); //开始返奖,订单校验
                //此处的if-else逻辑只是为了表达状态的转换过程,并非实际的业务逻辑
                if (rewardContext.isResultFlag()) { //如果订单校验成功,进入预返奖状态
                rewardContext.setRewardState(new BeforeRewardCheckState());
                rewardContext.echo(rewardContext, request);
                } else {//如果订单校验失败,进入返奖失败流程,...
                rewardContext.setRewardState(new RewardFailedState());
                rewardContext.echo(rewardContext, request);
                return false;
                }
                if (rewardContext.isResultFlag()) {//预返奖检查成功,进入待返奖流程,...
                rewardContext.setRewardState(new SendRewardState());
                rewardContext.echo(rewardContext, request);
                } else { //如果预返奖检查失败,进入返奖失败流程,...
                rewardContext.setRewardState(new RewardFailedState());
                rewardContext.echo(rewardContext, request);
                return false;
                }
                if (rewardContext.isResultFlag()) { //返奖成功,进入返奖结束流程,...
                rewardContext.setRewardState(new RewardSuccessState());
                rewardContext.echo(rewardContext, request);
                } else { //返奖失败,进入返奖补偿阶段,...
                rewardContext.setRewardState(new CompensateRewardState());
                rewardContext.echo(rewardContext, request);
                }
                if (rewardContext.isResultFlag()) { //补偿成功,进入返奖完成阶段,...
                rewardContext.setRewardState(new RewardSuccessState());
                rewardContext.echo(rewardContext, request);
                } else { //补偿失败,仍然停留在当前态,直至补偿成功(或多次补偿失败后人工介入处理)
                rewardContext.setRewardState(new CompensateRewardState());
                rewardContext.echo(rewardContext, request);
                }
                return true;
                }
                }


                状态模式的核心是封装,将状态以及状态转换逻辑封装到类的内部来实现,也很好的体现了“开闭原则”和“单一职责原则”。每一个状态都是一个子类,不管是修改还是增加状态,只需要修改或者增加一个子类即可。

                点评外卖投放系统中设计模式的实践


                业务建模

                1. 过滤规则大部分可重用,但也会有扩展和变更。

                2. 不同资源位的过滤规则和过滤顺序是不同的。

                3. 同一个资源位由于业务所处的不同阶段,过滤规则可能不同。

                设计模式:责任链模式

                为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

                在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。

                  //定义一个抽象的handle
                  public abstract class Handler {
                  private Handler nextHandler; //指向下一个处理者
                  private int level; //处理者能够处理的级别


                  public Handler(int level) {
                  this.level = level;
                  }


                  public void setNextHandler(Handler handler) {
                  this.nextHandler = handler;
                  }


                  // 处理请求传递,注意final,子类不可重写
                  public final void handleMessage(Request request) {
                  if (level == request.getRequstLevel()) {
                  this.echo(request);
                  } else {
                  if (this.nextHandler != null) {
                  this.nextHandler.handleMessage(request);
                  } else {
                  System.out.println("已经到最尽头了");
                  }
                  }
                  }
                  // 抽象方法,子类实现
                  public abstract void echo(Request request);
                  }


                  // 定义一个具体的handleA
                  public class HandleRuleA extends Handler {
                  public HandleRuleA(int level) {
                  super(level);
                  }
                  @Override
                  public void echo(Request request) {
                  System.out.println("我是处理者1,我正在处理A规则");
                  }
                  }


                  //定义一个具体的handleB
                  public class HandleRuleB extends Handler {} //...


                  //客户端实现
                  class Client {
                  public static void main(String[] args) {
                  HandleRuleA handleRuleA = new HandleRuleA(1);
                  HandleRuleB handleRuleB = new HandleRuleB(2);
                  handleRuleA.setNextHandler(handleRuleB); //这是重点,将handleA和handleB串起来
                  handleRuleA.echo(new Request());
                  }
                  }

                  工程实践

                    //定义一个抽象的规则
                    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)
                    }
                    }
                    }

                        责任链模式最重要的优点就是解耦,将客户端与处理者分开,客户端不需要了解是哪个处理者对事件进行处理,处理者也不需要知道处理的整个流程。

                       后台的过滤规则会经常变动,规则和规则之间可能也会存在传递关系,通过责任链模式,我们将规则与规则分开,将规则与规则之间的传递关系通过Spring注入到List中,形成一个链的关系。当增加一个规则时,只需要实现BasicRule接口,然后将新增的规则按照顺序加入Spring中即可。当删除时,只需删除相关规则即可,不需要考虑代码的其他逻辑。从而显著地提高了代码的灵活性,提高了代码的开发效率,同时也保证了系统的稳定性。


                    结束语

                        设计模式只是软件开发领域内多年来的经验总结,任何一个或简单或复杂的设计模式都会遵循上述的七大设计原则,只要大家真正理解了七大设计原则,设计模式对我们来说应该就不再是一件难事。但是,使用设计模式也不是要求我们循规蹈矩,只要我们的代码模型设计遵循了上述的七大原则,我们会发现原来我们的设计中就已经使用了某种设计模式。    

                        关于领域模型和设计模式的学习其实远不止文章这些东西,要想熟练掌握、灵活运用,一方面需要对理论基础深刻理解,另一方面需要大量的项目经验,以业务为基础,深刻思考系统设计方案。本文从美团技术团队对业务的整理和领域模型的建立一步一步学习,还是很值得反复学习的。

                       最后老话,欢迎有问题,有建议,有好资料的哥们和我分享,当然我也很乐意分享我的资料。只要你主动我们就有故事!!!

                    我是星宇,一个满头黑发,渴望秃头的开发,我们下期见!



                    文章转载自程序员星宇,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论