读书笔记 - 重学Java设计模式

发布于:2025-02-26 ⋅ 阅读:(21) ⋅ 点赞:(0)

读书笔记 - 重学Java设计模式

第1章 设计模式介绍

注意:设计模式讲求的是思想,而不是固定的实现方式。设计模式是一种开发设计指导思想,每一种设计模式都是解决某一类问题的概念模型,所以在实际的使用过程中,不要拘泥于某种已经存在的固定代码格式,而要根据实际的业务场景做出改变。

有一部分开发人员虽然没有阅读过设计模式的相关书籍和资料,但依旧可以编写出优秀的代码。经过众多项目的锤炼,个人可以提炼出心得体会,而这些体会可能会与设计模式的理念一致,即同样要求高内聚、低耦合、可扩展和可复用。

即使有这样差异化的实现方式,也可以把设计模式按照其实现形式归为三类:
· 创建型模式:提供创建对象的机制,提升已有代码的灵活性和可复用性。
· 结构型模式:介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。
· 行为模式:负责对象间的高效沟通和职责传递委派。

该如何学习设计模式,很多人没有学会或领会设计模式,正是因为看理论书籍的学习过程是在别人总结的经验上倒推实现方案得来的,没有做到融会贯通。就像即使知道汽车是怎么开的,但如果没驾驶过几千公里,司机能记住的也只是理论,上路后依然会手忙脚乱!

第2章 六大设计原则

单一职责原则定义

单一职责原则(Single Responsibility Principle,SRP)又称单一功能原则,是面向对象的五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因,该原则由罗伯特·C.马丁(Robert C.Martin)在《敏捷软件开发:原则、模式与实践》一书中提出。

所谓的职责就是指类变化的原因,也就是业务需求。如果一个类有多于一个的原因被改变,那么这个类就有超过两个及以上的职责。而单一职责约定一个类应该有且仅有一个改变类的原因。

定义一个接口,多个实现类实现,这种一般就可以解耦实现单一原则了。

开闭原则定义

一般认为最早提出开闭原则(Open-Close Principle,OCP)的是伯特兰·迈耶。他在1988 年发表的《面向对象软件构造》中给出的。在面向对象编程领域中,开闭原则规定软件中的对象、类、模块和函数对扩展应该是开放的,但对于修改是封闭的。

特点是父类或接口定义的一个变量或方法、类是无法被修改的,比如:private final static double π = 3.14D;

里氏替换原则定义

里氏替换原则(Liskov Substitution Principle,LSP)是由麻省理工学院计算机科学系教授芭芭拉·利斯科夫(BarbaraLiskov)于 1987 年在“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstractionand Hierarchy
)里提出的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立。

特点是直接复用父类定义的方法。

迪米特法则定义

1987年秋天,迪米特法则由美国Northeastern University的Ian Holland提出,被UML的创始者之一Booch等人普及。后来,因为经典著作The Pragmatic Programmer
而广为人知。
迪米特法则(Law of Demeter,LoD)又称为最少知道原则(Least Knowledge Principle,LKP),是指一个对象类对于其他对象类来说,知道得越少越好。也就是说,两个类之间不要有过多的耦合关系,保持最少关联性。

不该让校长直接管理学生,校长应该管理老师,由老师提供相应的学生信息查询服务。

接口隔离原则定义

《代码整洁之道》的作者Robert C.Martin于2002年给“接口隔离原则”的定义是:客户端不应该被迫依赖于它不使用的方法(Clients should not be forced to depend on methodsthey do not use)。该原则还有另外一个定义:一个类对另一个类的依赖应该建立在最小的接口上(The dependency ofone class to another one should depend on the smallestpossible interface)。
接口隔离是为了高内聚、低耦合。

依赖倒置原则定义

依赖倒置原则是Robert C.Martin于1996年在C++Report上发表的文章中提出的。

依赖倒置原则(Dependence Inversion Principle,DIP)是指在设计代码架构时,高层模块不应该依赖于底层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒置原则是实现开闭原则的重要途径之一,它降低了类之间的耦合,提高了系统的稳定性和可维护性,同时这样的代码一般更易读,且便于传承。

第3章 设计模式如何落地

有人觉得使用设计模式会浪费时间,最终使用一个类加几十行的if…else编写代码。虽然这样可以快速地实现业务逻辑,但这样的代码将难以维护,更不具备良好的可扩展性。

从真实的业务场景中提取相应的开发需求,作为学习指导思路的案例,让读者真真切切地感受到设计模式的魅力。

当设计模式的思想与自己的开发思路融合后,再勤加练习,就能在设计模式的基础上构建出更加合理的代码。

第4章 工厂模式

粗暴的开发方式可以归纳为三步:定义属性,创建方法,调用展示。虽然初次实现很快,但不便于后期维护和扩展。

视觉盲区决定了你的选择。
同样一本书、同样一条路、同样一座城,真的以为生活中有选择吗?有时候很多选项只是摆设,给多少次机会我们选择的都是一模一样的。这不是如何选的问题,而是认知范围决定了下一秒做的事情,下一秒做的事情又影响了再下一秒的决定。就像管中窥豹一样,总有一部分视野是黑色的,会被忽略掉,而这看不到的部分却举足轻重。

工厂模式介绍

工厂模式也称简单工厂模式,是创建型设计模式的一种,这种设计模式提供了按需创建对象的最佳方式。同时,这种创建方式不会对外暴露创建细节,并且会通过一个统一的接口创建所需对象。

这种设计模式也是Java开发中常见的一种模式,它的主要意图是定义一个创建对象的接口,让其子类自己决定将哪一个工厂类实例化,工厂模式使创建过程延迟到子类中进行。
简单地说,就是为了给代码结构提供扩展性,屏蔽每一个功能类中的具体实现逻辑。这种方式便于外部更加简单地调用,同时也是去掉众多if…else的最佳手段。缺点是工程类里还是有一堆if

模拟发放多种奖品

为了更贴近真实场景的实际开发,这里模拟互联网运营活动中营销场景的业务需求,如图4-2所示。由于营销场景存在复杂性、多变性、临时性,因此在研发设计时需要更加深入地了解业务需求;否则会经常面临各种紧急穿插的需求,让原本简单的增删改查(CRUD)实现变得臃肿不堪、代码结构极其混乱,最终难以维护,也无法防控风险。

发奖接口

public interface ICommodity {
    void sendCommodity(String uId, String commodityId, String bizId, java.util.Map<String, String> extMap) throws Exception;
}

三种发奖接口实现

优惠券

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;

public class CouponCommodityService implements ICommodity {
    private static final Logger logger = LoggerFactory.getLogger(CouponCommodityService.class);
    private final CouponService couponService = new CouponService();

    @Override
    public void sendCommodity(String uId, String commodityId, String bizId, java.util.Map<String, String> extMap) throws Exception {
        CouponResult couponResult = couponService.sendCoupon(uId, commodityId, bizId);
        logger.info("请求参数[优惠券] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[优惠券]:{}", JSON.toJSON(couponResult));
        if (!"0000".equals(couponResult.getCode())) {
            throw new RuntimeException(couponResult.getInfo());
        }
    }
}

实物商品

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;

public class GoodsCommodityService implements ICommodity {
    private static final Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class);
    private final GoodsService goodsService = new GoodsService();

    @Override
    public void sendCommodity(String uId, String commodityId, String bizId, java.util.Map<String, String> extMap) throws Exception {
        DeliverReq deliverReq = new DeliverReq();
        deliverReq.setUserName(queryUserName(uId));
        deliverReq.setUserPhone(queryUserPhoneNumber(uId));
        deliverReq.setSku(commodityId);
        deliverReq.setOrderId(bizId);
        deliverReq.setConsigneeUserName(extMap.get("consigneeUserName"));
        deliverReq.setConsigneeUserPhone(extMap.get("consigneeUserPhone"));
        deliverReq.setConsigneeUserAddress(extMap.get("consigneeUserAddress"));

        boolean isSuccess = goodsService.deliverGoods(deliverReq);
        logger.info("请求参数[实物商品] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[实物商品]:{}", isSuccess);
        if (!isSuccess) {
            throw new RuntimeException("实物商品发放失败");
        }
    }

    private String queryUserName(String uId) {
        return "花花";
    }

    private String queryUserPhoneNumber(String uId) {
        return "15200101232";
    }
}

第三方兑换卡(爱奇艺)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;

public class CardCommodityService implements ICommodity {
    private static final Logger logger = LoggerFactory.getLogger(CardCommodityService.class);
    private final IQiYiCardService iQiYiCardService = new IQiYiCardService();

    @Override
    public void sendCommodity(String uId, String commodityId, String bizId, java.util.Map<String, String> extMap) throws Exception {
        String mobile = queryUserMobile(uId);
        iQiYiCardService.grantToken(mobile, bizId);
        logger.info("请求参数[爱奇艺兑换卡] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[爱奇艺兑换卡]:success");
    }

    private String queryUserMobile(String uId) {
        return "15200101232";
    }
}

商店工厂

public class StoreFactory {
    /**
     * 奖品类型方式实例化
     * @param commodityType 奖品类型
     * @return 实例化对象
     */
    public ICommodity getCommodityService(Integer commodityType) {
        if (commodityType == null) {
            return null;
        }
        switch (commodityType) {
            case 1:
                return new CouponCommodityService();
            case 2:
                return new GoodsCommodityService();
            case 3:
                return new CardCommodityService();
            default:
                throw new RuntimeException("不存在的奖品服务类型");
        }
    }

    /**
     * 奖品类信息方式实例化
     * @param clazz 奖品类型
     * @return 实例化对象
     */
    public ICommodity getCommodityService(Class<? extends ICommodity> clazz) throws IllegalAccessException, InstantiationException {
        if (clazz == null) {
            return null;
        }
        return clazz.newInstance();
    }
}

单元测试

import org.junit.Test;
import java.util.HashMap;

public class StoreFactoryTest {
    @Test
    public void test_StoreFactory_01() throws Exception {
        StoreFactory storeFactory = new StoreFactory();

        // 1. 优惠券
        ICommodity commodityService1 = storeFactory.getCommodityService(1);
        commodityService1.sendCommodity("10001", "EGM1023938910232121323432", "791098764902132", null);

        // 2. 实物商品
        ICommodity commodityService2 = storeFactory.getCommodityService(2);
        java.util.Map<String, String> extMap = new HashMap<>();
        extMap.put("consigneeUserName", "谢先生");
        extMap.put("consigneeUserPhone", "15200292123");
        extMap.put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");
        commodityService2.sendCommodity("10001", "9820198721311", "1023000020112221113", extMap);

        // 3. 第三方兑换卡(爱奇艺)
        ICommodity commodityService3 = storeFactory.getCommodityService(3);
        commodityService3.sendCommodity("10001", "AQY1xjkUodl8LO975GdfrYUio", null, null);
    }

    @Test
    public void test_StoreFactory_02() throws Exception {
        StoreFactory storeFactory = new StoreFactory();
        // 1. 优惠券
        ICommodity commodityService = storeFactory.getCommodityService(CouponCommodityService.class);
        commodityService.sendCommodity("10001", "EGM1023938910232121323432", "791098764902132", null);
    }
}

个人解读

只是将if中扎堆的代码,抽取到单独的类中而已,但总比一大片的if好一些。

第5章 抽象工厂模式

抽象工厂也可以称作其他工厂的工厂,它可以在抽象工厂中创建出其他工厂,与工厂模式一样,都是用来解决接口选择的问题,同样都属于创建型模式。

此设计模式同样用来解决if…else方式,但偏向有多套相似的组件能力来使用。
缓存集群升级场景
很多初创团队的蛮荒期,并没有完整的底层服务。

团队在初建时业务体量不大,在预估的系统服务 QPS 较低、系统压力较小、并发访问量少、近一年没有大动作等条件下,结合快速起步、时间紧迫、成本投入的因素,并不会投入特别多的研发资源构建出非常完善的系统架构。如图5-3所示,就像对Redis的使用,可能最开始只需要一个单机就可以满足现状。但随着业务超预期的快速发展,系统的负载能力也要随之跟上,原有的单机Redis已经无法满足系统的需要。这时就需要建设或者更换更为健壮的Redis集群服务,在这个升级的过程中是不能停系统的,并且需要平滑过渡。

集群适配器接口

import java.util.concurrent.TimeUnit;

public interface ICacheAdapter {
    String get(String key);
    void set(String key, String value);
    void set(String key, String value, long timeout, TimeUnit timeUnit);
    void del(String key);
}

实现集群适配器接口

EGM 集群:EGMCacheAdapter
import java.util.concurrent.TimeUnit;

public class EGMCacheAdapter implements ICacheAdapter {
    private final EGM egm = new EGM();

    @Override
    public String get(String key) {
        return egm.gain(key);
    }

    @Override
    public void set(String key, String value) {
        egm.set(key, value);
    }

    @Override
    public void set(String key, String value, long timeout, TimeUnit timeUnit) {
        egm.setEx(key, value, timeout, timeUnit);
    }

    @Override
    public void del(String key) {
        egm.delete(key);
    }
}
IIR 集群:IIRCacheAdapter
import java.util.concurrent.TimeUnit;

public class IIRCacheAdapter implements ICacheAdapter {
    private final IIR iir = new IIR();

    @Override
    public String get(String key) {
        return iir.get(key);
    }

    @Override
    public void set(String key, String value) {
        iir.set(key, value);
    }

    @Override
    public void set(String key, String value, long timeout, TimeUnit timeUnit) {
        iir.setExpire(key, value, timeout, timeUnit);
    }

    @Override
    public void del(String key) {
        iir.del(key);
    }
}

代理方式的抽象工厂类

代理抽象工厂 JDKProxyFactory

import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;

public class JDKProxyFactory {
    public static <T> T getProxy(Class<T> cacheClazz, Class<? extends ICacheAdapter> cacheAdapter) throws Exception {
        InvocationHandler handler = new JDKInvocationHandler(cacheAdapter.newInstance());
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        return (T) Proxy.newProxyInstance(classLoader, new Class[]{cacheClazz}, handler);
    }
}

反射调用方法 JDKInvocationHandler

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class JDKInvocationHandler implements InvocationHandler {
    private final ICacheAdapter cacheAdapter;

    public JDKInvocationHandler(ICacheAdapter cacheAdapter) {
        this.cacheAdapter = cacheAdapter;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return ICacheAdapter.class.getMethod(method.getName(), getClazzByArgs(args)).invoke(cacheAdapter, args);
    }

    // 假设 ClassLoaderUtils 类中的 getClazzByArgs 方法实现如下
    private Class<?>[] getClazzByArgs(Object[] args) {
        Class<?>[] classes = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            classes[i] = args[i].getClass();
        }
        return classes;
    }
}

单元测试

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CacheServiceTest {
    private static final Logger logger = LoggerFactory.getLogger(CacheServiceTest.class);

    @Test
    public void test_CacheService() throws Exception {
        CacheService proxy_EGM = JDKProxyFactory.getProxy(CacheService.class, EGMCacheAdapter.class);
        proxy_EGM.set("user_name_01", "小傅哥");
        String val01 = proxy_EGM.get("user_name_01");
        logger.info("缓存服务 EGM 测试,proxy_EGM.get 测试结果:{}", val01);

        CacheService proxy_IIR = JDKProxyFactory.getProxy(CacheService.class, IIRCacheAdapter.class);
        proxy_IIR.set("user_name_01", "小傅哥");
        String val02 = proxy_IIR.get("user_name_01");
        logger.info("缓存服务 IIR 测试,proxy_IIR.get 测试结果:{}", val02);
    }
}

个人解读

这种模式其实可以进一步应用,比如我们系统中有一些调用第三方Api的地方,全部抽取成抽象工程的模式,两个实现类,一个mock、一个正式使用。

结合SpringBoot启动加载所谓的全局的ProdAdapter.class,或者是MockAdapter.class,可以埋入自定义的模拟接口逻辑。

第6章 建造者模式(能力的组合)

建造者模式的核心目的是通过使用多个简单对象一步步地构建出一个复杂对象。
例如,《王者荣耀》游戏的初始化界面有道路、树木、野怪和守卫塔等。换一个场景选择其他模式时,同样会建设道路、树木、野怪和守卫塔等,但是它们的摆放位置和大小各有不同。这种初始化游戏元素的场景就可以使用建造者模式。
这种根据相同的物料、不同的组装方式产生出具体内容,就是建造者模式的最终意图,即将一个复杂的构建与其表示分离,用同样的构建过程可以创建不同的表示。

