阿里面试官亲述:如何利用设计模式改善业务代码

在业务部门的开发中,大多数的我们在完成的业务的各种需求和提供解决方案,很多场景下的我们通过 CRUD 就能解决问题,但是这样的工作对技术人的提升并不多,如何让自己从业务中解脱出来找到写代码的乐趣呢,我做过一些尝试,使用设计模式改善自己的业务代码就是其中的一种。让代码变得更加简洁和提升健壮性,从代码中寻找一些欢乐。

前言

阿里优秀的人很多,他们身上都有着共同的特质,就是看问题的思考能力,让我最佩服的是思考力强的人,对事情有深入见解和观点的人,大多数人还是停留在表面看问题,很多禁锢在思想里逃不出来,古人说,立德立言立功为三不朽,立言就是思考力和认知力,人和人的差异,在长久的职场中或者生活中,除去运气外,其实就是认知和思考力的差异。所以除去繁琐的工作后,如何在有限的时间从代码中寻找欢乐,需要提高的是思考和规划能力。整理了一份562页设计模式PDF文档

责任链设计模式

▐ 模式定义

责任链模式(Chain of Responsibility Pattern), 是行为型设计模式之一。这种模型结构有点类似现实生活中铁链,由一个个铁环首尾相接构成一条链,如果这种结构用在编程领域,则每个节点可以看做一个对象,每个对象有不同的处理逻辑,将一个请求从链的首端发出,沿着链的路径依次传递每个节点对象,直到有对象处理这个请求为止,我们将这样一种模式称为责任链模式。

▐ 适用场景

适用于多节点的流程处理,每个节点完成各自负责的部分,节点之间不知道彼此的存在,比如 OA 的审批流,Java Web 开发中的 Filter 机制。

  • 多个对象可以处理同一个请求,但具体由哪个对象处理则在运行时动态决定。

  • 在请求处理者不明确的情况下向对个对象中的一个提交一个请求。

  • 需要动态处理一组对象处理请求。

举一个生活中的例子,比如你突然想世界那么大你想去看看,但是处于现实的你还不能丢了工作,得到请假的OA申请,请假天数如果是半天到1天,可能直接主管批准即可;如果是1到3天的假期,需要部门经理批准;如果是3天到30天,则需要总经理审批;大于30天,正常不会批准。这种简单的流程即可试用于我们当前业务场景。

▐ 实践经验

业务流程很简单:

  • 打电话注销信用卡

  • 工作人员注销信用卡

注销信用卡有个背景是这样的,如果信用卡存在账单未还清,存在溢出款,存在特殊年费未使用等情况是不允许注销信用卡的,鉴于此,我们在注销之前加了一套是否允许注销的检验逻辑。

大体如下:

  • 是否存在账单未还清,比如有已出账单未还清,有未出账单未还清,有年费管理费等未还清等。

  • 是否存在溢出款多余的钱。

  • 是否存在高额积分未使用,需用户确认放弃积分等。

针对这几类情况建立了三类过滤器,分别是:

  • UserLogoutUnpaidBillsLimitFilter:是否存在未还清金额。

  • UserLogoutOverflowLimitFilter:是否存在溢出款。

  • UserLogoutGiveUpPointsLimitFilter:是否放弃高额金额。

判断逻辑是先通过UserLogoutUnpaidBillsLimitFilter判断当前用户是否可以注销信用卡。如果允许继续由 UserLogoutOverflowLimitFilter 判断是否存在溢出款,是否可以注销信用卡;如果没有溢出款继续由UserLogoutGiveUpPointsLimitFilter 判断当前用户是否存在高额积分,前面三条判断,只要有一个不满足就提前返回。

