设计模式串讲

我们常常对未知事物无感,因为我们看不到它们,所以它们对我们来说可有可无。但是话说回来,凡是躲避掉的,未来某一天我们总是会为此默默付出代价而不自知,不仅浪费时间还消耗了情绪。不使用设计模式就是其中的一种,we will pay for it,那么学习设计模式有什么好处呢?

作为一个开发,我们每天的日常不是在写代码的路上就是在修改代码的路上。当接到一个巨复杂的任务时,这时候,你会将所有的逻辑都写在一个方法内或者是一个文件里面吗?答案是no。首先我们想到的应该是怎么划分模块,层次和类。每个类要有哪些属性、方法,类之间如何交互,该用继承还是组合,该使用接口还是抽象类,怎样做到解耦、高内聚低耦合等等这些问题。而设计模式就是回答这些问题而被总结出来的一套标准答案,学好设计模式可以让我们告别烂代码,提高复杂代码的设计和开发的能力。

面向对象与面向过程

在说设计模式之前我们看一下面向对象和面向过程的区别。面向对象编程是以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石。而面向过程编程以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。

面向对象比面向过程的优势在哪里呢?

  1. 面向对象更能应对大规模复杂程序的开发,面向对象中类的概念能有效组织起业务内的各个关联方,和复杂的网状流程更加贴近,面向过程通过单一地拼接方法和数据的方式模拟程序流程反而比较吃力。

  2. 面向对象风格的代码更易复用、易扩展、易维护。面向对象支持封装、抽象、继承和多态四大特性如下:

    封装:使用类将属性和行为封装并加以控制访问权限,只允许外部调用者通过类暴露的有限方法访问数据;

    抽象:基于接口的抽象,在不改变原有实现的情况下,轻松替换新的实现逻辑;

    继承:将多个类中重复的代码抽取到父类中,避免了代码重复写多遍;

    多态:不同的类对象可以传递给相同的方法,在运行时时执行不同的代码逻辑;

  3. 面向对象语言更加人性化、更加高级、更加智能。

讲了面向对象这么多好处,那这和设计模式有什么关系呢?设计模式就是建立在面向对象之上的,依赖于面向对象的特性,解决一些划分和交互的问题。

设计模式

我们知道事情都是从简单到复杂,从复杂到失控 。为了维持平衡,那复杂就一定会失控吗?我们都知道大事化小,小事化了嘛,在面对复杂问题时,可以将大问题进行分解成小问题,然后将小问题独个解决,最后大问题也就解决了。而设计模式就像构成复杂程序的精细零件,随着各个部分有效地组织和联结,最后整个程序能够高效和稳定地运转。

编程是现实生活的模拟,复杂程序的类和类关系拼接起来就像是一个图。类本身并不复杂,而对象之间的关系才是最复杂。类可以自底向上去继承类和实现接口组合,正因为这样它才能很好的模拟现实生活中各种复杂的关系,而设计模式就是帮我们绘制现实的工具。

那任何情况下都使用设计模式有没有问题呢?如果程序简单到可以在几个步骤内完成,使用设计模式反而会南辕北辙,散失代码的可读性,引入不必要地复杂度,所以要视实际情况而定。常用的设计模式分为创建型、结构型和行为型,具体划分如下表:

类型 设计模式
创建型 工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
结构型模式 适配器模式、装饰者模式、代理模式、外观模式、桥接模式、组合模式、享元模式
行为型
策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

这三类大概描述了对象如何产生,对象间的结构以及对象间的联系,那怎样才能将它们应用到日常的开发中呢?

举例需求

某公司开发了一个网上商城应用,项目初期想通过发优惠券来提高会员的活跃度。系统的用户分为VIP、非VIP和新注册的用户三类,发的券分为满减券、兑换券和积分券。下表是不同会员的发券逻辑:

会员类型 优惠券
普通会员
积分券
VIP会员 满减券 > 积分券
新注册会员 满减券 > 兑换券 > 积分券