建造者模式代码工程有三个核心类,这三个核心类是建造者模式的具体实现。与使用if…else判断方式实现逻辑相比,它额外新增了两个类,具体功能如下:
1· Builder:建造者类具体的各种组装,都由此类实现。
2· DecorationPackageMenu:是IMenu接口的实现类,主要承载建造过程中的填充器,相当于一套承载物料和创建者中间衔接的内容。

模拟需求

很多装修公司都会提供一些套餐服务,一般会有:豪华欧式、轻奢田园和现代简约装修服务套餐等。而这些套餐的背后是不同装修材料和设计风格的组合,例如一级顶、二级顶、多乐士涂料、立邦涂料、圣象地板、德尔地板、马可波罗地砖、东鹏地砖等。按照不同的套餐价格,选取不同的品牌进行组合,最终再结合装修面积给出整体报价。

下面模拟装修公司推出的一些装修服务套餐,按照不同的价格组合品牌,并介绍使用建造者模式实现这一需求。

定义装修包接口

接口类定义了填充吊顶、涂料、地板、地砖各种材料的方法,以及最终提供获取全部明细的方法。

public interface IMenu {
    IMenu appendCeiling(Matter matter); // 吊顶
    IMenu appendCoat(Matter matter);    // 涂料    
    IMenu appendFloor(Matter matter);   // 地板    
    IMenu appendTile(Matter matter);    // 地砖    
    String getDetail();                 // 明细
}

实现装修包接口

在装修包的实现中,每一种方法都返回了this对象本身,可以非常方便地用于连续填充各种物料。同时,在填充时也会根据物料计算相应面积的报价,吊顶和涂料按照面积乘以单价计算。最后,同样提供了统一的获取装修清单的明细方法。

public class DecorationPackageMenu implements IMenu {
    private List<Matter> list = new ArrayList<Matter>(); // 装修清单
    private BigDecimal price = BigDecimal.ZERO; // 装修价格
    private BigDecimal area; // 面积
    private String grade; // 装修等级:豪华欧式、轻奢田园和现代简约

    private DecorationPackageMenu() {
    }

    public DecorationPackageMenu(Double area, String grade) {
        this.area = new BigDecimal(area);
        this.grade = grade;
    }

    public IMenu appendCeiling(Matter matter) {
        list.add(matter);
        price = price.add(area.multiply(new BigDecimal("0.2")).multiply(matter.price()));
        return this;
    }

    public IMenu appendCoat(Matter matter) {
        list.add(matter);
        price = price.add(area.multiply(new BigDecimal("1.4")).multiply(matter.price()));
        return this;
    }

    public IMenu appendFloor(Matter matter) {
        list.add(matter);
        price = price.add(area.multiply(matter.price()));
        return this;
    }

    public IMenu appendTile(Matter matter) {
        list.add(matter);
        price = price.add(area.multiply(matter.price()));
        return this;
    }

    public String getDetail() {
        StringBuilder detail = new StringBuilder("\r\n-------------------------------------------------------\r\n"
                + "装修清单:" + "\r\n"
                + "装修等级:" + grade + "\r\n"
                + "装修价格:" + price.setScale(2, BigDecimal.ROUND_HALF_UP) + " 元                        \r\n"
                + "房屋面积:" + area.doubleValue() + " 平方米\r\n"
                + "材料清单:\r\n");
        for (Matter matter : list) {
            detail.append(matter.scene()).append(":").append(matter.brand()).append("、")
                  .append(matter.model()).append("、每平方米价格:").append(matter.price()).append(" 元。\n");
        }
        return detail.toString();
    }
}

建造者类创建

public class Builder {
    public IMenu levelOne(Double area) {
        return new DecorationPackageMenu(area, "豪华欧式")
               .appendCeiling(new LevelTwoCeiling())    // 吊顶:二级顶
               .appendCoat(new DuluxCoat())       // 涂料:多乐士
               .appendFloor(new ShengXiangFloor());     // 地板:圣象
    }

    public IMenu levelTwo(Double area) {
        return new DecorationPackageMenu(area, "轻奢田园")
               .appendCeiling(new LevelTwoCeiling())   // 吊顶:二级顶
               .appendCoat(new LiBangCoat())      // 涂料:立邦
               .appendTile(new MarcoPoloTile());       // 地砖:马可波罗
    }

    public IMenu levelThree(Double area) {
        return new DecorationPackageMenu(area, "现代简约")
               .appendCeiling(new LevelOneCeiling())   // 吊顶:二级顶
               .appendCoat(new LiBangCoat())      // 涂料:立邦
               .appendTile(new DongPengTile());        // 地砖:东鹏
    }
}

单元测试

@Test
public void test_Builder() {
    Builder builder = new Builder();
    // 豪华欧式
    System.out.println(builder.levelOne(132.52D).getDetail());
    // 轻奢田园
    System.out.println(builder.levelTwo(98.25D).getDetail());
    // 现代简约
    System.out.println(builder.levelThree(85.43D).getDetail());
}

个人解读

其实有多种应用,比如文中的一致append到最后的build,也可以结合接口实现形式实现各个子类的build模式。

第7章 原型模式(题库中生成试卷)

原型模式主要解决的是创建重复对象的问题,而这部分对象内容本身比较复杂,从数据库或者RPC接口中获取相关对象数据的耗时较长,因此需要采用复制的方式节省时间。

这种场景也经常出现在我们身边,只不过很少有人提炼出这种设计思想,并运用到自己的系统开发中,就像:
1· 经常使用Ctrl+C、Ctrl+V组合键复制和粘贴代码。
2· Java多数类中提供的API方法Object clone()。
3· 细胞的有丝分裂

模拟需求

每个人都经历过考试,大部分情况都是在纸质的试卷上答题,随着互联网的兴起,也有一些考试改为上机考试。

下面就来实现这样的功能:同样一张试卷、同样的题目、同样的答案,把题目和答案全部混排。

题目混排工具包

考题答案混排的工具包提供了实现混排的random方法。其核心逻辑如下:
1· 在混排操作方法中,首先把题目选项使用 Java 中Collections 工具包里的shuffle方法进行混排操作;
2· 记录混排后正确答案的位置key.equals(next),最终返回新的题目选项单Topic;
3· 混排的过程也就是把A的选项内容给B、把B的选项内容给C,同时把正确答案位置标记出来

public class TopicRandomUtil {
    /**
     * 混排Map元素,记录对应答案key
     * @param option 题目
     * @param key    答案
     * @return Topic 混排后 {A=c., B=d., C=a., D=b.}
     */
    static public Topic random(Map<String, String> option, String key) {
        Set<String> keySet = option.keySet();
        ArrayList<String> keyList = new ArrayList<String>(keySet);
        Collections.shuffle(keyList);

        HashMap<String, String> optionNew = new HashMap<String, String>();
        int idx = 0;
        String keyNew = "";

        for (String next : keySet) {
            String randomKey = keyList.get(idx++);
            if (key.equals(next)) {
                keyNew = randomKey;
            }
            optionNew.put(randomKey, option.get(next));
        }

        return new Topic(optionNew, keyNew);
    }
}

题库复制对象类

类中的操作内容主要包括以下三个方面:
1· 两个 append()对各项题目的添加有点像在建造者模式中使用的方式——添加装修材料。
2· clone()的核心操作是复制对象,这里的复制不仅包括对象本身,也包括两个集合。只有这样的复制才能确保在操作复制对象时不影响原对象。

public class QuestionBank implements Cloneable {
    private String candidate; // 考生
    private String number;    // 考号
    private ArrayList<ChoiceQuestion> choiceQuestionList = new ArrayList<ChoiceQuestion>();
    private ArrayList<AnswerQuestion> answerQuestionList = new ArrayList<AnswerQuestion>();

    public QuestionBank append(ChoiceQuestion choiceQuestion) {
        choiceQuestionList.add(choiceQuestion);
        return this;
    }

    public QuestionBank append(AnswerQuestion answerQuestion) {
        answerQuestionList.add(answerQuestion);
        return this;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        QuestionBank questionBank = (QuestionBank) super.clone();
        questionBank.choiceQuestionList = (ArrayList<ChoiceQuestion>) choiceQuestionList.clone();
        questionBank.answerQuestionList = (ArrayList<AnswerQuestion>) answerQuestionList.clone();

        // 题目混排
        Collections.shuffle(questionBank.choiceQuestionList);
        Collections.shuffle(questionBank.answerQuestionList);

        // 答案混排
        ArrayList<ChoiceQuestion> choiceQuestionList = questionBank.choiceQuestionList;
        for (ChoiceQuestion question : choiceQuestionList) {
            Topic random = TopicRandomUtil.random(question.getOption(), question.getKey());
            question.setOption(random.getOption());
            question.setKey(random.getKey());
        }

        return questionBank;
    }

    public void setCandidate(String candidate) {
        this.candidate = candidate;
    }

    public void setNumber(String number) {
        this.number = number;
    }

    @Override
    public String toString() {
        StringBuilder detail = new StringBuilder("考生:" + candidate + "\r\n"
                + "考号:" + number + "\r\n"
                + "--------------------------------------------------------\r\n"
                + "一、选择题" + "\r\n\n");

        for (int idx = 0; idx < choiceQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(choiceQuestionList.get(idx).getName()).append("\r\n");
            Map<String, String> option = choiceQuestionList.get(idx).getOption();
            for (String key : option.keySet()) {
                detail.append(key).append(":").append(option.get(key)).append("                \r\n");
            }
            detail.append("答案:").append(choiceQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        detail.append("二、问答题" + "\r\n\n");
        for (int idx = 0; idx < answerQuestionList.size(); idx++) {
            detail.append("第").append(idx + 1).append("题:").append(answerQuestionList.get(idx).getName()).append("\r\n");
            detail.append("答案:").append(answerQuestionList.get(idx).getKey()).append("\r\n\n");
        }

        return detail.toString();
    }
}

初始化试卷数据

这个类的内容就比较简单了,主要提供对试卷内容的模式初始化操作(所有考生的试卷一样,但题目顺序不一致)。
对外部提供创建试卷的方法,在创建的过程中使用的是复制的方式(QuestionBank) questionBank.clone();,并最终返回试卷信息。

public class QuestionBankController {
    private QuestionBank questionBank = new QuestionBank();

    public QuestionBankController() {
        Map<String, String> map01 = new HashMap<String, String>();
        map01.put("A", "JAVA2 EE");
        map01.put("B", "JAVA2 Card");
        map01.put("C", "JAVA2 ME");
        map01.put("D", "JAVA2 HE");
        map01.put("E", "JAVA2 SE");

        Map<String, String> map02 = new HashMap<String, String>();
        map02.put("A", "JAVA程序的main方法必须写在类里面");
        map02.put("B", "JAVA程序中可以有多个main方法");
        map02.put("C", "JAVA程序中类名必须与文件名一样");
        map02.put("D", "JAVA程序的main方法中如果只有一条语句,可以不用{}(大括号)括起来");

        Map<String, String> map03 = new HashMap<String, String>();
        map03.put("A", "变量由字母、下划线、数字、$符号随意组成");
        map03.put("B", "变量不能以数字作为开头");
        map03.put("C", "A和a在java中是同一个变量");
        map03.put("D", "不同类型的变量,可以起相同的名字");

        Map<String, String> map04 = new HashMap<String, String>();
        map04.put("A", "STRING");
        map04.put("B", "x3x;");
        map04.put("C", "void");
        map04.put("D", "de$f");

        Map<String, String> map05 = new HashMap<String, String>();
        map05.put("A", "31");
        map05.put("B", "0");
        map05.put("C", "1");
        map05.put("D", "2");

        questionBank.append(new ChoiceQuestion("JAVA所定义的版本中不包括", map01, "D"))
                    .append(new ChoiceQuestion("下列说法正确的是", map02, "A"))
                    .append(new ChoiceQuestion("变量命名规范说法正确的是", map03, "B"))
                    .append(new ChoiceQuestion("以下()不是合法的标识符", map04, "C"))
                    .append(new ChoiceQuestion("表达式(11 + 3 * 8) / 4 % 3的值是", map05, "D"))
                    .append(new AnswerQuestion("小红马和小黑马生的小马有几条腿", "4条腿"))
                    .append(new AnswerQuestion("铁棒打头疼还是木棒打头疼", "头最疼"))
                    .append(new AnswerQuestion("什么床不能睡觉", "牙床"))
                    .append(new AnswerQuestion("为什么好马不吃回头草", "后面的草没了"));
    }

    public String createPaper(String candidate, String number) throws CloneNotSupportedException {
        QuestionBank questionBankClone = (QuestionBank) questionBank.clone();
        questionBankClone.setCandidate(candidate);
        questionBankClone.setNumber(number);
        return questionBankClone.toString();
    }
}

单元测试

@Test
public void test_QuestionBank() throws CloneNotSupportedException {
    QuestionBankController questionBankController = new QuestionBankController();
    System.out.println(questionBankController.createPaper("花花", "1000001921032"));
    System.out.println(questionBankController.createPaper("豆豆", "1000001921051"));
    System.out.println(questionBankController.createPaper("大宝", "1000001921987"));
}

应用场景思考

需要通过原始数据,根据一定特性构建出来一份定制的对象。
个人解读
从有限的基础数据中,根据一些规则生成一些信息,生成试卷这个样例比较经典,想不出其他更好的应用示例了。

第8章 单例模式

单例模式适用的场景非常简单,是在日常开发中能遇到的,如数据库的连接池不会反复创建,Spring中一个单例模式Bean的生成和使用,代码中需要设置全局的一些属性并保存
单例模式的实现方式比较多,主要分为在实现上是否支持懒汉模式,是否支持在线程安全中运用各项技巧。当然,也有一些场景不需要考虑懒汉模式的情况,会直接使用static静态类或属性和方法的方式,供外部调用。

静态类使用

它可以在第一次运行时直接初始化Map类,同时也不需要直到延迟加载再使用。

public class Singleton_00 {
    public static Map<String, String> cache = new ConcurrentHashMap<String, String>();
}

懒汉模式(线程不安全)

单例模式有一个特点是不允许外部直接创建,也就是 newSingleton_01(),因此这里在默认的构造函数上添加了私有属性private。

public class Singleton_01 {
    private static Singleton_01 instance;

    private Singleton_01() {
    }

    public static Singleton_01 getInstance() {
        if (null != instance) return instance;
        instance = new Singleton_01();
        return instance;
    }
}

懒汉模式(线程安全)

此种模式虽然是安全的,但由于把锁加到方法中后,所有的访问因为需要锁占用,导致资源浪费。除非在特殊情况下,否则不建议用此种方式实现单例模式。

public class Singleton_02 {
    private static Singleton_02 instance;

    private Singleton_02() {
    }

    public static synchronized Singleton_02 getInstance() {
        if (null != instance) return instance;
        instance = new Singleton_02();
        return instance;
    }
}

饿汉模式(线程安全)

这种方式与开头的第一个实例化 Map 基本一致,在程序启动时直接运行加载,后续有外部需要使用时获取即可。这种方式并不是懒加载,也就是说无论程序中是否用到这样的类,都会在程序启动之初进行创建。

这种方式造成的问题就像一款游戏软件,可能游戏地图还没有打开,但是程序已经将这些地图全部实例化。在手机上最明显的体验就是打开游戏提示内存满了,造成手机卡顿。

public class Singleton_03 {
    private static Singleton_03 instance = new Singleton_03();

    private Singleton_03() {
    }

    public static Singleton_03 getInstance() {
        return instance;
    }
}

使用类的内部类(线程安全)

使用类的静态内部类实现的单例模式,既保证了线程安全,又保证了懒汉模式,同时不会因为加锁而降低性能。这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确地加载。这也是推荐使用的一种单例模式。

public class Singleton_04 {
    private static class SingletonHolder {
        private static Singleton_04 instance = new Singleton_04();
    }

    private Singleton_04() {
    }

    public static Singleton_04 getInstance() {
        return SingletonHolder.instance;
    }
}

双重锁校验(线程安全)

双重锁的方式是方法级锁的优化,减少了获取实例的耗时。同时,这种方式也满足了懒汉模式。

public class Singleton_05 {
    private static volatile Singleton_05 instance;

    private Singleton_05() {
    }

    public static Singleton_05 getInstance() {
        if (null != instance) return instance;
        synchronized (Singleton_05.class) {
            if (null == instance) {
                instance = new Singleton_05();
            }
        }
        return instance;
    }
}

CAS“AtomicReference”(线程安全)

Java 并发库提供了很多原子类支持并发访问的数据安全性,如:AtomicInteger、AtomicBoolean、AtomicLong 和AtomicReference。AtomicReference 可以封装引用一个V实例,上面支持并发访问的单例模式就是利用了这种特性。使用CAS的好处是不需要使用传统的加锁方式,而是依赖CAS的忙等算法、底层硬件的实现保证线程安全。相对于其他锁的实现,没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发。当然,CAS也有一个缺点就是忙等,如果一直没有获取到,会陷于死循环。

public class Singleton_06 {
    private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>();
    private static Singleton_06 instance;

    private Singleton_06() {
    }

    public static final Singleton_06 getInstance() {
        for (; ; ) {
            Singleton_06 instance = INSTANCE.get();
            if (null != instance) return instance;
            INSTANCE.compareAndSet(null, new Singleton_06());
            return INSTANCE.get();
        }
    }

    public static void main(String[] args) {
        // cn.bugstack.design.Singleton_06@2b193f2d
        System.out.println(Singleton_06.getInstance());
        // cn.bugstack.design.Singleton_06@2b193f2d
        System.out.println(Singleton_06.getInstance());
    }
}

Effective Java作者推荐的枚举单例(线程安全)

这种方式解决了最主要的线程安全、自由串行化和单一实例问题。

即使在面对复杂的串行化或反射攻击时,也无偿地提供了串行化机制,绝对防止对此实例化。
虽然这种方式还没有被广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
同时,我们也要知道在存在继承的场景下,此种方式是不可用的。

public enum Singleton_07 {
    INSTANCE;

    public void test() {
        System.out.println("hi~");
    }
}

@Test
public void test() {    
  Singleton_07.INSTANCE.test();
}

自由串行化:枚举类型默认支持串行化,并且 Java 会保证在反序列化时不会创建新的实例,从而保证单例的唯一性。当对枚举类型进行反序列化时,Java 虚拟机会根据枚举常量的名称来查找对应的枚举实例,而不是创建一个新的实例。
防止反射攻击:Java 不允许通过反射来创建枚举类型的实例,这就从根本上杜绝了反射攻击破坏单例的可能性。

import java.io.*;

public class EnumSerializationTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
        oos.writeObject(Singleton_07.INSTANCE);
        oos.close();

        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
        Singleton_07 singleton = (Singleton_07) ois.readObject();
        ois.close();

        // 验证是否为同一个实例
        System.out.println(singleton == Singleton_07.INSTANCE); // 输出: true
    }
}