public boolean canLogout(String userId) {
        //获取用户信息
        UserInfo userInfo = getUserInfo(userId);

        // 构造注销信用卡限制过滤器链条
        LogoutLimitFilterChain filterChain = new LogoutLimitFilterChain();
        filterChain.addFilter(new UserLogoutUnpaidBillsLimitFilter());
        filterChain.addFilter(new UserLogoutOverflowLimitFilter());
        filterChain.addFilter(new UserLogoutGiveUpPointsLimitFilter());
        boolean checkResult = filterChain.doFilter(filterChain, userInfo);

        //filterChain.doFilter方法
        public boolean doFilter (LogoutLimitFilterChain filterChain, UserInfo userInfo){
            //迭代调用过滤器
            if (index < filters.size()) {
                return filters.get(index++).doFilter(filterChain, userInfo);
            }
        }
    }

    //UserLogoutUnpaidBillsLimitFilter.doFilter方法
    public boolean doFilter(LogoutLimitFilterChain filterChain, UserInfo userInfo) {
        //获取用户当前欠款金额
        UserCardBillInfo userCardBillInfo = findUserCardBillInfo(userInfo);

        // 判断当前卡用户是否允许消费
        if (userCardBillInfo != null) {
            if ((!CAN_LOGOUT.equals(userCardBillInfo.getEnabledLogout()))) {
                return false;
            }
        }
        //其余情况,继续往后传递
        return filterChain.doFilter(filterChain, memberInfo, consumeConfig);
    }

    //UserLogoutOverflowLimitFilter.doFilter方法
    public boolean doFilter(LogoutLimitFilterChain filterChain, UserInfo userInfo) {
        //判断用户是否存在溢出款
        UserCardDeposit userCardDeposit = findUserCardDeposit(userInfo);

        // 判断当前卡用户是否允许消费
        if (userCardDeposit != null) {
            if (userCardDeposit.getDeposit() != 0) {
                return false;
            }
        }
        //其余情况,继续往后传递
        return filterChain.doFilter(filterChain, memberInfo, consumeConfig);
    }

总结:将每种限制条件的判断逻辑封装到了具体的 Filter 中,如果某种限制条件的逻辑有修改不会影响其他条件,如果需要新加限制条件只需要重新构造一个 Filter 织入到 FilterChain 上即可。

责任链中一个处理者对象,其中只有两个行为,一是处理请求,二是将请求转送给下一个节点,不允许某个处理者对象在处理了请求后又将请求转送给上一个节点的情况。对于一条责任链来说,一个请求最终只有两种情况,一是被某个处理对象所处理,另一个是所有对象均未对其处理,前一种情况称该责任链为纯的责任链,对于后一种情况称为不纯的责任链,实际应用中,多为不纯的责任链。整理了一份562页设计模式PDF文档

策略设计模式

▐ 模式定义

策略这个词应该怎么理解,打个比方说,我们出门的时候会选择不同的出行方式,比如骑自行车、坐公交、坐火车、坐飞机等等,这些出行方式,每一种都是一个策略。

再比如我们去逛商场,商场现在正在搞活动,有打折的、有满减的、有返利的等等,其实不管商场如何进行促销,说到底都是一些算法,这些算法本身只是一种策略,并且这些算法是随时都可能互相替换的,比如针对同一件商品,今天打八折、明天满100减30,这些策略间是可以互换的。

策略模式(Strategy Pattern)是定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。

▐ 适用场景

主要是为了消除大量的 if else 代码,将每种判断背后的算法逻辑提取到具体的策略对象中,当算法逻辑修改时对使用者无感知,只需要修改策略对象内部逻辑即可。这类策略对象一般都实现了某个共同的接口,可以达到互换的目的。

  • 多个类只有算法或行为上稍有不同的场景

  • 算法需要自由切换的场景

  • 需要屏蔽算法规则的场景

▐ 实践经验

业务流程很简单:

  • 挑选商品

  • 选择不同的优惠方式结账

比如即将到来的双十一活动某些线下商家举办活动,折扣力度如下满300-80,部分商品5折,根据不同会员等级享受不同的折扣最低7折,周年庆活动可享8折等等。假如这些活动折扣不可同享,那么如何去实现以及考虑可扩展性的话策略模式是一种不错的选择。

/**
 * 抽象折扣策略接口
 */

public abstract class DiscountStrategy {
  /**
   * 计算折扣后的价格
    * @param price      原价
   * @return           折扣后的价格
   */
  public abstract CalculationResult getDiscountPrice(Long userId ,BigDecimal price);
}

/**
 * 满减活动 -- 满300减80
 */
public class FullReductionStrategyOne extends DiscountStrategy {
    /**
     * 计算折扣后的价格
     * @param price      原价
     * @return
     */
    @Override
    public CalculationResult getDiscountPrice(Long userId ,BigDecimal price) {
        if (price.doubleValue() < 300) {
            return price;
        }
        BigDecimal dealPrice= price.subtract(BigDecimal.valueOf(80));
        return getCalculationResult(userId,dealPrice);
    }
}

/**
 * 部分商品5折
 */
public class MerchandiseDiscountStrategy extends DiscountStrategy {
    /**
     * 计算折扣后的价格
     * @param price      原价
     * @return
     */
    @Override
    public CalculationResult getDiscountPrice(BigDecimal price) {
        BigDecimal dealPrice=  price.multiply(BigDecimal.valueOf(0.5));
        return getCalculationResult(userId,dealPrice);
    }
}