发券场景的需求可以总结为以下四个方面:

  1. 不同的会员需要发放不同的优惠券,并且对每类会员发放的优惠券有重叠的部分;
  2. 发券前需要对用户资格进行检查,看会员是否符合发券的条件;
  3. 给会员发完券后,要求发送消息到消息队列供其他系统监听处理;
  4. 部分兑换券的优先级比较高,会在未来的几天内过期,发券后需要给会员发送短信通知;

假如让你来实现这个功能,你会怎么实现呢?如果按照面向过程的编码风格,我们很自然写出下面的程序:

if("notVip" == user.type) {
    releaseNotVip(user);
} else if ("vip" == user.type) {
    releaseVip(user);
} else if("newUser" == user.type) {
    releaseNewUser(user);
}
/*普通用户*/
void releaseNotVip(User user) {
    releaseScoreCoupon(user);
}
/*VIP用户*/
void releaseVip(User user) {
    releaseScoreCoupon(user);
    releaseFrCoupon(user);
}
/*新用户*/
void releaseNewUser(User user) {
    releaseScoreCoupon(user);
    releaseFrCoupon(user);
    releaseExCoupon(user);
}
//发放优惠券方法
void releaseScoreCoupon(User user){...}//发送积分券
void releaseFrCoupon(User user){...}//发送满减券
void releaseExCoupon(User user){...}//发送兑换券

代码清单3-0 面向过程代码

如果发券程序只需运行一次,业务关系单一,发券步骤简单的情况下,使用以上方法并不会付出很大的代价。但是如果后续系统围绕发券流程频繁变更时,以上代码修改总是影响之前写好的代码,变化随处传导,这样无论对开发的人还是对系统稳定都是不小的问题。加上时间赶项目急,不仅写完之后提心吊胆,而且一不小心就会酿成生产事故,造成损失。那怎么写才能让自己不用开发上线后出现问题呢?首先我们要隔离变化,面向扩展开放,面对修改关闭。对于应用来讲,就是下一次修改发券逻辑这段代码的时候,只需要增加额外的逻辑,并不会影响原来的操作流程。下面通过一个虚拟的场景尽可能多地使用设计模式来说明如何应用设计模式。

设计模式实现

发券场景中名词有“会员”、“优惠券”以及“发券行为”,首先将它们抽象成类,并且辨识每个类中的属性和行为。由于会员和优惠券比较简单,直接将属性封装成类就可以。而发券行为有三种,分别为发积分券、发兑换券和发满减券,因此可以实现一个发券抽象类,在子类中覆盖发券抽象类的发券方法。这样可以实现发券逻辑的有效隔离,自然避免了上述说到牵一处而动全身的尴尬情况。

上面说到为每种发券类型单独建一个子类的实现,然而实际的发券行为实例化有可能更加复杂,所以将示例化发券对象放在工厂中就很自然和必要,以下使用工厂模式管理创建发券类并提供方法获取发券行为列表:

public class ProcessorFactory {

    static Map<String, List<Processor>> map = new HashMap<>();

    static {
        FrProcessor frProcessor = new FrProcessor();
        EnhanceExProcessor enhanceExProcessor = new EnhanceExProcessor();
        ScoreProcessor scoreProcessor = new ScoreProcessor();

        //责任链
        List<Processor> newUserProcessors = Arrays.asList(frProcessor, enhanceExProcessor, scoreProcessor);
        List<Processor> vipUserProcessors = Arrays.asList(frProcessor, scoreProcessor);
        List<Processor> normalUserProcessors = Arrays.asList(scoreProcessor);

        //使用MAP实现
        map.put("PU", normalUserProcessors);
        map.put("VU", vipUserProcessors);
        map.put("NU", newUserProcessors);
    }

    public static List<Processor> getProcessor(String userType) {
        return map.get(userType);
    }
}

代码清单3-1 发放工厂类(工厂模式)