个人解读

用第一个吧,一般情况下也没有机会用到枚举单例。
单例模式本身没必要过多研究,了解技术点即可,更多的要关注这个单例对象占用的内存大小,尽可能的节省内存,简化存储的数据结构。

第9章 适配器模式(API接入的封装-有字段映射)

适配器模式的主要作用是把原本不兼容的接口通过适配修改做到统一,方便调用方使用,变速箱通过不同齿轮之间的啮合,实现与不同转速的齿轮适配。
就像日常生活中用到的万能充电器、数据线和笔记本的转换接头,它们都为适配各种不同的接口进行了兼容。
在业务开发中,经常需要做不同接口的兼容,尤其是中台服务。中台需要把各个业务线的类型服务统一包装,再对外提供接口。

适配器模式解决的主要问题是如何针对多种差异化类型的接口实现统一输出。在介绍工厂方法模式时,也提到过不同种类的奖品处理,其实也是适配器模式的应用。在本节中,还会再体现另外一个多种MQ消息接收的场景。把不同类型的消息中的属性字段做统一处理,便于减少后续人工硬编码方式对MQ的接收。

模拟需求

些营销系统,常见的有裂变、拉客,如邀请一位用户开户,或者邀请一位用户下单,平台就会返利,并且多邀多得。同时,随着拉新量的增多,平台开始设置每月首单返现等奖励。
开发这样一个营销系统就会遇到各种各样的MQ消息或接口,如果逐个开发,会耗费很高的成本,同时后期的扩展也有一定的难度。此时会希望有一个系统,配置后就能把外部的MQ接入,这些MQ就像上面提到的注册开户消息、商品下单消息等。而适配器的思想也恰恰可以运用在这里。需要强调的是,适配器不只可以适配接口,还可以适配一些属性信息。

MQ适配

单独创建一个类RebateInfo。后续所有的MQ信息都需要提供这些属性。

public class RebateInfo {
    private String userId; // 用户ID
    private String bizId;  // 业务ID
    private Date bizTime;  // 业务时间
    private String desc;   // 业务描述
    // ... get/set
}

这个类里的方法非常重要,主要用于把不同类型的MQ中的各种属性映射成需要的属性并返回。就像一个属性中有用户IDuId,将其映射到需要的userId,做统一处理。而这个处理过程需要把映射管理传递给Map<String,String>link,也就是准确地描述了当前MQ中某个属性名称,映射为指定的某个属性名称。接收到的MQ消息基本是JSON格式,可以转换为MAP结构。最后,使用反射调用的方式对类型赋值。

public class MQAdapter {
    public static RebateInfo filter(String strJson, Map<String, String> link) 
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        return filter(SON.parseObject(strJson, Map.class), link);
    }

    public static RebateInfo filter(Map obj, Map<String, String> link) 
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        RebateInfo rebateInfo = new RebateInfo();
        for (String key : link.keySet()) {
            Object val = obj.get(link.get(key));
            RebateInfo.class.getMethod("set" + key.substring(0, 1).toUpperCase() + key.substring(1), String.class)
                   .invoke(rebateInfo, val.toString());
        }
        return rebateInfo;
    }
}

MQ消息适配测试验证

这里分别模拟传入了两个不同的MQ消息,并设置字段的映射关系。在实际业务开发场景中,可以把这种映射配置关系交给配置文件或数据库后台,以减少编码。

@Test
public void test_MQAdapter() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
    create_account create_account = new create_account();
    create_account.setNumber("100001");
    create_account.setAddress("河北省.廊坊市.广阳区.大学里职业技术学院");
    create_account.setAccountDate(new Date());
    create_account.setDesc("在校开户");

    HashMap<String, String> link01 = new HashMap<String, String>();
    link01.put("userId", "number");
    link01.put("bizId", "number");
    link01.put("bizTime", "accountDate");
    link01.put("desc", "desc");

    RebateInfo rebateInfo01 = MQAdapter.filter(create_account.toString(), link01);

    System.out.println("mq.create_account(适配前)" + create_account.toString());
    System.out.println("mq.create_account(适配后)" + JSON.toJSONString(rebateInfo01));
    System.out.println("");

    OrderMq orderMq = new OrderMq();
    orderMq.setUid("100001");
    orderMq.setSku("10928092093111123");
    orderMq.setOrderId("100000890193847111");
    orderMq.setCreateOrderTime(new Date());

    HashMap<String, String> link02 = new HashMap<String, String>();
    link02.put("userId", "uid");
    link02.put("bizId", "orderId");
    link02.put("bizTime", "createOrderTime");

    RebateInfo rebateInfo02 = MQAdapter.filter(orderMq.toString(), link02);

    System.out.println("mq.orderMq(适配前)" + orderMq.toString());
    System.out.println("mq.orderMq(适配后)" + JSON.toJSONString(rebateInfo02));
}

定义统一适配接口

接口的实现需要完成此接口定义的方法,并把具体的逻辑包装到指定的类中,满足单一职责。

public interface OrderAdapterService {
   boolean isFirst(String uId);
}

分别实现两个不同的接口

// 1.内部商品接口
public class InsideOrderServiceImpl implements OrderAdapterService {
    private OrderService orderService = new OrderService();

    public boolean isFirst(String uId) {
        return orderService.queryUserOrderCount(uId) <= 1;
    }
}

// 2.第三方商品接口
public class POPOrderAdapterServiceImpl implements OrderAdapterService {
    private POPOrderService popOrderService = new POPOrderService();

    public boolean isFirst(String uId) {
        return popOrderService.isFirstOrder(uId);
    }
}

接口适配验证

此时的接口已经统一包装,外部使用者不需要关心内部的具体逻辑。而且在调用时,只需要传入统一的参数即可,这样就能满足适配的作用

@Test
public void test_itfAdapter() {
    OrderAdapterService popOrderAdapterService = new POPOrderAdapterServiceImpl();
    System.out.println("判断首单,接口适配(POP):" + popOrderAdapterService.isFirst("100001"));

    OrderAdapterService insideOrderService = new InsideOrderServiceImpl();
    System.out.println("判断首单,接口适配(自营):" + insideOrderService.isFirst("100001"));
}

第10章 桥接模式(多套能力的组合)

桥接模式的主要作用是通过将抽象部分与实现部分分离,将多种可匹配的使用进行组合。其核心实现是在A类中含有B类接口,通过构造函数传递B类的实现,这个B类就是设计的桥。手机通过“桥”(蓝牙)可以连接玩具车、电视、冰箱和空调等智能家居。

这样的桥接模式存在于日常开发中的哪些场景里呢?包括JDBC 多种驱动程序的实现、同品牌类型的台式机和笔记本电脑、业务实现中的多类接口同组过滤服务等。这些场景都比较适合用桥接模式实现,因为在一些组合中,如果每一个类都实现不同的服务,可能会出现笛卡儿积,而使用桥接模式就可以变得非常简单。

模拟需求

在支付服务行业中,有微信、支付宝及一些其他支付服务,但是对于商家来说,并不能只接受某一种支付方式。如果商家只支持使用微信或支付宝付款,那么就会让顾客为难,商品销量也会受到影响。

这时就出现了第三方平台,它们把市面上的多种支付服务都集中到自己平台中,再把这些平台提供给店铺、超市等商家使用,同时支持人脸支付、指纹支付和密码支付等多种方式。

支付方式:微信、支付宝
支付模式:人脸支付、指纹支付和密码支付
支付方式桥接抽象类

public abstract class Pay {
    protected Logger logger = LoggerFactory.getLogger(Pay.class);
    protected IPayMode payMode;

    public Pay(IPayMode payMode) {
        this.payMode = payMode;
    }

    public abstract String transfer(String uId, String tradeId, BigDecimal amount);
}

支付方式的实现

这里分别模拟了调用第三方的两种支付渠道:微信和支付宝。当然,作为支付综合平台,可能不只接入了这两种渠道,还会有其他渠道。另外,可以看到,在支付时分别调用了风控的接口进行校验,也就是不同的模式支付(人脸、指纹),都需要通过指定的风控校验,才能保证支付安全。

public class WxPay extends Pay {
    public WxPay(IPayMode payMode) {
        super(payMode);
    }