/**
*当有新的需求式,我们只需要添加一个新的接口即可,不需要修改原有的具体策略实现代码即可完成。
*定义完策略后,我们再定义一个”环境角色”,假设我们这个环境角色就使用价格对象吧
*/

public class Price {

    private DiscountStrategy discountStrategy;

    /**
     * 定义一个无参构造,用于实例对象
     */
    private Price(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    /**
     * 获取折扣后的价格
     *
     * @param price 原始价格
     * @return
     */
    public CalculationResult discount(Long userId,BigDecimal price) {
        return discountStrategy.getDiscountPrice(userId ,price);
    }
}

策略模式是一种行为型模式,将算法的使用和算法本身分割开,委派给不同的对象管理。策略实现类一般是封装好的轻量级的算法,当客户端(调用方)遇到不同的情况时,这些算法可以根据需要动态地去互相替换。策略的选择完全是由客户端进行的,这就要求客户端必须理解所有的策略实现类,虽然提高了系统的灵活性,但也增加了客户端的使用难度。策略模式体现了开闭原则——“对扩展开放,对修改关闭”。新的策略增加时,不会影响其他类的修改,增加了扩展性,对扩展开放;只依赖于抽象,而不依赖于具体实现,所以对修改是关闭的。这样在提高代码扩展性的同时,也降低了耦合。

总结:将每种通道的推送逻辑封装到了具体的策略中,某种策略的变更不会影响其他策略,由于实现了共同接口,所以策略可以互相替换,对使用者友好。比如 Java ThreadPoolExecutor 中的任务拒绝策略,当线程池已经饱和的时候会执行拒绝策略,具体的拒绝逻辑被封装到了 RejectedExecutionHandler 的 rejectedExecution 中。

模板设计模式

▐ 模式定义

模板的价值就在于骨架的定义,骨架内部将问题处理的流程已经定义好,通用的处理逻辑一般由父类实现,个性化的处理逻辑由子类实现。

比如炒土豆丝和炒麻婆豆腐,大体逻辑都是:

1、切菜

2、放油

3、炒菜

4、出锅

1,2,4 都差不多,但是第 3 步是不一样的,炒土豆丝得拿铲子翻炒,但是炒麻婆豆腐得拿勺子轻推,否则豆腐会烂。

▐ 使用场景

不同场景的处理流程,部分逻辑是通用的,可以放到父类中作为通用实现,部分逻辑是个性化的,需要子类去个性实现。

模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

▐ 实践经验

还是接着之前商品折扣的例子来说,后期我们新加了两个需求:

  • 用户享受不同折扣增加 trace。

  • 用户享受折扣后是否升级会员等级。

所以现在的流程变成了这样:

1、trace 开始。

2、计算用户不同折扣力度。

3、是否允许升级会员等级,如果允许执行升级会员等级逻辑。

4、trace 结束。

其中 1 和 4 是通用的,2 和 3 是个性化的,鉴于此可以在折扣策略之前增加了一层父类的策略,将通用逻辑放到了父类中。

修改后的代码如下:

abstract class AbstractDiscountStrategy implements DiscountStrategy{

    @Override
    public CalculationResult getDiscountPrice(Long userId ,BigDecimal price) {

        //1.构造span
        Span span = buildSpan();
        //2.具体通道推送逻辑由子类实现
        CalculationResult  calculationResult =getCalculationResult(userId,price);

        //3.是否允许升级会员等级,如果允许执行升级逻辑
        if(!calculationResult.isSuccess() && canUpgrade()){
            upgradeLevel(userId,calculationResult);
        }

        //4.trace结束
        span.finish();
        return calculationResult;
    } 

    //具体推送逻辑由子类实现
    protected abstract CalculationResult getCalculationResult(Long userId,BigDecimal price) ;

    //是否允许升级会员等级由子类实现
    protected abstract boolean canUpgrade(Long userId,CallResult callResult);

}

/**
 * 满减活动 -- 满300减80
 */
public class FullReductionStrategyOne extends AbstractDiscountStrategy {
    @Override
    protectedCalculationResult getCalculationResult(Long userId,BigDecimal price){
        //执行折扣逻辑
    }