另外,由于需要对用户资格进行检查和发券后消息通知,也就是说发券操作有前置操作也有后置操作,适合使用模板模式,把前置操作和后置操作在父类中实现即可。以下为整个发券类图关系,其中Processor 为发券抽象类,ExProcessor 为发兑换券实现类,ScoreProcessor 为发积分券实现类,FrProcessor为发满减券实现类:

图1 发放券类图

发券抽象类代码实现如下:

public abstract class Processor {

    public void check(SendRequest sendRequest) {
        System.out.println(String.format("检查用户 %s 是否合法", sendRequest.getUserId()));
    }

    abstract SendRst execute(SendRequest sendRequest);

    private void sendMsgToMQ(SendRst sendRst) {
        String msg = String.format("[%s]将发放的消息通知给MQ: ", sendRst.getCoupon().getName()) + sendRst;
        MessageSender.send(
                //建造者模式
                SendMsgReq.builder()
                        .userId(sendRst.getUserId())
                        .msgBody(msg)
                        .sendType(SendType.MQ)
                        .build()
        );
    }

    public void apply(SendRequest sendRequest) {
        check(sendRequest);
        //添加一条记录
        SendRst sendRst = execute(sendRequest);
        sendMsgToMQ(sendRst);
    }

    public SendRst newSendRst(SendRequest sendRequest) {
        SendRst sendRst = new SendRst();
        sendRst.setUserId(sendRequest.getUserId());
        sendRst.setLineId(UUID.randomUUID() + "");
        return sendRst;

    }
}

代码清单3-2 发券抽象类(模板模式)

继承发券抽象类的三种实现类代码如下:

//发积分券实现类
public class ScoreProcessor extends Processor {
    @Override
    SendRst execute(SendRequest sendRequest) {
        Coupon coupon = DB.findCouponByType(CouponType.SC);
        System.out.println(String.format("[%s]发送积分给 ", coupon.getName()) + sendRequest.getUserId() + "用户");

        SendRst sendRst = newSendRst(sendRequest);
        sendRst.setCoupon(coupon);

        //保存发放记录
        CouponLine couponLine = new CouponLine();
        couponLine.setCouponCode(coupon.getCode());
        couponLine.setCouponType(coupon.getType());
        couponLine.setId(sendRst.getLineId());
        DB.saveCouponLine(sendRequest.getUserId(), couponLine);

        sendRst.setSendResult(true);


        return sendRst;
    }
}

//发兑换券实现类
public class ExProcessor extends Processor {

    @Override
    public void check(SendRequest sendRequest) {
        System.out.println(String.format("[兑换券]特殊检查用户 %s 是否合法", sendRequest.getUserId()));
    }

    @Override
    SendRst execute(SendRequest sendRequest) {
        Coupon coupon = DB.findCouponByType(CouponType.EX);
        System.out.println(String.format("[%s] 发送兑换券给 ", coupon.getName()) + sendRequest.getUserId() + "用户");
        SendRst sendRst = newSendRst(sendRequest);

        if (isEnough(coupon)) {//检查数量是否足够
            sendRst.setSendResult(true);
            coupon.setCount(coupon.getCount() - 1);
            DB.update(coupon);
            //保存发放记录
            CouponLine couponLine = new CouponLine();
            couponLine.setCouponCode(coupon.getCode());
            couponLine.setCouponType(coupon.getType());
            couponLine.setId(sendRst.getLineId());
            DB.saveCouponLine(sendRequest.getUserId(), couponLine);
        }

        sendRst.setCoupon(coupon);
        return sendRst;
    }

    private boolean isEnough(Coupon coupon) {
        return coupon.getCount() > 0;
    }
}

//发兑换券实现类
public class ExProcessor extends Processor {

    @Override
    public void check(SendRequest sendRequest) {
        String msg = String.format(
            "[兑换券]特殊检查用户 %s 是否合法", sendRequest.getUserId())
        System.out.println(msg);
    }