    public String transfer(String uId, String tradeId, BigDecimal amount) {
        logger.info("模拟微信渠道支付划账开始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
        boolean security = payMode.security(uId);
        logger.info("模拟微信渠道支付风控校验。uId:{} tradeId:{} security:{}", uId, tradeId, security);

        if (!security) {
            logger.info("模拟微信渠道支付划账拦截。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
            return "0001";
        }

        logger.info("模拟微信渠道支付划账成功。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
        return "0000";
    }
}

public class ZfbPay extends Pay {
    public ZfbPay(IPayMode payMode) {
        super(payMode);
    }

    public String transfer(String uId, String tradeId, BigDecimal amount) {
        logger.info("模拟支付宝渠道支付划账开始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
        boolean security = payMode.security(uId);
        logger.info("模拟支付宝渠道支付风控校验。uId:{} tradeId:{} security:{}", uId, tradeId, security);

        if (!security) {
            logger.info("模拟支付宝渠道支付划账拦截。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
            return "0001";
        }

        logger.info("模拟支付宝渠道支付划账成功。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
        return "0000";
    }
}

支付模式接口定义

任何一种支付模式,包括人脸、指纹和密码,都会通过风控校验不同程度的安全信息,这里定义一个安全校验接口。

public interface IPayMode {
    boolean security(String uId);
}
支付模式实现(人脸、指纹和密码)
// 1.人脸
public class PayFaceMode implements IPayMode {
    protected Logger logger = LoggerFactory.getLogger(PayCypher.class);

    public boolean security(String uId) {
        logger.info("人脸支付,风控校验脸部识别");
        return true;
    }
}

// 2.指纹
public class PayFingerprintMode implements IPayMode {
    protected Logger logger = LoggerFactory.getLogger(PayCypher.class);

    public boolean security(String uId) {
        logger.info("指纹支付,风控校验指纹信息");
        return true;
    }
}

// 3.密码
public class PayCypher implements IPayMode {
    protected Logger logger = LoggerFactory.getLogger(PayCypher.class);

    public boolean security(String uId) {
        logger.info("密码支付,风控校验环境安全");
        return true;
    }
}

单元测试

与上面的 if…else 实现方式相比,这里的调用方式变得整洁、干净和易用。

外部使用接口的用户不需要关心具体的实现,只要按需选择使用即可。以上优化主要针对桥接模式的使用进行重构if逻辑部分。对于调用部分,可以使用抽象工厂或策略模式配合MAP结构。在 Map 结构中,Key 作为逻辑名称、Value 作为逻辑实现。通过这种方式把服务配置到Map键值对中,可以更方便地获取和使用,从而避免大量使用if…else。

@Test
public void test_pay() {
    System.out.println("\r\n模拟测试场景:微信支付、人脸方式。");
    Pay wxPay = new WxPay(new PayFaceMode());
    wxPay.transfer("weixin_1092033111", "100000109893", new BigDecimal(100));

    System.out.println("\r\n模拟测试场景:支付宝支付、指纹方式。");
    Pay zfbPay = new ZfbPay(new PayFingerprintMode());
    zfbPay.transfer("jlu19dlxo111", "100000109894", new BigDecimal(100));
}

第11章 组合模式

使用乐高玩具,通过一系列的连接组织出一棵结构树。而这种通过把相似对象或方法组合成一组可被调用的结构树对象的设计思路,称为组合模式。

这种设计方式可以让服务组节点进行自由组合并对外提供服务,例如有三个原子校验功能服务(A:身份证;B:银行卡;C:手机号)并对外提供调用。有些调用方需要使用AB 组合,有些调用方需要使用 CBA 组合,还有一些调用方可能只使用三者中的一个。这时就可以使用组合模式构建服务,对于不同类型的调用方配置不同的组织关系树,而这个树形结构可以配置到数据库中,通过程序图形界面控制树形结构的创建和修改。

决策树场景模拟

为一个简化版的营销规则决策树,根据性别、年龄的不同组合,发放不同类型的优惠券,目的是刺激消费,对精准用户进行促活。

重构代码的改动量相对来说会比较大,为了把不同类型的决策节点和最终的果实组装成一棵可被程序运行的决策树,需要做适配设计和工厂方法调用,具体会体现在定义接口和抽象类、初始化配置决策节点(性别、年龄)上。

树节点逻辑过滤器接口

这部分定义了适配的通用接口和相应的方法:逻辑决策器方法、获取决策值方法,让每一个提供决策能力的节点都必须实现此接口,保证统一性。

public interface LogicFilter {
    /**
     * 逻辑决策器
     *
     * @param matterValue    决策值
     * @param treeNodeLineInfoList 决策节点
     * @return 下一个节点Id
     */
    Long filter(String matterValue, List<TreeNodeLink> treeNodeLineInfoList);

    /**
     * 获取决策值方法
     *
     * @param decisionMatter 决策物料
     * @return 决策值
     */
    String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);
}

决策抽象类提供基础服务

在抽象方法中实现了接口方法,同时定义了基本的决策方法:1、2、3、4、5,等于、小于、大于、小于或等于、大于或等于的判断逻辑。同时定义了抽象方法,让每一个实现接口的类都必须按照规则提供决策值,这个决策值用于进行逻辑比对。

public abstract class BaseLogic implements LogicFilter {
    @Override
    public Long filter(String matterValue, List<TreeNodeLink> treeNodeLinkList) {
        for (TreeNodeLink nodeLine : treeNodeLinkList) {
            if (decisionLogic(matterValue, nodeLine)) {
                return nodeLine.getNodeIdTo();
            }
        }
        return 0L;
    }

    @Override
    public abstract String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);

    private boolean decisionLogic(String matterValue, TreeNodeLink nodeLink) {
        switch (nodeLink.getRuleLimitType()) {
            case 1:
                return matterValue.equals(nodeLink.getRuleLimitValue());
            case 2:
                return Double.parseDouble(matterValue) > Double.parseDouble(nodeLink.getRuleLimitValue());
            case 3:
                return Double.parseDouble(matterValue) < Double.parseDouble(nodeLink.getRuleLimitValue());
            case 4:
                return Double.parseDouble(matterValue) <= Double.parseDouble(nodeLink.getRuleLimitValue());
            case 5:
                return Double.parseDouble(matterValue) >= Double.parseDouble(nodeLink.getRuleLimitValue());
            default:
                return false;
        }
    }
}

树节点逻辑实现类

// 1.年龄节点
public class UserAgeFilter extends BaseLogic {
    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("age");
    }
}

// 2.性别节点
public class UserGenderFilter extends BaseLogic {
    @Override
    public String matterValue(Long treeId, String userId, Map<String, String> decisionMatter) {
        return decisionMatter.get("gender");
    }
}

决策引擎接口定义

定义统一的接口操作,这样的好处是便于后续扩展出不同类型的决策引擎,也就是建造不同的决策工厂。

public interface IEngine {
    EngineResult process(final Long treeId, final String userId, TreeRich treeRich, final Map<String, String> decisionMatter);
}

决策节点配置

这里可以将服务的决策节点配置到Map结构中,这样的Map结构可以抽取到XML或数据库中,就可以方便地在 ERP 界面中配置服务了。当需要变更时,不用改动代码,便于管理。

public class EngineConfig {
    static Map<String, LogicFilter> logicFilterMap;

    static {
        logicFilterMap = new ConcurrentHashMap<>();
        logicFilterMap.put("userAge", new UserAgeFilter());
        logicFilterMap.put("userGender", new UserGenderFilter());
    }

    public Map<String, LogicFilter> getLogicFilterMap() {
        return logicFilterMap;
    }

    public void setLogicFilterMap(Map<String, LogicFilter> logicFilterMap) {
        this.logicFilterMap = logicFilterMap;
    }
}

基础决策引擎功能

这里主要提供决策树流程的处理过程,有点像通过链路的关系(性别、年龄)在二叉树中寻找果实节点的过程。同时提供一个抽象方法,执行决策流程的方法,供外部做具体的实现。

public abstract class EngineBase extends EngineConfig implements IEngine {
    private Logger logger = LoggerFactory.getLogger(EngineBase.class);

    @Override
    public abstract EngineResult process(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter);

    protected TreeNode engineDecisionMaker(TreeRich treeRich, Long treeId, String userId, Map<String, String> decisionMatter) {
        TreeRoot treeRoot = treeRich.getTreeRoot();
        Map<Long, TreeNode> treeNodeMap = treeRich.getTreeNodeMap();
        // 规则树根ID
        Long rootNodeId = treeRoot.getTreeRootNodeId();
        TreeNode treeNodeInfo = treeNodeMap.get(rootNodeId);

        // 节点类型[NodeType];1叶子、2果实
        while (treeNodeInfo.getNodeType().equals(1)) {
            String ruleKey = treeNodeInfo.getRuleKey();
            LogicFilter logicFilter = logicFilterMap.get(ruleKey);
            String matterValue = logicFilter.matterValue(treeId, userId, decisionMatter);
            Long nextNode = logicFilter.filter(matterValue, treeNodeInfo.getTreeNodeLinkList());
            treeNodeInfo = treeNodeMap.get(nextNode);
            logger.info("决策树引擎=>{} userId:{} treeId:{} treeNode:{} ruleKey:{} matterValue:{}",
                    treeRoot.getTreeName(), userId, treeId, treeNodeInfo.getTreeNodeId(), ruleKey, matterValue);
        }
        return treeNodeInfo;
    }
}

决策引擎的实现

这里对于决策引擎的实现就非常简单了,通过传递进来的必要信息——决策树信息、决策物料值,做具体的树形结构决策。

public class TreeEngineHandle extends EngineBase {
    @Override
    public EngineResult process(Long treeId, String userId, TreeRich treeRich, Map<String, String> decisionMatter) {
        // 决策流程
        TreeNode treeNode = engineDecisionMaker(treeRich, treeId, userId, decisionMatter);

        // 决策结果
        return new EngineResult(userId, treeId, treeNode.getTreeNodeId(), treeNode.getNodeValue());
    }
}

初始化决策树数据

注意:
这部分是组合模式非常重要的使用,在已经建造好的决策树关系下,可以创建出树的各个节点,以及对节点间使用链路进行串联,如图 11-4 所示。即使后续需要做任何业务的扩展,都可以在里面添加相应的节点,并做动态化的配置。

@Before
public void init() {
    // 节点:1
    TreeNode treeNode_01 = new TreeNode();
    treeNode_01.setTreeId(10001L);
    treeNode_01.setTreeNodeId(1L);
    treeNode_01.setNodeType(1);
    treeNode_01.setNodeValue(null);
    treeNode_01.setRuleKey("userGender");
    treeNode_01.setRuleDesc("用户性别[男/女]");

    // 链接:1->11
    TreeNodeLink treeNodeLink_11 = new TreeNodeLink();
    treeNodeLink_11.setNodeIdFrom(1L);
    treeNodeLink_11.setNodeIdTo(11L);
    treeNodeLink_11.setRuleLimitType(1);
    treeNodeLink_11.setRuleLimitValue("man");

    // 链接:1->12
    TreeNodeLink treeNodeLink_12 = new TreeNodeLink();
    treeNodeLink_12.setNodeIdFrom(1L);
    treeNodeLink_12.setNodeIdTo(12L);
    treeNodeLink_12.setRuleLimitType(1);
    treeNodeLink_12.setRuleLimitValue("woman");

    List<TreeNodeLink> treeNodeLinkList_1 = new ArrayList<>();
    treeNodeLinkList_1.add(treeNodeLink_11);
    treeNodeLinkList_1.add(treeNodeLink_12);
    treeNode_01.setTreeNodeLinkList(treeNodeLinkList_1);

    // 节点:11
    TreeNode treeNode_11 = new TreeNode();
    treeNode_11.setTreeId(10001L);
    treeNode_11.setTreeNodeId(11L);
    treeNode_11.setNodeType(1);
    treeNode_11.setNodeValue(null);
    treeNode_11.setRuleKey("userAge");
    treeNode_11.setRuleDesc("用户年龄");

    // 链接:11->111
    TreeNodeLink treeNodeLink_111 = new TreeNodeLink();
    treeNodeLink_111.setNodeIdFrom(11L);
    treeNodeLink_111.setNodeIdTo(111L);
    treeNodeLink_111.setRuleLimitType(3);
    treeNodeLink_111.setRuleLimitValue("25");

    // 链接:11->112
    TreeNodeLink treeNodeLink_112 = new TreeNodeLink();
    treeNodeLink_112.setNodeIdFrom(11L);
    treeNodeLink_112.setNodeIdTo(112L);
    treeNodeLink_112.setRuleLimitType(5);
    treeNodeLink_112.setRuleLimitValue("25");

    List<TreeNodeLink> treeNodeLinkList_11 = new ArrayList<>();
    treeNodeLinkList_11.add(treeNodeLink_111);
    treeNodeLinkList_11.add(treeNodeLink_112);
    treeNode_11.setTreeNodeLinkList(treeNodeLinkList_11);

    // 节点:12
    TreeNode treeNode_12 = new TreeNode();
    treeNode_12.setTreeId(10001L);
    treeNode_12.setTreeNodeId(12L);
    treeNode_12.setNodeType(1);
    treeNode_12.setNodeValue(null);
    treeNode_12.setRuleKey("userAge");
    treeNode_12.setRuleDesc("用户年龄");

    // 链接:12->121
    TreeNodeLink treeNodeLink_121 = new TreeNodeLink();
    treeNodeLink_121.setNodeIdFrom(12L);
    treeNodeLink_121.setNodeIdTo(121L);
    treeNodeLink_121.setRuleLimitType(3);
    treeNodeLink_121.setRuleLimitValue("25");

    // 链接:12->122
    TreeNodeLink treeNodeLink_122 = new TreeNodeLink();
    treeNodeLink_122.setNodeIdFrom(12L);
    treeNodeLink_122.setNodeIdTo(122L);
    treeNodeLink_122.setRuleLimitType(5);
    treeNodeLink_122.setRuleLimitValue("25");

    List<TreeNodeLink> treeNodeLinkList_12 = new ArrayList<>();
    treeNodeLinkList_12.add(treeNodeLink_121);
    treeNodeLinkList_12.add(treeNodeLink_122);
    treeNode_12.setTreeNodeLinkList(treeNodeLinkList_12);

    // 节点:111
    TreeNode treeNode_111 = new TreeNode();
    treeNode_111.setTreeId(10001L);
    treeNode_111.setTreeNodeId(111L);
    treeNode_111.setNodeType(2);
    treeNode_111.setNodeValue("果实A");

    // 节点:112
    TreeNode treeNode_112 = new TreeNode();
    treeNode_112.setTreeId(10001L);
    treeNode_112.setTreeNodeId(112L);
    treeNode_112.setNodeType(2);
    treeNode_112.setNodeValue("果实B");

    // 节点:121
    TreeNode treeNode_121 = new TreeNode();
    treeNode_121.setTreeId(10001L);
    treeNode_121.setTreeNodeId(121L);
    treeNode_121.setNodeType(2);
    treeNode_121.setNodeValue("果实C");

    // 节点:122
    TreeNode treeNode_122 = new TreeNode();
    treeNode_122.setTreeId(10001L);
    treeNode_122.setTreeNodeId(122L);
    treeNode_122.setNodeType(2);
    treeNode_122.setNodeValue("果实D");

    // 树根
    TreeRoot treeRoot = new TreeRoot();
    treeRoot.setTreeId(10001L);
    treeRoot.setTreeRootNodeId(1L);
    treeRoot.setTreeName("规则决策树");

    Map<Long, TreeNode> treeNodeMap = new HashMap<>();
    treeNodeMap.put(1L, treeNode_01);
    treeNodeMap.put(11L, treeNode_11);
    treeNodeMap.put(12L, treeNode_12);
    treeNodeMap.put(111L, treeNode_111);
    treeNodeMap.put(112L, treeNode_112);
    treeNodeMap.put(121L, treeNode_121);
    treeNodeMap.put(122L, treeNode_122);

    treeRich = new TreeRich(treeRoot, treeNodeMap);
}

单元测试

@Test
public void test_tree() {
    logger.info("决策树组合结构信息:\r\n" + JSON.toJSONString(treeRich));
    IEngine treeEngineHandle = new TreeEngineHandle();
    Map<String, String> decisionMatter = new HashMap<>();
    decisionMatter.put("gender", "man");
    decisionMatter.put("age", "29");
    EngineResult result = treeEngineHandle.process(10001L, "Oli09pLkdjh", treeRich, decisionMatter);
    logger.info("测试结果:{}", JSON.toJSONString(result));
}

在单元测试中,提供了通过组合模式创建出的流程决策树,调用时传入了决策树的ID。在业务开发中,可以很方便地解耦决策树与业务的绑定关系,按需传入决策树ID即可。这里的入参提供了需要决策的信息男(man)、年龄(29 岁)的参数信息。另外,以下数据也可以模拟测试。

果实A:gender=man、age=22
果实B:gender=man、age=29
果实C:gender=woman、age=22
果实D:gender=woman、age=29

第12章 装饰器模式

装饰器模式就像俄罗斯套娃,它的核心是在不改变原有类的基础上给类新增功能。对于不改变原有类,可能有的人会想到继承、AOP 切面,虽然这些方式都可以实现,但是使用装饰器模式是另外一种更灵活的思路,能够避免继承导致的子类过多问题,也可以避免AOP带来的复杂性问题。

很多熟悉的场景都用到了装饰器模式,例如是否熟悉 newBufferedReader(new FileReader(“”));这段代码?大家在学习Java开发的字节流、字符流和文件流的内容时都见到过,一层嵌套一层,字节流转字符流等。这就是使用装饰器模式的一种体现。

单点登录场景模拟

在业务开发的初期,往往运营人员使用的是 ERP 系统,只需要登录账户验证即可,验证通过后即可访问 ERP 的所有资源。但随着业务的不断发展,团队里开始出现专门的运营人员、营销人员和数据人员,每类人员对ERP的使用需求不同,有些需要创建活动,有些只是查看数据。同时,为了保证数据的安全,不会让每位运营人员都有最高的权限。

装饰器主要解决的是直接继承时因功能的不断横向扩展导致子类膨胀的问题,而使用装饰器模式比直接继承更加灵活,同时也不再需要维护子类。
在装饰器模式中,有四点比较重要:
1· 抽象构件角色(Component):定义抽象接口;
2· 具体构件角色(ConcreteComponent):实现抽象接口,可以是一组;
3· 装饰角色(Decorator):定义抽象类并继承接口中的方法,保证一致性;
4· 具体装饰角色(ConcreteDecorator):扩展装饰具体的实现逻辑。
通过以上四种实现装饰器模式,主要核心内容会体现在抽象类的定义和实现方面

抽象类装饰角色 SsoDecorator

public abstract class SsoDecorator implements HandlerInterceptor {
    private HandlerInterceptor handlerInterceptor;

    private SsoDecorator() {}

    public SsoDecorator(HandlerInterceptor handlerInterceptor) {
        this.handlerInterceptor = handlerInterceptor;
    }

    public boolean preHandle(String request, String response, Object handler) {
        return handlerInterceptor.preHandle(request, response, handler);
    }
}

装饰角色逻辑实现 LoginSsoDecorator

public class LoginSsoDecorator extends SsoDecorator {
    private Logger logger = LoggerFactory.getLogger(LoginSsoDecorator.class);
    private static Map<String, String> authMap = new ConcurrentHashMap<String, String>();

    static {
        authMap.put("huahua", "queryUserInfo");
        authMap.put("doudou", "queryUserInfo");
    }

    public LoginSsoDecorator(HandlerInterceptor handlerInterceptor) {
        super(handlerInterceptor);
    }

    @Override
    public boolean preHandle(String request, String response, Object handler) {
        boolean success = super.preHandle(request, response, handler);
        if (!success) return false;
        String userId = request.substring(8);
        String method = authMap.get(userId);
        logger.info("模拟单点登录方法访问拦截校验:{} {}", userId, method);
        // 模拟方法校验
        return "queryUserInfo".equals(method);
    }
}

单元测试

@Test
public void test_LoginSsoDecorator() {
    LoginSsoDecorator ssoDecorator = new LoginSsoDecorator(new SsoInterceptor());
    String request = "1successhuahua";
    boolean success = ssoDecorator.preHandle(request, "ewcdqwt40liuiu", "t");
    System.out.println("登录校验:" + request + (success ? " 放行" : " 拦截"));
}

思考
就是先定义一个abstract父类 以及所有需要能力的方法。
所有子类继续父类实现
应用各处new自己的子类,也可以用父类实现好的方法。

第13章 外观模式

中间件场景模拟
本章模拟一个将所有服务接口添加白名单的场景,如图13-2所示。
在项目不断壮大发展的过程中,每一次发版上线都需要测试,而这部分测试验证一般会通过白名单开量或切量的方式验证。如果在每一个接口中都添加这种逻辑,就会非常麻烦且不易维护。另外,这是一类具备通用逻辑的共性需求,非常适合开发成组件,以此治理服务,从而让研发人员可以将精力放在业务功能逻辑的开发上。

配置服务类

public class StarterService {
    private String userStr;

    public StarterService(String userStr) {
        this.userStr = userStr;
    }

    public String[] split(String separatorChar) {
        return StringUtils.split(this.userStr, separatorChar);
    }
}

配置类注解定义

// 配置类注解定义
@ConfigurationProperties("itstack.door")
public class StarterServiceProperties {
    private String userStr;

    public String getUserStr() {
        return userStr;
    }

    public void setUserStr(String userStr) {
        this.userStr = userStr;
    }
}

获取自定义配置类信息

// 获取自定义配置类信息
@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {
    @Autowired
    private StarterServiceProperties properties;

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
    StarterService starterService() {
        return new StarterService(properties.getUserStr());
    }
}

切面注解定义

切面注解定义了外观模式切面注解,后续将此注解添加到需要扩展白名单的方法上。这里提供了两个入参:key获取某个字段,例如用户ID;returnJson确定白名单拦截后返回的具体内容。

// 切面注解定义
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {
    String key() default "";
    String returnJson() default "";
}

白名单切面逻辑

见注解

// 白名单切面逻辑
@Aspect
@Component
public class DoJoinPoint {
    private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);
    @Autowired
    private StarterService starterService;

    //1. 定义切面,这里采用的是注解路径,也就是所有加入这个注解的方法都会被切面管理。
    @Pointcut("@annotation(cn.bugstack.design.door.annotation.DoDoor)")
    public void aopPoint() {
    }

    //4. 切面核心逻辑,这部分主要是判断当前访问的用户ID是否为白名单用户。如果是,则放行jp.proceed();,否则返回自定义的拦截提示信息。
    @Around("aopPoint()")
    public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
        // 获取内容
        Method method = getMethod(jp);
        DoDoor door = method.getAnnotation(DoDoor.class);
        // 获取字段值
        String keyValue = getFiledValue(door.key(), jp.getArgs());
        logger.info("itstack door handler method:{} value:{}", method.getName(), keyValue);
        if (null == keyValue || "".equals(keyValue)) return jp.proceed();
        // 配置内容
        String[] split = starterService.split(",");
        // 白名单过滤
        for (String str : split) {
            if (keyValue.equals(str)) {
                return jp.proceed();
            }
        }
        // 拦截
        return returnObject(door, method);
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

    private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
        return jp.getTarget().getClass();
    }

    //3. 返回拦截后的转换对象,当非白名单用户访问时,会返回一些提示信息。
    private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
        Class<?> returnType = method.getReturnType();
        String returnJson = doGate.returnJson();
        if ("".equals(returnJson)) {
            return returnType.newInstance();
        }
        return JSON.parseObject(returnJson, returnType);
    }