    @Override
    protected  boolean canUpgrade(Long userId,CallResult callResult){
        return false
    }
}

观察者设计模式

▐ 模式定义

拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价 这种模式就可以使用观察者模式。顾名思义,此模式需要有观察者(Observer)和被观察者(Observable)两类角色。当 Observable 状态变化时会通知 Observer,Observer 一般会实现一类通用的接口。

比如 java.util.Observer,Observable 需要通知 Observer 时,逐个调用 Observer 的 update 方法即可,Observer 的处理成功与否不应该影响 Observable 的流程。

▐ 使用场景

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

一个对象(Observable)状态改变需要通知其他对象,Observer 的存在不影响 Observable 的处理结果,Observer 的增删对 Observable 无感知。

比如 Kafka 的消息订阅,Producer 发送一条消息到 Topic,至于是 1 个还是 10 个 Consumer 订阅这个 Topic,Producer 是不需要关注的。

▐ 实践经验

在责任链设计模式那块我通过三个 Filter 解决了注销信用卡限制检验的问题,其中有一个 Filter 是用来检验用户积分的,我这里只是读取用户的积分总额和次数,那么消费次数获得积分的累加是怎么完成的呢?

其实累加这块就用到了观察者模式,具体来讲是这样,当交易系统收到支付成功回调时会通过 Spring 的事件机制发布“支付成功事件”。

这样负责积分消费次数累加和负责语音播报的订阅者就会收到“支付成功事件”,进而做各自的业务逻辑。

画个简单的图描述一下:

/**
支付回调处理者
*/
PayCallBackController implements ApplicationContextAware {
     private ApplicationContext applicationContext;

    //如果想获取applicationContext需要实现ApplicationContextAware接口,Spring容器会回调setApplicationContext方法将applicationContext注入进来
    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.applicationContext = applicationContext;
    }

     @RequestMapping(value = "/pay/callback.do")
     public View callback(HttpServletRequest request){
        if(paySuccess(request){
            //构造支付成功事件
            PaySuccessEvent event = buildPaySuccessEvent(...);
            //通过applicationContext发布事件,从而达到通知观察者的目的
            this.applicationContext.publishEvent(event);
        }
    }
}
/**
 * 语音播报处理者
 *
 */
public class VoiceBroadcastHandler implements ApplicationListener<PaySuccessEvent>{
    @Override
    public void onApplicationEvent(PaySuccessEvent event) {
        //语音播报逻辑
    }
}

//其他处理者的逻辑类似

总结:观察者模式将被观察者和观察者之间做了解耦,观察者存在与否不会影响被观察者的现有逻辑。

装饰器设计模式

▐ 模式定义

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

装饰器用来包装原有的类,在对使用者透明的情况下做功能的增强,比如 Java 中的 BufferedInputStream 可以对其包装的 InputStream 做增强,从而提供缓冲功能。

▐ 使用场景

在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。 当不能采用继承的方式对系统进行扩展或者继承。希望对原有类的功能做增强,但又不希望增加过多子类时,可以使用装饰器模式来达到同样的效果。

▐ 实践经验

有一个咖啡店,销售各种各样的咖啡,拿铁,卡布奇洛,蓝山咖啡等,在冲泡前,会询问顾客是否要加糖,加奶,加薄荷等。这样不同的咖啡配上不同的调料就会卖出不同的价格。

/**
* 抽象类Coffee
*/
public abstract class Coffee {
    /**
     * 获取咖啡得名字
     */
    public abstract String getName();

    /**
     * 获取咖啡的价格
     */
    public abstract double getPrice();
}

/**
*利用继承和组合的结合,现在我们可以考虑设计出一个装饰类,它也继承自coffee,
*并且它内部有一个coffee的实例对象
*/
public abstract class CoffeeDecorator implements Coffee {
    private Coffee delegate;

    public CoffeeDecorator(Coffee coffee) {
        this.delegate = coffee;
    }

    @Override
    public String getName() {
        return delegate.getName();
    }

    @Override
    public double getPrice() {
        return delegate.getPrice();
    }
}

/**
*牛奶咖啡的装饰者模式的案例
*/
public class MilkCoffeeDecorator extends CoffeeDecorator {
    public MilkCoffeeDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getName() {
        return "牛奶, " + super.getName();
    }

    @Override
    public double getPrice() {
        return 1.1 + super.getPrice();
    }
}

//其他咖啡的模式类似

/**
*测试案例 可以通过加入不用内容 咖啡名称和价格都是不同的
*/
public class App {
    public static void main(String[] args) {
        // 得到一杯原始的蓝山咖啡
        Coffee blueCoffee = new BlueCoffee();
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 加入牛奶
        blueCoffee = new MilkCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 再加入薄荷
        blueCoffee = new MintCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());

        // 再加入糖
        blueCoffee = new SugarCoffeeDecorator(blueCoffee);
        System.out.println(blueCoffee.getName() + ": " + blueCoffee.getPrice());
    }
}