    @Override
    SendRst execute(SendRequest sendRequest) {
        Coupon coupon = DB.findCouponByType(CouponType.EX);
        String msg = String.format(
            "[%s] 发送兑换券给 ", coupon.getName()) + sendRequest.getUserId() + "用户";
        System.out.println(msg);
        SendRst sendRst = newSendRst(sendRequest);

        if (isEnough(coupon)) {//检查数量是否足够
            sendRst.setSendResult(true);
            coupon.setCount(coupon.getCount() - 1);
            DB.update(coupon);
            //保存发放记录
            CouponLine couponLine = new CouponLine();
            couponLine.setCouponCode(coupon.getCode());
            couponLine.setCouponType(coupon.getType());
            couponLine.setId(sendRst.getLineId());
            DB.saveCouponLine(sendRequest.getUserId(), couponLine);
        }

        sendRst.setCoupon(coupon);
        return sendRst;
    }

    private boolean isEnough(Coupon coupon) {
        return coupon.getCount() > 0;
    }
}

代码清单3-3 发券实现类

对于优先级高的兑换券需要发送短信,而发送短信属于增强功能的范畴,故使用装饰器模式:

//发兑换券实现增强(装饰器模式)
public class EnhanceExProcessor extends ExProcessor {
    SendRst execute(SendRequest sendRequest) {
        SendRst sendRst = super.execute(sendRequest);
        sendSmsMsg(sendRst);
        return sendRst;
    }

    //发送短信
    private void sendSmsMsg(SendRst sendRst) {
        System.out.println(String.format("[%s]发送短信给 ", sendRst.getCoupon().getName()) + sendRst.getUserId() + "用户");
    }
}

代码清单3-4 发兑换券增强版本(装饰器模式)

因发券过程种发送信息可以是发送短信或发送消息队列,为此我们写一个消息的发送接口,在子实现类中实现短信或消息队列的发送,类图如下:

图2 发送消息类图

以下是发送消息中各个类的代码实现:

//消息发送接口
public interface Sender {
    void send(SendMsgReq sendMsgReq);
}
//发送消息队列
public class MqSender implements Sender {
    @Override
    public void send(SendMsgReq sendMsgReq) {
        System.out.println("发送消息到消息队列:"  + sendMsgReq);
    }
}
//发送短信
public class SmsSender implements Sender {
    @Override
    public void send(SendMsgReq sendMsgReq) {
        System.out.println("发送短信消息:" + sendMsgReq);
    }
}

代码清单3-5 发送消息接口及实现

有了上面三个类后,我们在消息发送类MessageSender中实现一个发送消息的方法,根据请求对象中请求类型的不同获取不同的消息发送子类:

public class MessageSender {

    public static Map<SendType, Sender> senderMap = new HashMap<>();

    static {
        senderMap.put(SendType.SMS, new SmsSender());
        senderMap.put(SendType.MQ, new MqSender());
    }

    public static void send(SendMsgReq sendMsgReq) {
        assert Objects.nonNull(sendMsgReq.getSendType());

        //获取发送给的策略
        Sender sender = senderMap.get(sendMsgReq.getSendType());
        if(sender == null) return;

        //得到发送实现类后,将消息发送出去
        sender.send(sendMsgReq);
    }
}

代码清单3-6 发送消息实现(策略模式)

以上就是使用设计模式实现一个简单发券程序的过程。在上诉场景中我们用到多个设计模式,包括不限于工厂模式(创建发券类)、责任链模式(实现链式发放券)、装饰器模式(高优先级兑换券发送短信通知)、模板模式(发券资格检查及后置消息通知)以及策略模式(发送短信或消息队列),麻雀虽小五脏俱全,这些思想完全可以迁移到其他场景中灵活使用。

总结

编码编久了就会觉得写代码和写文章并没有什么不同,条条大路通罗马,为了更快地到达终点,设计模式为我们指明了方向缩短了距离,使我们面对选择的时候有了参考。

文章中的代码可以从这里下载测试。

PS:关于设计模式理论部分参考极客时间王争老师的<<设计模式>>加以修改和总结,如有问题请多多指正,谢谢!

posted @ 2022-08-06 19:05  SJ12217  阅读(110)  评论(0)    收藏  举报