    //2. 获取指定key,也就是获取入参中的某个属性,这里主要是获取用户ID,通过ID拦截校验。
    private String getFiledValue(String filed, Object[] args) {
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (null == filedValue || "".equals(filedValue)) {
                    filedValue = BeanUtils.getProperty(arg, filed);
                } else {
                    break;
                }
            } catch (Exception e) {
                if (args.length == 1) {
                    return args[0].toString();
                }
            }
        }
        return filedValue;
    }
}

第14章 享元模式(基础数据缓存)

享元模式主要用于共享通用对象,减少内存的使用,提升系统的访问效率。较大的对象通常比较耗费内存,需要查询大量的接口或使用数据库资源,因此有必要统一抽离出来作为共享对象使用。

享元模式可以分为在服务端和在客户端,一般在互联网H5和Web场景下,大部分数据都需要在服务端处理,如数据库连接池的使用、多线程线程池的使用。除了这些功能,有些需要经过服务端包装处理再下发给客户端,因为服务端需要做享元处理。但在一些游戏场景中,很多客户端需要渲染地图效果,如树木、花草、鱼虫,通过设置不同的元素描述,使用享元共用对象,可以减少内存的占用,让客户端的游戏更加流畅。在享元模式的实现过程中,需要用到享元工厂管理独立的对象和共享的对象,避免出现线程安全的问题

存优化查询场景

本案例模拟商品秒杀场景中使用享元模式优化查询,如图14-2所示。
你是否经历过一个商品下单功能的项目,从最初的日均十几单到一个月后每个时段流量破万。如果没有相关经验,最初可能会使用数据库行级锁的方式保证商品库存的扣减操作。随着业务的快速发展,参与秒杀的用户越来越多,这时数据库已经无法支撑,所以一般会使用Redis的分布式锁控制商品库存。另外,针对不同商品信息的查询,不需要每次都从数据库中获取,因为除了商品库存,其他商品信息都是固定不变的,所以一般会缓存到Redis中。这里模拟使用享元模式搭建工厂结构,提供活动商品的查询服务。商品信息相当于不变的信息,商品库存相当于变化的信息。

商品活动信息类

public class Activity {
    private Long id; // 活动 ID
    private String name; // 活动名称
    private String desc; // 活动描述
    private Date startTime; // 开始时间
    private Date stopTime; // 结束时间
    private Stock stock; // 活动库存
    // ...get/set
}

商品活动库存信息类

public class Stock {
    private int total; // 库存总量
    private int used; // 库存已用
    // ...get/set
}

享元工厂

public class ActivityFactory {
    static Map<Long, Activity> activityMap = new HashMap<Long, Activity>();

    public static Activity getActivity(Long id) {
        Activity activity = activityMap.get(id);
        if (null == activity) {
            // 模拟实际业务应用从接口中获取活动信息
            activity = new Activity();
            activity.setId(10001L);
            activity.setName("图书嗨乐");
            activity.setDesc("图书优惠券分享激励活动第二期");
            activity.setStartTime(new Date());
            activity.setStopTime(new Date());
            activityMap.put(id, activity);
        }
        return activity;
    }
}

模拟 Redis 服务

public class RedisUtils {
    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
    private AtomicInteger stock = new AtomicInteger(0);

    public RedisUtils() {
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            // 模拟库存消耗
            stock.addAndGet(1);
        }, 0, 100000, TimeUnit.MICROSECONDS);
    }

    public int getStockUsed() {
        return stock.get();
    }
}

活动控制类

public class ActivityController {
    private RedisUtils redisUtils = new RedisUtils();

    public Activity queryActivityInfo(Long id) {
        Activity activity = ActivityFactory.getActivity(id);
        // 模拟从 Redis 中获取商品库存变化信息
        Stock stock = new Stock(1000, redisUtils.getStockUsed());
        activity.setStock(stock);
        return activity;
    }
}

单元测试

public class ApiTest {
    private Logger logger = LoggerFactory.getLogger(ApiTest.class);
    private ActivityController activityController = new ActivityController();

    @Test
    public void test_queryActivityInfo() throws InterruptedException {
        for (int idx = 0; idx < 10; idx++) {
            Long req = 10001L;
            Activity activity = activityController.queryActivityInfo(req);
            logger.info("测试结果:{} {}", req, JSON.toJSONString(activity));
            Thread.sleep(1200);
        }
    }
}

第15章 代理模式

代理模式经常会出现在系统或组件中,它们提供一种非常简单易用的方式,控制原本需要编写很多代码才能实现的服务类。类似以下场景:
1· 在数据库访问层面会提供一个比较基础的应用,避免在对应用服务扩容时造成数据库连接数暴增。
2· 使用过的一些中间件,例如RPC框架,在拿到jar包对接口的描述后,中间件会在服务启动时生成对应的代理类。当调用接口时,实际是通过代理类发出的 Socket信息。
3· 常用的MyBatis基本功能是定义接口,不需要写实现类就可以对XML或自定义注解里的SQL语句增删改查。

自定义注解 Select

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Select {
    String value() default "";  // SQL 语句
}

Dao 层接口 IUserDao

public interface IUserDao {
    @Select("select userName from user where id = #{uId}")
    String queryUserInfo(String uId);
}

代理类定义 MapperFactoryBean

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.FactoryBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class MapperFactoryBean<T> implements FactoryBean<T> {
    private Logger logger = LoggerFactory.getLogger(MapperFactoryBean.class);
    private Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() throws Exception {
        InvocationHandler handler = (proxy, method, args) -> {
            Select select = method.getAnnotation(Select.class);
            logger.info("SQL:{}", select.value().replace("#{uId}", args[0].toString()));
            return args[0] + ",小傅哥,bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!";
        };
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(),
                new Class[]{mapperInterface}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

将 Bean 定义注册到 Spring 容器 RegisterBeanFactory

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
            throws BeansException {
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(MapperFactoryBean.class);
        beanDefinition.setScope("singleton");
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(IUserDao.class);
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory)
            throws BeansException {
    }
}

配置文件 spring-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
       default-autowire="byName">
    <bean id="userDao" class="cn.bugstack.design.agent.RegisterBeanFactory"/>
</beans>

单元测试

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;

public class TestClass {
    private Logger logger = LoggerFactory.getLogger(TestClass.class);

    @Test
    public void test_IUserDao() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
        IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
        String res = userDao.queryUserInfo("100001");
        logger.info("测试结果:{}", res);
    }
}

第16章 责任链模式(无限审核模式)

小和尚们在一个接一个地挑水,你是否会想起周星驰主演的电影《回魂夜》,大家坐在海边围成一个圈,拿着一个点燃的炸弹,互相传递。

责任链模式的核心是解决一组服务中的先后执行关系,就像出差借款需要审批,5000元以下直接找部门领导、分管领导、财务部门审批,5000 元以上需要找更高一级的领导审批。

系统上线审批场景

本案例模拟在“618”大促期间,各大电商平台的业务系统上线审批流程的场景,如图16-2所示。
电商平台在“618”大促期间都会做一些运营活动,所有开发的这些系统都需要陆续上线。当临近大促时,会有一些紧急的需求需要上线,为了保障线上系统的稳定性,会相应地增强审批力度,就像一级响应、二级响应一样。在审批的过程中,在特定时间点会加入不同级别的负责人,每位负责人就像责任链模式中的一个核心点。研发人员并不需要关心具体的审批流程处理细节,只需要知道审批上线更严格、级别也更高。
接下来模拟一个业务场景,使用责任链的设计模式实现此功能。

模拟审批服务

import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class AuthService {
    private static Map<String, Date> authMap = new ConcurrentHashMap<>();

    public static Date queryAuthInfo(String uId, String orderId) {
        return authMap.get(uId.concat(orderId));
    }

    public static void auth(String uId, String orderId) {
        authMap.put(uId.concat(orderId), new Date());
    }
}

责任链中返回对象定义 AuthInfo

public class AuthInfo {
    private String code;
    private String info = "";

    public AuthInfo(String code, String... infos) {
        this.code = code;
        for (String str : infos) {
            this.info = this.info.concat(str);
        }
    }

    // ...get/set
}

链路抽象类定义 AuthLink

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.Date;

public abstract class AuthLink {
    protected Logger logger = LoggerFactory.getLogger(AuthLink.class);
    protected SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 时间格式化
    protected String levelUserId; // 级别人员ID
    protected String levelUserName; // 级别人员姓名
    private AuthLink next; // 责任链

    public AuthLink(String levelUserId, String levelUserName) {
        this.levelUserId = levelUserId;
        this.levelUserName = levelUserName;
    }

    public AuthLink next() {
        return next;
    }

    public AuthLink appendNext(AuthLink next) {
        this.next = next;
        return this;
    }

    public abstract AuthInfo doAuth(String uId, String orderId, Date authDate);
}

三个审批实现类

Level1AuthLink
import java.text.ParseException;
import java.util.Date;

public class Level1AuthLink extends AuthLink {
    private Date beginDate;
    private Date endDate;

    public Level1AuthLink(String levelUserId, String levelUserName) throws ParseException {
        super(levelUserId, levelUserName);
        this.beginDate = f.parse("2020-06-11 00:00:00");
        this.endDate = f.parse("2020-06-20 23:59:59");
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "单号:", orderId, " 状态:待一级负责人审批 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "单号:", orderId, "状态:一级负责人审批完成", "时间:", f.format(date), " 审批人:", levelUserName);
        }
        if (authDate.before(beginDate) || authDate.after(endDate)) {
            return new AuthInfo("0000", "单号:", orderId, "状态:一级负责人审批完成", "时间:", f.format(date), " 审批人:", levelUserName);
        }
        return next.doAuth(uId, orderId, authDate);
    }
}
Level2AuthLink
import java.text.ParseException;
import java.util.Date;

public class Level2AuthLink extends AuthLink {
    private Date beginDate;
    private Date endDate;

    public Level2AuthLink(String levelUserId, String levelUserName) throws ParseException {
        super(levelUserId, levelUserName);
        this.beginDate = f.parse("2020-06-01 00:00:00");
        this.endDate = f.parse("2020-06-25 23:59:59");
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "单号:", orderId, " 状态:待二级负责人审批 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "单号:", orderId, " 状态:二级负责人审批完成", " 时间:", f.format(date), " 审批人:", levelUserName);
        }
        if (authDate.before(beginDate) || authDate.after(endDate)) {
            return new AuthInfo("0000", "单号:", orderId, " 状态:二级负责人审批完成", " 时间:", f.format(date), " 审批人:", levelUserName);
        }
        return next.doAuth(uId, orderId, authDate);
    }
}
Level3AuthLink
import java.util.Date;

public class Level3AuthLink extends AuthLink {
    public Level3AuthLink(String levelUserId, String levelUserName) {
        super(levelUserId, levelUserName);
    }

    public AuthInfo doAuth(String uId, String orderId, Date authDate) {
        Date date = AuthService.queryAuthInfo(levelUserId, orderId);
        if (null == date) {
            return new AuthInfo("0001", "单号:", orderId, " 状态:待三级负责人审批 ", levelUserName);
        }
        AuthLink next = super.next();
        if (null == next) {
            return new AuthInfo("0000", "单号:", orderId, " 状态:三级负责人审批完成", " 时间:", f.format(date), " 审批人:", levelUserName);
        }
        return next.doAuth(uId, orderId, authDate);
    }
}

单元测试

import com.alibaba.fastjson.JSON;
import org.junit.Test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 假设该类为测试类
public class AuthLinkTest {
    private Logger logger = LoggerFactory.getLogger(AuthLinkTest.class);