总结:使用装饰器模式做了功能的增强,对使用者来说只需要做简单的组合就能继续使用原功能。装饰器模式充分展示了组合的灵活。利用它来实现扩展。它同时也是开闭原则的体现。如果相对某个类实现运行时功能动态的扩展。这个时候你就可以考虑使用装饰者模式!

桥接设计模式

▐ 模式定义

桥接模式是一种结构型设计模式, 可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而能在开发时分别使用。

桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。

▐ 使用场景

如果一个系统需要在抽象类和具体类之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。

抽象部分和实现部分可以以继承的方式独立扩展而互不影响,在程序运行时可以动态的将一个抽象类子类的对象和一个实现类子类的对象进行组合,及系统需要对抽象类角色和实现类角色进行动态耦合。

一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。

对于那些不希望使用继承或因为多层继承导致系统的个数急剧增加的系统,桥接模式尤为适用。

▐ 实践经验

性能管理系统中,数据产生后需要经过采集,汇聚,入库三个流程,用户才能查询使用。采集可以是snmp采集,也可以是ems采集;汇聚可以使storm汇聚,也可以是spark汇聚;入库可以是hdfs入库,也可以是mppdb入库。针对不同场景,我们可以灵活选择不同的采集,汇聚,入库方式。这种一个功能需要多种服务支持,每种服务又有不同类型的实现,使用桥接模式再适合不过。

桥接模式,顾名思义,就是把每种服务看做一座桥,我们可以根据实际场景选择不同的桥。上述例子表示数据产生到可以使用之前需要经过三座桥:采集桥->汇聚桥->入库桥。每座桥可以选择不同的构造。整理了一份562页设计模式PDF文档

/**
 *
 * 采集桥采集服务
 *
 */
public abstract class CollectionService
{
    abstract void execute();
    public void run()
{
        execute();
    }
}

/**
 *  汇聚桥  汇聚服务
 *
 */
public abstract class AggregationService
{
    public void run()
{
        if(null != collectionService)
        {
            collectionService.run();
        }
        execute();
    }

    abstract void execute();
    CollectionService collectionService;
    public AggregationService(CollectionService collectionService)
{
        this.collectionService = collectionService;
    }
}

/**
 *
 * 入库桥   入库服务
 *
 */
public abstract class StoreService
{
    public void run()
{
        if(null != aggregationService)
        {
            aggregationService.run();
        }
        execute();
    }

    abstract void execute();
    AggregationService aggregationService;
    public StoreService(AggregationService aggregationService)
{
        this.aggregationService = aggregationService;
    }
}

/**
*
* EMS采集桥
*
*/

public class EMSCollectionService extends CollectionService
{
    @Override
    void execute()
{
        System.out.println("EMS collection.");
    }
}

/**
*
* SNMP采集桥
*
*/
public class SNMPCollectionService extends CollectionService
{
    @Override
    void execute()
{
        System.out.println("SNMP collection.");
    }
}

/**
*
*  Storm汇聚桥
*
*/
public class StormAggregationService extends AggregationService
{
    public StormAggregationService(CollectionService collectionService)
{
        super(collectionService);
    }

    @Override
    void execute()
{
        System.out.println("Storm aggregation.");
    }
}

/**
*
*  Spark汇聚桥
*
*/
public class SparkAggregationService extends AggregationService
{
    public SparkAggregationService(CollectionService collectionService)
{
        super(collectionService);
    }

    @Override
    void execute()
{
        System.out.println("Spark aggregation.");
    }
}

/**
*
*  MPPDB汇聚桥
*
*/
public class MPPDBStoreService extends StoreService
{
    public MPPDBStoreService(AggregationService aggregationService)
{
        super(aggregationService);
    }

    @Override
    void execute()
{
        System.out.println("MPPDB store.");
    }
}

/**
*
*  HDFS汇聚桥
*
*/
public class HDFSStoreService extends StoreService
{
    public HDFSStoreService(AggregationService aggregationService)
{
        super(aggregationService);
    }

    @Override
    void execute()
{
        System.out.println("HDFS store.");
    }
}

/**
 *
 * 类功能说明:   桥接模式测试
 */
public class BridgeTest
{
    public static void main(String[] args)
{
        CollectionService snmpService = new SNMPCollectionService();
        AggregationService stormService = new StormAggregationService(snmpService);
        StoreService hdfsService = new HDFSStoreService(stormService);
        hdfsService.run();
    }
}

总结:桥接模式可以将系统中稳定的部分和可扩展的部分解耦,使得系统更加容易扩展,且满足OCP原则,对调用者修改关闭。

**作者:阿里巴巴淘系技术官方
***my.oschina.net/u/4662964/b…

(0)

相关推荐