    @Test
    public void test_AuthLink() throws ParseException {
        AuthLink authLink = new Level3AuthLink("1000013", "王工")
               .appendNext(new Level2AuthLink("1000012", "张经理")
                       .appendNext(new Level1AuthLink("1000011", "段总")));

        logger.info("测试结果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date currentDate = f.parse("2020-06-18 22:49:46");

        // 模拟三级负责人审批
        AuthService.auth("1000013", "1000998004813441");
        logger.info("测试结果:{}", "模拟三级负责人审批,王工");
        logger.info("测试结果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

        // 模拟二级负责人审批
        AuthService.auth("1000012", "1000998004813441");
        logger.info("测试结果:{}", "模拟二级负责人审批,张经理");
        logger.info("测试结果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));

        // 模拟一级负责人审批
        AuthService.auth("1000011", "1000998004813441");
        logger.info("测试结果:{}", "模拟一级负责人审批,段总");
        logger.info("测试结果:{}", JSON.toJSONString(authLink.doAuth("小傅哥", "1000998004813441", new Date())));
    }
}

第17章 命令模式

命令模式是把逻辑实现与操作请求分离,降低耦合,方便扩展。命令模式是行为模式中的一种,以数据驱动的方式将命令对象用构造函数的方式传递给调用者。调用者再提供相应的实现,为命令执行提供操作方法。

在命令模式的实现过程中,重要的有以下几点:
· 抽象命令类:声明执行命令的接口和方法;
· 具体的命令实现类:接口类的具体实现可以是一组相似的行为逻辑;
· 实现者:给命令开发执行逻辑的具体实现类;
· 调用者:处理命令、实现的具体操作者,负责对外提供命令服务。

餐厅点餐场景

本案例模拟在餐厅里点餐,并交给厨师烹饪的场景,如图17-2所示。
命令模式的核心逻辑是调用方不需要关心具体的逻辑实现。在本例中,顾客只需要把点的菜交给服务员(店小二)就可以,服务员再请厨师烹饪。顾客不需要与厨师交流,只需和服务员沟通就可以。

在这个场景中有不同的菜品,包括山东菜(鲁菜)、四川菜(川菜)、江苏菜(苏菜)和广东菜(粤菜)、福建菜(闽菜)、浙江菜(浙菜)、湖南菜(湘菜),每种菜品都由不同的厨师烹饪。而顾客不会关心具体是哪位厨师烹饪的,厨师也不会关心谁点的菜,中间的衔接工作由服务员完成。

在这个模拟场景中,可以先思考哪部分是命令模式的拆解,哪部分是命令的调用者及实现逻辑。

抽象命令定义(菜品接口)

/**
 * 菜系:山东菜(鲁菜) 四川菜(川菜) 江苏菜(苏菜) 广东菜(粤菜) 福建菜(闽菜) 浙江菜(浙菜) 湖南菜(湘菜) 安徽菜(徽菜)
 */
public interface ICuisine {
    void cook(); // 烹饪,制作
}

具体命令实现(四种菜品)

广东菜(粤菜)
public class GuangDongCuisine implements ICuisine {
    private ICook cook;

    public GuangDongCuisine(ICook cook) {
        this.cook = cook;
    }

    public void cook() {
        cook.doCooking();
    }
}
江苏菜(苏菜)
public class JiangSuCuisine implements ICuisine {
    private ICook cook;

    public JiangSuCuisine(ICook cook) {
        this.cook = cook;
    }

    public void cook() {
        cook.doCooking();
    }
}
山东菜(鲁菜)
public class ShanDongCuisine implements ICuisine {
    private ICook cook;

    public ShanDongCuisine(ICook cook) {
        this.cook = cook;
    }

    public void cook() {
        cook.doCooking();
    }
}
四川菜(川菜)
public class SiChuanCuisine implements ICuisine {
    private ICook cook;

    public SiChuanCuisine(ICook cook) {
        this.cook = cook;
    }

    public void cook() {
        cook.doCooking();
    }
}

抽象实现者定义(厨师接口)

public interface ICook {
    void doCooking();
}

实现者具体实现(四种厨师)

粤菜厨师
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GuangDongCook implements ICook {
    private Logger logger = LoggerFactory.getLogger(ICook.class);

    public void doCooking() {
        logger.info("广东厨师,烹饪粤菜,国内民间第二大菜系,国外最有影响力的菜系之一。");
    }
}
苏菜厨师
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JiangSuCook implements ICook {
    private Logger logger = LoggerFactory.getLogger(ICook.class);

    public void doCooking() {
        logger.info("江苏厨师,烹饪苏菜,宫廷第二大菜系,古今国宴上最受人欢迎的菜系之一。");
    }
}
鲁菜厨师
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ShanDongCook implements ICook {
    private Logger logger = LoggerFactory.getLogger(ICook.class);

    public void doCooking() {
        logger.info("山东厨师,烹饪鲁菜,宫廷最大菜系,以孔府风味为龙头。");
    }
}
川菜厨师
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SiChuanCook implements ICook {
    private Logger logger = LoggerFactory.getLogger(ICook.class);

    public void doCooking() {
        logger.info("四川厨师,烹饪川菜,中国最有特色的菜系,也是民间最大的菜系之一。");
    }
}

调用者(店小二)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;

public class XiaoEr {
    private Logger logger = LoggerFactory.getLogger(XiaoEr.class);
    private List<ICuisine> cuisineList = new ArrayList<>();

    public void order(ICuisine cuisine) {
        cuisineList.add(cuisine);
    }

    public synchronized void placeOrder() {
        for (ICuisine cuisine : cuisineList) {
            cuisine.cook();
        }
        cuisineList.clear();
    }
}

测试验证 单元测试

这里可以主要观察菜品与厨师的组合:new GuangDongCuisine(newGuangDongCook());,每一个具体的命令都拥有一个对应的实现类,可以组合。当定义完菜品和具体的实现后,由店小二操作点单 xiaoEr.order(guangDongCuisine);,这里分别给店小二添加了四种菜品。最后是下单,是具体命令实现的操作,相当于把店小二手里的菜单转给厨师。当然,这里也可以提供删除和撤销操作,也就是顾客取消了自己点的某个菜。

import org.junit.Test;

public class CommandPatternTest {
    @Test
    public void test_xiaoEr() {
        // 菜系 + 厨师:广东菜(粤菜)、江苏菜(苏菜)、山东菜(鲁菜)、四川菜(川菜)
        ICuisine guangDongCuisine = new GuangDongCuisine(new GuangDongCook());
        JiangSuCuisine jiangSuCuisine = new JiangSuCuisine(new JiangSuCook());
        ShanDongCuisine shanDongCuisine = new ShanDongCuisine(new ShanDongCook());
        SiChuanCuisine siChuanCuisine = new SiChuanCuisine(new SiChuanCook());

        // 点单
        XiaoEr xiaoEr = new XiaoEr();
        xiaoEr.order(guangDongCuisine);
        xiaoEr.order(jiangSuCuisine);
        xiaoEr.order(shanDongCuisine);
        xiaoEr.order(siChuanCuisine);

        // 下单
        xiaoEr.placeOrder();
    }
}

第18章 迭代器模式

军训中队员依次报数,就是一个迭代的过程。其实迭代器模式就像日常使用的Iterator方法遍历。虽然这种设计模式在实际业务开发中用得并不多,但却要使用JDK提供的list集合遍历。另外,增强的for循环语句虽然是循环输出数据,但并不是迭代器模式。迭代器模式的特点是实现Iterable接口,通过next方式获取集合元素,同时具备删除元素等操作功能;而增强的for循环语句是无法实现的。

迭代器模式的优点是能够以相同的方式遍历不同的数据结构元素,这些数据结构包括:数组、链表和树等。而用户在使用遍历时,并不需要关心每一种数据结构的遍历处理逻辑,做到让使用变得统一易用。

在实现迭代器模式之前,可以先阅读Java中list集合关于iterator方法的实现部分,大部分迭代器开发都会按照此模式实现。迭代器模式主要分为以下几块:
1· Collection:集合方法部分用于对自定义的数据结构添加通用方法,包括 add、remove、iterator 等核心方法。
2· Iterable:提供获取迭代器,这个接口类会被 Collection 继承。
3· Iterator:提供了两个方法的定义,包括 hasNext、next,会在具体的数据结构中编写实现方式。

组织架构树形结构遍历场景

本案例模拟迭代遍历,并输出公司中具有树形结构的组织架构关系中的雇员列表,如图18-2所示。
大部分公司的组织架构呈金字塔结构,也就是树形结构,分为一级、二级和三级等部门,每个组织部门由雇员填充,最终呈现出一个整体的树形组织架构。一般常用的遍历采用JDK默认提供的方法,对list集合遍历。但是对于业务特性较大的树形结构,如果需要使用遍历方法,可以自己实现。接下来会把以上组织层次关系用树形数据结构实现,并完成迭代器功能。

雇员实体类

/**
 * 雇员
 */
public class Employee {
    private String uId;   // ID
    private String name;  // 姓名
    private String desc;  // 备注

    // ..get/set
}

树节点链路

/**
 * 树节点链路
 */
public class Link {
    private String fromId; // 雇员ID
    private String toId;   // 雇员ID

    // ...get/set
}

迭代器定义


public interface Iterator<E> {
    boolean hasNext();
    E next();
}

可迭代接口定义


public interface Iterable<E> {
    Iterator<E> iterator();
}

集合功能接口定义


public interface Collection<E, L> extends Iterable<E> {
    boolean add(E e);
    boolean remove(E e);
    boolean addLink(String key, L l);
    boolean removeLink(String key);
    Iterator<E> iterator();
}

迭代器功能实现

这里总结迭代器的实现思路:
1· 对于树形结构,需要做的是深度遍历,也就是对左侧一直遍历,直至遍历到最深的节点。
2· 当遍历到最深的节点后,开始遍历它的横向节点。
3· 当遍历完成横向节点后,则向顶部寻找还未遍历的横向节点,直至树形结构全部遍历完成。

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class GroupStructure implements Collection<Employee, Link> {
    private String groupId;               // 组织 ID,也是一个组织链的头部ID
    private String groupName;             // 组织名称
    private Map<String, Employee> employeeMap = new ConcurrentHashMap<>();    // 雇员列表
    private Map<String, List<Link>> linkMap = new ConcurrentHashMap<>();     // 组织架构关系:id->list
    private Map<String, String> invertedMap = new ConcurrentHashMap<>();     // 反向关系链

    public GroupStructure(String groupId, String groupName) {
        this.groupId = groupId;
        this.groupName = groupName;
    }

    public boolean add(Employee employee) {
        return employeeMap.put(employee.getuId(), employee) != null;
    }

    public boolean remove(Employee o) {
        return employeeMap.remove(o.getuId()) != null;
    }

    public boolean addLink(String key, Link link) {
        invertedMap.put(link.getToId(), link.getFromId());
        if (linkMap.containsKey(key)) {
            return linkMap.get(key).add(link);
        } else {
            List<Link> links = new LinkedList<>();
            links.add(link);
            linkMap.put(key, links);
            return true;
        }
    }

    public boolean removeLink(String key) {
        return linkMap.remove(key) != null;
    }

    public Iterator<Employee> iterator() {
        return new Iterator<Employee>() {
            HashMap<String, Integer> keyMap = new HashMap<>();
            int totalIdx = 0;
            private String fromId = groupId;  // 雇员 ID,From
            private String toId = groupId;    // 雇员 ID,To

            public boolean hasNext() {
                return totalIdx < employeeMap.size();
            }

            public Employee next() {
                List<Link> links = linkMap.get(toId);
                int cursorIdx = getCursorIdx(toId);
                // 同级节点扫描
                if (links == null) {
                    cursorIdx = getCursorIdx(fromId);
                    links = linkMap.get(fromId);
                }
                // 上级节点扫描
                while (cursorIdx > links.size() - 1) {
                    fromId = invertedMap.get(fromId);
                    cursorIdx = getCursorIdx(fromId);
                    links = linkMap.get(fromId);
                }
                // 获取节点
                Link link = links.get(cursorIdx);
                toId = link.getToId();
                fromId = link.getFromId();
                totalIdx++;
                // 返回结果
                return employeeMap.get(link.getToId());
            }

            // 给每个层级定义宽度,遍历进度
            public int getCursorIdx(String key) {
                int idx = 0;
                if (keyMap.containsKey(key)) {
                    idx = keyMap.get(key);
                    keyMap.put(key, ++idx);
                } else {
                    keyMap.put(key, idx);
                }
                return idx;
            }
        };
    }
}

单元测试

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Iterator;

public class GroupStructureTest {
    private Logger logger = LoggerFactory.getLogger(GroupStructureTest.class);

    @Test
    public void test_iterator() {
        // 数据填充
        GroupStructure groupStructure = new GroupStructure("1", "小傅哥");
        // 雇员信息
        groupStructure.add(new Employee("2", "花花", "二级部门"));
        groupStructure.add(new Employee("3", "豆包", "二级部门"));
        groupStructure.add(new Employee("4", "蹦蹦", "三级部门"));
        groupStructure.add(new Employee("5", "大烧", "三级部门"));
        groupStructure.add(new Employee("6", "虎哥", "四级部门"));
        groupStructure.add(new Employee("7", "玲姐", "四级部门"));
        groupStructure.add(new Employee("8", "秋雅", "四级部门"));
        // 节点关系 1->(1,2) 2->(4,5)
        groupStructure.addLink("1", new Link("1", "2"));
        groupStructure.addLink("1", new Link("1", "3"));
        groupStructure.addLink("2", new Link("2", "4"));
        groupStructure.addLink("2", new Link("2", "5"));
        groupStructure.addLink("5", new Link("5", "6"));
        groupStructure.addLink("5", new Link("5", "7"));
        groupStructure.addLink("5", new Link("5", "8"));

        Iterator<Employee> iterator = groupStructure.iterator();
        while (iterator.hasNext()) {
            Employee employee = iterator.next();
            logger.info("{},雇员 Id:{} Name:{}", employee.getDesc(), employee.getuId(), employee.getName());
        }
    }
}

第19章 中介者模式

交警站在道路中间指挥过往车辆,避免拥堵,这就像是一个介入道路的中介者。中介者的作用是,当复杂功能应用之间重复调用时,在中间添加一层中介者包装服务,对外提供简单、通用和易扩展的服务能力。

中介者模式开发ORM框架

中介者模式开发ORM框架

//定义SqlSession接口
public interface SqlSession {
  <T> T selectOne(String statement);

//SqlSession具体实现类
public class DefaultSqlSession implements SqlSession {
  @Override    
  public <T> T selectOne(String statement) {

定义SqlSessionFactory接口
public interface SqlSessionFactory {
    SqlSession openSession();}

SqlSessionFactory具体实现类
public class DefaultSqlSessionFactory implements SqlSessionFactory {
  @Override    
  public SqlSession openSession() {
  
SqlSessionFactoryBuilder实现
public class SqlSessionFactoryBuilder {
    public DefaultSqlSessionFactory build(Reader reader) {

ORM框架测试

1.用户类
public class User {
    private Long id;
    private String name;
2.学校类
public class School {
    private Long id;
    private String name;
}
用户Dao
public interface IUserDao {
     User queryUserInfoById(Long id);
}
学校Dao
public interface ISchoolDao {
    School querySchoolInfoById(Long treeId);
}

单元测试

这里的使用方式和MyBatis一样,包括资源加载和解析、构建SqlSession工厂和开启SqlSession,以及执行查询操作selectOne。

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import org.junit.Test;

// 假设存在 User 类和 IUserDao 接口
class User {
    // User 类的属性和方法
}

interface IUserDao {
    // IUserDao 接口的方法
}

public class TestClass {
    private static final Logger logger = LoggerFactory.getLogger(TestClass.class);

    @Test
    public void test_queryUserInfoById() {
        String resource = "mybatis-config-datasource.xml";
        java.io.Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            SqlSession session = sqlMapper.openSession();
            try {
                User user = session.selectOne("cn.bugstack.design.dao.IUserDao.queryUserInfoById", 1L);
                logger.info("测试结果:{}", JSON.toJSONString(user));
            } finally {
                session.close();
                reader.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import org.junit.Test;

import java.io.Reader;
import java.util.List;

// 假设 User 类存在
class User {
    private Integer age;

    public void setAge(Integer age) {
        this.age = age;
    }

    // 可以添加其他必要的属性和方法
}

// 假设 IUserDao 接口存在
interface IUserDao {
    // 可以添加其他必要的方法
    List<User> queryUserList(User req);
}

public class MyTestClass {
    private static final Logger logger = LoggerFactory.getLogger(MyTestClass.class);

    @Test
    public void test_queryUserList() {
        String resource = "mybatis-config-datasource.xml";
        Reader reader;
        try {
            reader = Resources.getResourceAsReader(resource);
            SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
            SqlSession session = sqlMapper.openSession();
            try {
                User req = new User();
                req.setAge(18);
                List<User> userList = session.selectList("cn.bugstack.design.dao.IUserDao.queryUserList", req);
                logger.info("测试结果:{}", JSON.toJSONString(userList));
            } finally {
                session.close();
                reader.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

第20章 备忘录模式

备忘录模式是以可恢复或回滚配置、以版本为核心功能的设计模式,这种设计模式属于行为模式。在功能实现上,是以不破坏原对象为基础增加备忘录操作类,记录原对象的行为,从而实现备忘录模式。通过照相机拍照的方式,记录出生、上学、结婚和生子的各个阶段。

这种设计模式在日常生活或者开发中也比较常见,如后悔药、孟婆汤(一下回滚到0)、IDEA编辑和撤销,以及小霸王游戏机存档。当然,还有Photoshop软件中的历史记录等.

系统上线配置回滚场景
本案例模拟系统在发布上线的过程中记录线上配置文件用于紧急回滚

配置信息类

public class ConfigFile {
    private String versionNo; // 版本号
    private String content;   // 配置内容
    private Date dateTime;    // 配置时间
    private String operator;  // 操作人
}

备忘录类

public class ConfigMemento {
    private ConfigFile configFile;

    public ConfigMemento(ConfigFile configFile) {
        this.configFile = configFile;
    }

    public ConfigFile getConfigFile() {
        return configFile;
    }

    public void setConfigFile(ConfigFile configFile) {
        this.configFile = configFile;
    }
}

备忘录类是对原有配置类的扩展,可以设置和获取配置信息。

记录者类

public class ConfigOriginator {
    private ConfigFile configFile;

    public ConfigFile getConfigFile() {
        return configFile;
    }

    public void setConfigFile(ConfigFile configFile) {
        this.configFile = configFile;
    }

    public ConfigMemento saveMemento() {
        return new ConfigMemento(configFile);
    }

    public void getMemento(ConfigMemento memento) {
        this.configFile = memento.getConfigFile();
    }
}

管理员类

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Admin {
    private int cursorIdx = 0;
    private List<ConfigMemento> mementoList = new ArrayList<>();
    private Map<String, ConfigMemento> mementoMap = new ConcurrentHashMap<>();

    public void append(ConfigMemento memento) {
        mementoList.add(memento);
        mementoMap.put(memento.getConfigFile().getVersionNo(), memento);
        cursorIdx++;
    }

    public ConfigMemento undo() {
        if (--cursorIdx <= 0) {
            return mementoList.get(0);
        }
        return mementoList.get(cursorIdx);
    }

    public ConfigMemento redo() {
        if (++cursorIdx > mementoList.size()) {
            return mementoList.get(mementoList.size() - 1);
        }
        return mementoList.get(cursorIdx);
    }

    public ConfigMemento get(String versionNo) {
        return mementoMap.get(versionNo);
    }
}

单元测试

其实就是缓存每一次的快照数据,存入map,用索引判断当前版本与历史版本。

import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;

public class MementoPatternTest {
    private static final Logger logger = LoggerFactory.getLogger(MementoPatternTest.class);

    @Test
    public void test() {
        Admin admin = new Admin();
        ConfigOriginator configOriginator = new ConfigOriginator();

        configOriginator.setConfigFile(new ConfigFile("1000001", "配置内容A=哈哈", new Date(), "小傅哥"));
        admin.append(configOriginator.saveMemento()); // 保存配置

        configOriginator.setConfigFile(new ConfigFile("1000002", "配置内容A=嘻嘻", new Date(), "小傅哥"));
        admin.append(configOriginator.saveMemento()); // 保存配置

        configOriginator.setConfigFile(new ConfigFile("1000003", "配置内容A=么么", new Date(), "小傅哥"));
        admin.append(configOriginator.saveMemento()); // 保存配置

        configOriginator.setConfigFile(new ConfigFile("1000004", "配置内容A=嘿嘿", new Date(), "小傅哥"));
        admin.append(configOriginator.saveMemento()); // 保存配置

        // 历史配置(回滚)
        configOriginator.getMemento(admin.undo());
        logger.info("历史配置(回滚)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));

        // 历史配置(回滚)
        configOriginator.getMemento(admin.undo());
        logger.info("历史配置(回滚)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));

        // 历史配置(前进)
        configOriginator.getMemento(admin.redo());
        logger.info("历史配置(前进)redo:{}", JSON.toJSONString(configOriginator.getConfigFile()));

        // 历史配置(获取)
        configOriginator.getMemento(admin.get("1000002"));
        logger.info("历史配置(获取)get:{}", JSON.toJSONString(configOriginator.getConfigFile()));
    }
}

第21章 观察者模式(单机版-事件的发布与监听模式)

简单来讲,观察者模式是指当一个行为发生时,一个用户传递信息,另一个用户接收信息并做出相应的处理,行为和接收者之间没有直接的耦合关联。比如,霍去病通过烽火台传递的消息,做出防御匈奴的命令。

狙击手和观察员之间的关系,观察员协助狙击手在第一时间找到目标,并将射击参数告诉狙击手,狙击手根据这些信息进行射击

小客车摇号通知场景

本案例模拟每次小客车摇号通知场景。

监听事件接口定义

public interface EventListener {
    void doEvent(LotteryResult result);
}

两个监听事件的实现

短消息事件

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MessageEventListener implements EventListener {
    private Logger logger = LoggerFactory.getLogger(MessageEventListener.class);

    @Override
    public void doEvent(LotteryResult result) {
        logger.info("给用户 {} 发送通知(短信):{}", result.getuId(), result.getMsg());
    }
}

MQ 发送事件

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MQEventListener implements EventListener {
    private Logger logger = LoggerFactory.getLogger(MQEventListener.class);

    @Override
    public void doEvent(LotteryResult result) {
        logger.info("记录用户 {} 摇号结果(MQ):{}", result.getuId(), result.getMsg());
    }
}

事件处理类

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class EventManager {
    Map<EventType, List<EventListener>> listeners = new HashMap<>();

    public EventManager(EventType... operations) {
        for (EventType operation : operations) {
            this.listeners.put(operation, new ArrayList<>());
        }
    }

    public enum EventType {
        MQ, Message
    }

    /**
     * 订阅
     * @param eventType 事件类型
     * @param listener  监听
     */
    public void subscribe(EventType eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.add(listener);
    }

    /**
     * 取消订阅
     * @param eventType 事件类型
     * @param listener  监听
     */
    public void unsubscribe(EventType eventType, EventListener listener) {
        List<EventListener> users = listeners.get(eventType);
        users.remove(listener);
    }

    /**
     * 通知
     * @param eventType 事件类型
     * @param result    结果
     */
    public void notify(EventType eventType, LotteryResult result) {
        List<EventListener> users = listeners.get(eventType);
        for (EventListener listener : users) {
            listener.doEvent(result);
        }
    }
}

业务抽象类接口

import java.util.Date;

public abstract class LotteryService {
    private EventManager eventManager;

    public LotteryService() {
        eventManager = new EventManager(EventManager.EventType.MQ, EventManager.EventType.Message);
        eventManager.subscribe(EventManager.EventType.MQ, new MQEventListener());
        eventManager.subscribe(EventManager.EventType.Message, new MessageEventListener());
    }

    public LotteryResult draw(String uId) {
        LotteryResult lotteryResult = doDraw(uId);
        // 需要什么通知就调用什么方法
        eventManager.notify(EventManager.EventType.MQ, lotteryResult);
        eventManager.notify(EventManager.EventType.Message, lotteryResult);
        return lotteryResult;
    }

    protected abstract LotteryResult doDraw(String uId);
}

业务接口实现类

import java.util.Date;

public class LotteryServiceImpl extends LotteryService {
    private MinibusTargetService minibusTargetService = new MinibusTargetService();

    @Override
    protected LotteryResult doDraw(String uId) {
        // 摇号
        String lottery = minibusTargetService.lottery(uId);
        // 结果
        return new LotteryResult(uId, lottery, new Date());
    }
}

单元测试

import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LotteryTest {
    private static final Logger logger = LoggerFactory.getLogger(LotteryTest.class);

    @Test
    public void test_draw() {
        LotteryService lotteryService = new LotteryServiceImpl();
        LotteryResult result = lotteryService.draw("2765789109876");
        logger.info("测试结果:{}", JSON.toJSONString(result));
    }
}

第22章 状态模式(基础层各个状态的变更重构)

状态模式描述的是一个行为下的多种状态变更。例如,常见的一个网站的页面,在登录与不登录状态下展示的内容是略有差异的,如不登录不能展示个人信息。

活动审批状态流转场景

本案例模拟营销活动审批状态流转场景,即一个活动是由多个层级审批上线的,如图22-3所示。

可以看到流程节点包括了各个状态到下一个状态的关联条件,比如审批通过才能到活动中,而不能从编辑中直接到活动中,而这些状态的转变就是要完成的场景。
很多程序员都开发过类似的业务,需要对活动或者一些配置进行审批才能对外发布,而审批的过程往往会随着系统的重要程度的提高而设立多级控制,以保证一个活动可以安全上线,避免造成资损。当然,有时会用一些审批流的过程配置,也非常便于开发类似的流程,可以在配置中设定某个节点的审批人员。但这不是本章要体现的知识点,这里主要是模拟对一个活动的多个状态节点的审批控制。

状态模式和状态流程审批类关系如图所示。

其中,State 是一个抽象类,定义了各种操作接口,包括提审、审批和拒审等。右侧的不同状态与图中保持一致,是各种状态流程流转的实现操作。这里有一个关键点,对于每一种状态到下一个状态,都设置为在各个实现方法中控制,不需要if语句判断。最后,StateHandler对状态流程统一处理,里面提供Map结构的各项服务接口调用,避免使用 if语句判断状态转变的流程。

定义状态抽象类

public abstract class State {
    /**
     * 活动提审
     *
     * @param activityId    活动 ID
     * @param currentStatus 当前状态
     * @return 执行结果
     */
    public abstract Result arraignment(String activityId, Enum<Status> currentStatus);

    /**
     * 审批通过
     *
     * @param activityId    活动 ID
     * @param currentStatus 当前状态
     * @return 执行结果
     */
    public abstract Result checkPass(String activityId, Enum<Status> currentStatus);

    /**
     * 审批拒绝
     *
     * @param activityId    活动 ID
     * @param currentStatus 当前状态
     * @return 执行结果
     */
    public abstract Result checkRefuse(String activityId, Enum<Status> currentStatus);

    /**
     * 撤审撤销
     *
     * @param activityId    活动 ID
     * @param currentStatus 当前状态
     * @return 执行结果
     */
    public abstract Result checkRevoke(String activityId, Enum<Status> currentStatus);

    /**
     * 活动关闭
     *
     * @param activityId    活动 ID
     * @param currentStatus 当前状态
     * @return 执行结果
     */
    public abstract Result close(String activityId, Enum<Status> currentStatus);

    /**
     * 活动开启
     *
     * @param activityId    活动 ID
     * @param currentStatus 当前状态
     * @return 执行结果
     */
    public abstract Result open(String activityId, Enum<Status> currentStatus);

    /**
     * 活动执行
     *
     * @param activityId    活动 ID
     * @param currentStatus 当前状态
     * @return 执行结果
     */
    public abstract Result doing(String activityId, Enum<Status> currentStatus);
}

整个接口提供了各项状态流转服务的接口,例如活动提审、审批通过、审批拒绝和撤审撤销等 7 个方法。在这些方法中,所有的入参都是一样的,即 activityId(活动 ID)、currentStatus(当前状态),只有各自的具体实现方式是不同的。

部分状态流转实现

编辑状态

public class EditingState extends State {
    public Result arraignment(String activityId, Enum<Status> currentStatus) {
        ActivityService.execStatus(activityId, currentStatus, Status.Check);
        return new Result("0000", "活动提审成功");
    }

    public Result checkPass(String activityId, Enum<Status> currentStatus) {
        return new Result("0001", "编辑中不可审批通过");
    }

    public Result checkRefuse(String activityId, Enum<Status> currentStatus) {
        return new Result("0001", "编辑中不可审批拒绝");
    }

    @Override
    public Result checkRevoke(String activityId, Enum<Status> currentStatus) {
        return new Result("0001", "编辑中不可撤销审批");
    }

    public Result close(String activityId, Enum<Status> currentStatus) {
        ActivityService.execStatus(activityId, currentStatus, Status.Close);
        return new Result("0000", "活动关闭成功");
    }

    public Result open(String activityId, Enum<Status> currentStatus) {
        return new Result("0001", "非关闭活动不可开启");
    }

    public Result doing(String activityId, Enum<Status> currentStatus) {
        return new Result("0001", "编辑中活动不可执行活动中变更");
    }
}

提审状态

public class CheckState extends State {
    public Result arraignment(String activityId, Enum<Status> currentStatus) {
        return new Result("0001", "待审批状态不可重复提审");
    }

    public Result checkPass(String activityId, Enum<Status> currentStatus) {
        ActivityService.execStatus(activityId, currentStatus, Status.Pass);
        return new Result("0000", "活动审批通过完成");
    }

    public Result checkRefuse(String activityId, Enum<Status> currentStatus) {
        ActivityService.execStatus(activityId, currentStatus, Status.Refuse);
        return new Result("0000", "活动审批拒绝完成");
    }

    @Override
    public Result checkRevoke(String activityId, Enum<Status> currentStatus) {
        ActivityService.execStatus(activityId, currentStatus, Status.Editing);
        return new Result("0000", "活动审批撤销回到编辑中");
    }

    public Result close(String activityId, Enum<Status> currentStatus) {
        ActivityService.execStatus(activityId, currentStatus, Status.Close);
        return new Result("0000", "活动审批关闭完成");
    }

    public Result open(String activityId, Enum<Status> currentStatus) {
        return new Result("0001", "非关闭活动不可开启");
    }

    public Result doing(String activityId, Enum<Status> currentStatus) {
        return new Result("0001", "待审批活动不可执行活动中变更");
    }
}

这里提供两个具体实现类的内容 —— 编辑状态和提审状态。例如,在这两个实现类中,checkRefuse 方法对不同的类有不同的实现,也就是在不同状态下能做的下一步流转操作已经可以在每一个方法中控制了。其他五个类的操作是类似的,具体不再赘述,可以通过阅读源码学习。

状态处理服务

import java.util.concurrent.ConcurrentHashMap;

public class StateHandler {
    private Map<Enum<Status>, State> stateMap = new ConcurrentHashMap<>();

    public StateHandler() {
        stateMap.put(Status.Check, new CheckState());     // 待审批
        stateMap.put(Status.Close, new CloseState());     // 已关闭
        stateMap.put(Status.Doing, new DoingState());     // 活动中
        stateMap.put(Status.Editing, new EditingState()); // 编辑中
        stateMap.put(Status.Open, new OpenState());       // 已开启
        stateMap.put(Status.Pass, new PassState());       // 审批通过
        stateMap.put(Status.Refuse, new RefuseState());   // 审批拒绝
    }

    public Result arraignment(String activityId, Enum<Status> currentStatus) {
        return stateMap.get(currentStatus).arraignment(activityId, currentStatus);
    }

    public Result checkPass(String activityId, Enum<Status> currentStatus) {
        return stateMap.get(currentStatus).checkPass(activityId, currentStatus);
    }

    public Result checkRefuse(String activityId, Enum<Status> currentStatus) {
        return stateMap.get(currentStatus).checkRefuse(activityId, currentStatus);
    }

    public Result checkRevoke(String activityId, Enum<Status> currentStatus) {
        return stateMap.get(currentStatus).checkRevoke(activityId, currentStatus);
    }

    public Result close(String activityId, Enum<Status> currentStatus) {
        return stateMap.get(currentStatus).close(activityId, currentStatus);
    }

    public Result open(String activityId, Enum<Status> currentStatus) {
        return stateMap.get(currentStatus).open(activityId, currentStatus);
    }

    public Result doing(String activityId, Enum<Status> currentStatus) {
        return stateMap.get(currentStatus).doing(activityId, currentStatus);
    }
}

编辑中到提审活动测试验证

单元测试
import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ApiTest {
    private static final Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test_Editing2Arraignment() {
        String activityId = "100001";
        ActivityService.init(activityId, Status.Editing);
        StateHandler stateHandler = new StateHandler();
        Result result = stateHandler.arraignment(activityId, Status.Editing);
        logger.info("测试结果(编辑中To提审活动):{}", JSON.toJSONString(result));
        logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)),
                JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
    }
}

编辑中到开启活动测试验证

单元测试
import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ApiTest {
    private static final Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test_Editing2Open() {
        String activityId = "100001";
        ActivityService.init(activityId, Status.Editing);
        StateHandler stateHandler = new StateHandler();
        Result result = stateHandler.open(activityId, Status.Editing);
        logger.info("测试结果(编辑中To开启活动):{}", JSON.toJSONString(result));
        logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)),
                JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
    }
}

审批拒绝到活动中测试验证

单元测试
import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ApiTest {
    private static final Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test_Refuse2Doing() {
        String activityId = "100001";
        ActivityService.init(activityId, Status.Refuse);
        StateHandler stateHandler = new StateHandler();
        Result result = stateHandler.doing(activityId, Status.Refuse);
        logger.info("测试结果(拒绝To活动中):{}", JSON.toJSONString(result));
        logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)),
                JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
    }
}

审核拒绝到撤审测试验证

单元测试
import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ApiTest {
    private static final Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test_Refuse2Revoke() {
        String activityId = "100001";
        ActivityService.init(activityId, Status.Refuse);
        StateHandler stateHandler = new StateHandler();
        Result result = stateHandler.checkRevoke(activityId, Status.Refuse);
        logger.info("测试结果(拒绝To撤审):{}", JSON.toJSONString(result));
        logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)),
                JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
    }
}

第23章 策略模式

策略模式是一种行为模式,也是替代if…else的利器。它能解决的场景一般包括具有同类可替代的行为逻辑算法,例如:不同类型的交易方式(信用卡、支付宝、微信)、生成唯一 ID策略(UUID、DB自增、DB+Redis、雪花算法和Leaf算法)等。
就像三国演义中诸葛亮给赵子龙的锦囊:
· 第一个锦囊:见乔国老,让刘备娶亲的事情在东吴人尽皆知。
· 第二个锦囊:用谎言(曹操打荆州)让在温柔乡里的刘备回国。
· 第三个锦囊:让孙夫人拦住东吴的追兵。

各类营销优惠券场景
本案例模拟在购买商品时使用的各种类型的优惠券,包括满减、直减、折扣和 N
元购等,如图23-2所示。
这个场景模拟日常购物省钱渠道,在购买商品时使用优惠券。在大促时,会有更多的优惠券,需要计算哪些商品一起购买更加优惠。实现此功能并不容易,因为里面包括了很多的规则和优惠逻辑,可以模拟其中一种计算优惠的方式,使用策略模式实现。

优惠券接口
public interface ICouponDiscount<T> {
    /**
     * 优惠券金额计算
     * @param couponInfo 优惠券折扣信息:直减、满减、折扣和N元购
     * @param skuPrice   SKU 金额
     * @return           优惠后金额
     */
    java.math.BigDecimal discountAmount(T couponInfo, java.math.BigDecimal skuPrice);
}

策略模式上下文Context
Context 类起到了策略模式中上下文(Context)的作用。策略模式是一种行为设计模式,它定义了一系列的算法,并将每个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。

// 假设Context类已定义
class Context<T> {
    private ICouponDiscount<T> couponDiscount;

    public Context(ICouponDiscount<T> couponDiscount) {
        this.couponDiscount = couponDiscount;
    }

    public BigDecimal discountAmount(T couponInfo, BigDecimal skuPrice) {
        return couponDiscount.discountAmount(couponInfo, skuPrice);
    }
}

优惠券接口实现

1. 满减
import java.math.BigDecimal;
import java.util.Map;

public class MJCouponDiscount implements ICouponDiscount<Map<String, String>> {
    /**
     * 满减计算
     * 1. 判断满足x元后 - N元,否则不减
     * 2. 最低支付金额1元
     */
    @Override
    public BigDecimal discountAmount(Map<String, String> couponInfo, BigDecimal skuPrice) {
        String x = couponInfo.get("x");
        String n = couponInfo.get("n");
        // 小于商品金额条件,直接返回商品原价
        if (skuPrice.compareTo(new BigDecimal(x)) < 0) {
            return skuPrice;
        }
        // 减去优惠金额判断
        BigDecimal discountAmount = skuPrice.subtract(new BigDecimal(n));
        if (discountAmount.compareTo(BigDecimal.ZERO) < 1) {
            return BigDecimal.ONE;
        }
        return discountAmount;
    }
}
2. 直减
import java.math.BigDecimal;

public class ZJCouponDiscount implements ICouponDiscount<Double> {
    /**
     * 直减计算
     * 1. 使用商品价格减去优惠价格
     * 2. 最低支付金额1元
     */
    @Override
    public BigDecimal discountAmount(Double couponInfo, BigDecimal skuPrice) {
        BigDecimal discountAmount = skuPrice.subtract(new BigDecimal(couponInfo));
        if (discountAmount.compareTo(BigDecimal.ZERO) < 1) {
            return BigDecimal.ONE;
        }
        return discountAmount;
    }
}
3. 折扣
import java.math.BigDecimal;

public class ZKCouponDiscount implements ICouponDiscount<Double> {
    /**
     * 折扣计算
     * 1. 使用商品价格乘以折扣比例,为最后支付金额
     * 2. 保留两位小数
     * 3. 最低支付金额1元
     */
    @Override
    public BigDecimal discountAmount(Double couponInfo, BigDecimal skuPrice) {
        BigDecimal discountAmount = skuPrice.multiply(new BigDecimal(couponInfo))
                .setScale(2, BigDecimal.ROUND_HALF_UP);
        if (discountAmount.compareTo(BigDecimal.ZERO) < 1) {
            return BigDecimal.ONE;
        }
        return discountAmount;
    }
}
4. N 元购
import java.math.BigDecimal;

public class NYGCouponDiscount implements ICouponDiscount<Double> {
    /**
     * N元购购买
     * 1. 无论原价多少,都用固定金额购买
     */
    @Override
    public BigDecimal discountAmount(Double couponInfo, BigDecimal skuPrice) {
        return new BigDecimal(couponInfo);
    }
}

单元测试

// 测试类
public class CouponTest {
    private static final Logger logger = Logger.getLogger(CouponTest.class.getName());
    
    @Test
    public void test_zj() {
        // 直减:满100元减10元,商品价格100元
        Context<Double> context = new Context<>(new ZJCouponDiscount());
        BigDecimal discountAmount = context.discountAmount(10D, new BigDecimal(100));
        logger.info("测试结果:直减优惠后金额 " + discountAmount);
    }

    @Test
    public void test_mj() {
        // 满100元减10元,商品价格100元
        Context<Map<String, String>> context = new Context<>(new MJCouponDiscount());
        Map<String, String> mapReq = new HashMap<>();
        mapReq.put("x", "100");
        mapReq.put("n", "10");
        BigDecimal discountAmount = context.discountAmount(mapReq, new BigDecimal(100));
        logger.info("测试结果:满减优惠后金额 " + discountAmount);
    }

    @Test
    public void test_zk() {
        // 折扣9折,商品价格100元
        Context<Double> context = new Context<>(new ZKCouponDiscount());
        BigDecimal discountAmount = context.discountAmount(0.9D, new BigDecimal(100));
        logger.info("测试结果:折扣9折后金额 " + discountAmount);
    }

    @Test
    public void test_nyg() {
        // N元购;满100元减10元,商品价格100元
        Context<Double> context = new Context<>(new NYGCouponDiscount());
        BigDecimal discountAmount = context.discountAmount(90D, new BigDecimal(100));
        logger.info("测试结果:N元购优惠后金额 " + discountAmount);
    }
}

应用思考

Context 类存在的意义

封装策略选择逻辑
class CouponContext {
    public BigDecimal calculateDiscount(int memberLevel, BigDecimal skuPrice) {
        ICouponDiscount<?> couponDiscount;
        if (memberLevel == 1) {
            couponDiscount = new ZKCouponDiscount();
            return ((ZKCouponDiscount) couponDiscount).discountAmount(0.9D, skuPrice);
        } else if (memberLevel == 2) {
            Map<String, String> couponInfo = new HashMap<>();
            couponInfo.put("x", "200");
            couponInfo.put("n", "20");
            couponDiscount = new MJCouponDiscount();
            return ((MJCouponDiscount) couponDiscount).discountAmount(couponInfo, skuPrice);
        }
        // 默认策略
        return skuPrice;
    }
}

客户端代码只需调用 CouponContext 的 calculateDiscount 方法,无需关心具体的策略选择逻辑:
CouponContext context = new CouponContext();
BigDecimal result = context.calculateDiscount(1, new BigDecimal(100));

第24章 模板模式(普通的接口编程)

模板模式的核心设计思路是通过在抽象类中定义抽象方法的执行顺序,并将抽象方法设定为只有子类实现,但不设计独立访问的方法。简单地说,就是安排得明明白白。

就像《西游记》的九九八十一难,基本每一关都是:师傅被掳走、徒弟打妖怪、妖怪被收走。无论每一难会出现什么妖怪,徒弟们怎样制服妖怪,神仙怎样收走妖怪,具体的每一难都由观音菩萨安排,只需要定义执行顺序和基本策略。

模拟爬虫商品生成海报信息场景
模板模式的核心在于由抽象类定义抽象方法并执行策略,也就是父类规定一系列的执行标准,这些标准串联成一整套的业务流程,如图24-2所示。

本案例模拟爬取各类电商商品的商品信息,生成营销推广海报(海报中含有个人的邀请码),赚取商品返利的场景(这里是模拟爬取,并没有真正爬取)。

而整个爬取过程分为三步:模拟登录、爬取信息和生成海报。因为有些商品只有登录后才可以爬取,并且登录后可以看到一些特定的价格,这与未登录用户看到的价格不同。不同电商网站的爬取方式不同,解析方式也不同,因此可以作为每一个实现类中的特定实现。生成海报的步骤基本是一样的,但会有特定的商品来源标识。所以这三步可以使用模板模式设定,并有具体的场景做子类实现。

定义执行顺序的抽象类

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
import com.alibaba.fastjson.JSON;

// 基础电商推广服务
// 1. 生成最优价商品海报
// 2. 海报含带推广邀请码
public abstract class NetMall {
    protected Logger logger = LoggerFactory.getLogger(NetMall.class);
    String uId; // 用户ID
    String uPwd; // 用户密码

    public NetMall(String uId, String uPwd) {
        this.uId = uId;
        this.uPwd = uPwd;
    }

    /**
     * 生成商品推广海报
     * 
     * @param skuUrl 商品地址(京东、淘宝、当当)
     * @return 海报图片base64位信息
     */
    public String generateGoodsPoster(String skuUrl) {
        if (!login(uId, uPwd))
            return null;
        // 1. 模拟登录
        Map<String, String> reptile = reptile(skuUrl); // 2. 爬取信息
        return createBase64(reptile); // 3. 生成海报
    }

    // 模拟登录
    protected abstract Boolean login(String uId, String uPwd);

    // 爬取信息(登录后的优惠价格)
    protected abstract Map<String, String> reptile(String skuUrl);

    // 生成海报
    protected abstract String createBase64(Map<String, String> goodsInfo);
}
模拟爬取商品实现类
// 模拟爬取京东商城商品
class HttpClient {
    public static String doGet(String skuUrl) {
        return ""; // 简单模拟返回空字符串,实际应实现网络请求逻辑
    }
}

public class JDNetMall extends NetMall {
    public JDNetMall(String uId, String uPwd) {
        super(uId, uPwd);
    }

    public Boolean login(String uId, String uPwd) {
        logger.info("模拟京东用户登录 uId:{} uPwd:{}", uId, uPwd);
        return true;
    }

    public Map<String, String> reptile(String skuUrl) {
        String str = HttpClient.doGet(skuUrl);
        Pattern p9 = Pattern.compile("(?<=title\\>).*(?=</title)");
        Matcher m9 = p9.matcher(str);
        Map<String, String> map = new ConcurrentHashMap<>();
        if (m9.find()) {
            map.put("name", m9.group());
        }
        map.put("price", "5999.00");
        logger.info("模拟京东商品爬虫解析:{} | {} 元 {}", map.get("name"), map.get("price"), skuUrl);
        return map;
    }

    public String createBase64(Map<String, String> goodsInfo) {
        java.util.Base64.Encoder encoder = Base64.getEncoder();
        try {
            logger.info("模拟生成京东商品base64海报");
            return encoder.encodeToString(JSON.toJSONString(goodsInfo).getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }
}

// 模拟爬取淘宝商品
public class TaoBaoNetMall extends NetMall {
    public TaoBaoNetMall(String uId, String uPwd) {
        super(uId, uPwd);
    }

    @Override
    public Boolean login(String uId, String uPwd) {
        logger.info("模拟淘宝用户登录 uId:{} uPwd:{}", uId, uPwd);
        return true;
    }

    @Override
    public Map<String, String> reptile(String skuUrl) {
        String str = HttpClient.doGet(skuUrl);
        Pattern p9 = Pattern.compile("(?<=title\\>).*(?=</title)");
        Matcher m9 = p9.matcher(str);
        Map<String, String> map = new ConcurrentHashMap<>();
        if (m9.find()) {
            map.put("name", m9.group());
        }
        map.put("price", "4799.00");
        logger.info("模拟淘宝商品爬虫解析:{} | {} 元 {}", map.get("name"), map.get("price"), skuUrl);
        return map;
    }

    @Override
    public String createBase64(Map<String, String> goodsInfo) {
        java.util.Base64.Encoder encoder = Base64.getEncoder();
        try {
            logger.info("模拟生成淘宝商品base64海报");
            return encoder.encodeToString(JSON.toJSONString(goodsInfo).getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }
}

// 模拟爬取当当商品
public class DangDangNetMall extends NetMall {
    public DangDangNetMall(String uId, String uPwd) {
        super(uId, uPwd);
    }

    @Override
    public Boolean login(String uId, String uPwd) {
        logger.info("模拟当当用户登录 uId:{} uPwd:{}", uId, uPwd);
        return true;
    }

    @Override
    public Map<String, String> reptile(String skuUrl) {
        String str = HttpClient.doGet(skuUrl);
        Pattern p9 = Pattern.compile("(?<=title\\>).*(?=</title)");
        Matcher m9 = p9.matcher(str);
        Map<String, String> map = new ConcurrentHashMap<>();
        if (m9.find()) {
            map.put("name", m9.group());
        }
        map.put("price", "4548.00");
        logger.info("模拟当当商品爬虫解析:{} | {} 元 {}", map.get("name"), map.get("price"), skuUrl);
        return map;
    }

    @Override
    public String createBase64(Map<String, String> goodsInfo) {
        java.util.Base64.Encoder encoder = Base64.getEncoder();
        try {
            logger.info("模拟生成当当商品base64海报");
            return encoder.encodeToString(JSON.toJSONString(goodsInfo).getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return null;
        }
    }
}

单元测试

// 单元测试
public class NetMallTest {
    /**
     * 测试链接
     * 京东:https://item.jd.com/100008348542.html
     * 淘宝:https://detail.tmall.com/item.html
     * 当当:http://product.dangdang.com/1509704171.html
     */
    @Test
    public void test_NetMall() {
        NetMall netMall = new JDNetMall("1000001", "******");
        String base64 = netMall.generateGoodsPoster("https://item.jd.com/100008348542.html");
        logger.info("测试结果:{}", base64);
    }
}

第25章 访问者模式

访问者模式要解决的核心问题是在一个稳定的数据结构下,如何增加易变的业务访问逻辑。如何通过解耦增强业务扩展性。

简单地说,访问者模式的核心在于同一个事物或办事窗口,不同人办不同的事,各自关心的角度和访问的信息是不同的,按需选择。
例如,排队买早餐的用户会点自己想要的早餐,如鸡蛋灌饼、煎饼和烤冷面等。

不同用户对学生身份访问视角场景
如图25-2所示,案例场景模拟校园中有学生和老师两种身份的用户,家长和校长关心的视角是不同的,家长更关心孩子的成绩和老师的能力,校长更关心老师所在班级学生的人数和升学率。这样一来,学生和老师就是一个固定信息。想让站在不同视角的用户获取关心的信息,适合用观察者模式实现,从而让实体与业务解耦,增强扩展性。但观察者模式的整体类结构相对复杂,需要梳理清楚。

1. 实体类

//1. 用户抽象类,基础用户信息
public abstract class User {
    public String name;      // 姓名
    public String identity;  // 身份:重点班、普通班 | 特级教师、普通教师、实习教师
    public String clazz;     // 班级

    public User(String name, String identity, String clazz) {
        this.name = name;
        this.identity = identity;
        this.clazz = clazz;
    }

    // 核心访问方法
    public abstract void accept(Visitor visitor);
}

//2. 实现用户信息,老师类
import java.math.BigDecimal;

public class Teacher extends User {
    public Teacher(String name, String identity, String clazz) {
        super(name, identity, clazz);
    }

    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    // 升学率
    public double entranceRatio() {
        return BigDecimal.valueOf(Math.random() * 100).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
    }
}
//3. 学生类
public class Student extends User {
    public Student(String name, String identity, String clazz) {
        super(name, identity, clazz);
    }

    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public int ranking() {
        return (int) (Math.random() * 100);
    }
}

2. 访问数据接口定义

public interface Visitor {
    // 访问学生信息
    void visit(Student student);
    // 访问老师信息
    void visit(Teacher teacher);
}

3. 访问数据接口实现

访问者:校长

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Principal implements Visitor {
    private Logger logger = LoggerFactory.getLogger(Principal.class);

    public void visit(Student student) {
        // 原代码中 student.count() 有误,推测可能是需要修改的地方,这里先注释掉
        // logger.info("学生信息 姓名:{} 班级:{} 人数:{}", student.clazz, student.count());
    }

    public void visit(Teacher teacher) {
        logger.info("老师信息 姓名:{} 班级:{} 升学率:{}", teacher.name, teacher.clazz, teacher.entranceRatio());
    }
}

访问者:家长
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Parent implements Visitor {
    private Logger logger = LoggerFactory.getLogger(Parent.class);

    public void visit(Student student) {
        logger.info("学生信息 姓名:{} 班级:{} 排名:{}", student.name, student.clazz, student.ranking());
    }

    public void visit(Teacher teacher) {
        logger.info("老师信息 姓名:{} 班级:{} 级别:{}", teacher.name, teacher.clazz, teacher.identity);
    }
}

4. 数据看板

import java.util.ArrayList;
import java.util.List;

public class DataView {
    List<User> userList = new ArrayList<User>();

    public DataView() {
        userList.add(new Student("谢东", "重点班", "一年一班"));
        userList.add(new Student("windy", "重点班", "一年一班"));
        userList.add(new Student("大毛", "普通班", "二年三班"));
        userList.add(new Student("Shing", "普通班", "三年四班"));
        userList.add(new Teacher("BK", "特级教师", "一年一班"));
        userList.add(new Teacher("娜娜Goddess", "特级教师", "一年一班"));
        userList.add(new Teacher("dangdang", "普通教师", "二年三班"));
        userList.add(new Teacher("泽东", "实习教师", "三年四班"));
    }

    // 展示
    public void show(Visitor visitor) {
        for (User user : userList) {
            user.accept(visitor);
        }
    }
}

5. 测试验证

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VisitorPatternTest {
    private Logger logger = LoggerFactory.getLogger(VisitorPatternTest.class);

    @Test
    public void test() {
        DataView dataView = new DataView();
        logger.info("\r\n家长视角访问:");
        dataView.show(new Parent());     // 家长
        logger.info("\r\n校长视角访问:");
        dataView.show(new Principal());  // 校长
    }
}

个人解读

其实就是利用父类继承的能力,实体与接口实现包了一下,这种用例没啥新意,不经典。