4
2
0
2

设计模式

一、单例模式

image

因为在编程开发中经常会遇到这样⼀种场景,那就是需要保证⼀个类只有⼀个实例哪怕多线程同时访问,并需要提供⼀个全局访问此实例的点。

综上以及我们平常的开发中,可以总结⼀条经验,单例模式主要解决的是,⼀个全局使⽤的类频繁的创建和消费,从⽽提升提升整体的代码的性能。

1.1 案例场景

1. 数据库的连接池不会反复创建
2. spring中⼀个单例模式bean的⽣成和使⽤
3. 在我们平常的代码中需要设置全局的的⼀些属性保存

1.2 七种单例模式实现

0. 静态类

public class Singleton_00 {
 	public static Map<String,String> cache = new ConcurrentHashMap<String,String>();
}
- 以上这种⽅式在我们平常的业务开发中⾮常场常⻅,这样静态类的⽅式可以在第⼀次运⾏的时候直接初始化Map类,同时这⾥我们也不需要到延迟加载在使⽤。
- 在不需要维持任何状态下,仅仅⽤于全局访问,这个使⽤使⽤静态类的⽅式更加⽅便。
- 但如果需要被继承以及需要维持⼀些特定状态的情况下,就适合使⽤单例模式。

1. 懒汉模式(线程不安全)

package com.codertl.design;

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;
    }
}
- 单例模式有⼀个特点就是不允许外部直接创建,也就是 new Singleton_01() ,因此这⾥在默认的构造函数上添加了私有属性 private 。
- ⽬前此种⽅式的单例确实满⾜了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成⼀堆⼈在抢厕所,就会造成多个同样的实例并存,从⽽没有达到单例的要求。

2. 懒汉模式(线程安全)

package com.codertl.design;

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;
    }
}
- 此种模式虽然是安全的,但由于把锁加到⽅法上后,所有的访问都因需要锁占⽤导致资源的浪费。如果不是特殊情况下,不建议此种⽅式实现单例模式。

3. 饿汉模式(线程安全)

package com.codertl.design;

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

    private Singleton_03() {
    }

    public static Singleton_03 getInstance() {
        return instance;
    }
}
- 此种⽅式与我们开头的第⼀个实例化 Map 基本⼀致,在程序启动的时候直接运⾏加载,后续有外部需要使⽤的时候获取即可。
- 但此种⽅式并不是懒加载,也就是说⽆论你程序中是否⽤到这样的类都会在程序启动之初进⾏创建。
- 那么这种⽅式导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,但是程序已经将这些地图全部实例化。到你⼿机上最明显体验就⼀开游戏内存满了,⼿机卡了,需要换了。

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

package com.codertl.design;

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;
    }
}
- 使⽤类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的⽅式耗费性能。
- 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是⼀个类的构造⽅法在多线程环境下可以被正确的加载。
- 此种⽅式也是⾮常推荐使⽤的⼀种单例模式

5. 双重锁校验(线程安全)

package com.codertl.design;

public class Singleton_05 {
    private static 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;
    }
}
- 双重锁的⽅式是⽅法级锁的优化,减少了部分获取实例的耗时。
- 同时这种⽅式也满⾜了懒加载。

6. CAS [AtomicReference] (线程安全)

package com.codertl.design;

import java.util.concurrent.atomic.AtomicReference;

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) {

        System.out.println(Singleton_06.getInstance());// org.itstack.demo.design.Singleton_06@2b193f2d
  
        System.out.println(Singleton_06.getInstance());// org.itstack.demo.design.Singleton_06@2b193f2d
    }
}
- java并发库提供了很多原⼦类来⽀持并发访问的数据安全性; AtomicInteger 、 AtomicBoolean 、 AtomicLong 、 AtomicReference 。
- AtomicReference 可以封装引⽤⼀个V实例,⽀持并发访问如上的单例⽅式就是使⽤了这样的⼀个特点。
- 使⽤CAS的好处就是不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以⽀持较⼤的并发性。
- 当然CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中。

7. 枚举单例(线程安全)

package com.codertl.design;

public enum Singleton_07 {
    INSTANCE;

    public void test() {
        System.out.println("hi~");
    }
}
- Effective Java 作者推荐使⽤枚举的⽅式解决单例模式,此种⽅式可能是平时最少⽤到的。
- 这种⽅式解决了最主要的;线程安全、⾃由串⾏化、单⼀实例。

调用方式:

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

这种写法在功能上与共有域⽅法相近,但是它更简洁,⽆偿地提供了串⾏化机制,绝对防⽌对此实例化,即使是在⾯对复杂的串⾏化或者反射攻击的时候。虽然这中⽅法还没有⼴泛采⽤,但是单元素的枚举类型已经成为实现Singleton的最佳⽅法。

但也要知道此种⽅式在存在继承场景下是不可⽤的。

二、观察者模式

image

简单来讲观察者模式,就是当⼀个⾏为发⽣时传递信息给另外⼀个⽤户接收做出相应的处理,两者之间没有直接的耦合关联。例如;狙击⼿、李云⻰。

image

​ 除了⽣活中的场景外,在我们编程开发中也会常⽤到⼀些观察者的模式或者组件,例如我们经常使⽤的MQ服务,虽然MQ服务是有⼀个通知中⼼并不是每⼀个类服务进⾏通知,但整体上也可以算作是观察者模式的思路设计。再⽐如可能有做过的⼀些类似事件监听总线,让主线服务与其他辅线业务服务分离,为了使系统降低耦合和增强扩展性,也会使⽤观察者模式进⾏处理。

2.1 案例场景模拟

image

在本案例中我们模拟每次⼩客⻋指标摇号事件通知场景(真实的不会由官⽹给你发消息)

可能⼤部分⼈看到这个案例⼀定会想到⾃⼰每次摇号都不中的场景,收到⼀个遗憾的短信通知。当然⽬

前的摇号系统并不会给你发短信,⽽是由百度或者⼀些其他插件发的短信。那么假如这个类似的摇号功

能如果由你来开发,并且需要对外部的⽤户做⼀些事件通知以及需要在主流程外再添加⼀些额外的辅助

流程时该如何处理呢?

基本很多⼈对于这样的通知事件类的实现往往⽐较粗犷,直接在类⾥⾯就添加了。1是考虑这可能不会怎么扩展,2是压根就没考虑过。但如果你有仔细思考过你的核⼼类功能会发现,这⾥⾯有⼀些核⼼主链路,还有⼀部分是辅助功能。⽐如完成了某个⾏为后需要触发MQ给外部,以及做⼀些消息PUSH给⽤户等,这些都不算做是核⼼流程链路,是可以通过事件通知的⽅式进⾏处理。

那么接下来我们就使⽤这样的设计模式来优化重构此场景下的代码。

1. 场景模拟工程

image

2.场景简述

2.1 摇号服务接口

package com.codertl;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class MinibusTargetService {

    /**
     * 模拟摇号,但不是摇号算法
     *
     * @param uId ⽤户编号
     * @return 结果
     */
    public String lottery(String uId) {
        return Math.abs(uId.hashCode()) % 2 == 0 ?
                "恭喜你,编 码".concat(uId).concat("在本次摇号中签") :
                "很遗憾,编 码".concat(uId).concat("在本次摇号未中签或摇号资格已过期");
    }
}

⾮常简单的⼀个模拟摇号接⼝,与真实公平的摇号是有差别的

2.2 用一坨坨代码实现

这⾥我们先使⽤最粗暴的⽅式来实现功能

按照需求需要在原有的摇号接⼝中添加MQ消息发送以及短消息通知功能,如果是最直接的⽅式那么可以直接在⽅法中补充功能即可。

1. ⼯程结构

image

这段代码接⼝中包括了三部分内容;返回对象( LotteryResult )、定义接⼝( LotteryService )、具体实现( LotteryServiceImpl )。

2. 代码实现

package com.codertl.serivce.Impl;

import com.codertl.MinibusTargetService;
import com.codertl.pojo.LotteryResult;
import com.codertl.serivce.LotteryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class LotteryServiceImpl implements LotteryService {

    private Logger logger =
            LoggerFactory.getLogger(LotteryServiceImpl.class);
    private MinibusTargetService minibusTargetService = new
            MinibusTargetService();

    @Override
    public LotteryResult doDraw(String uId) {
        // 摇号
        String lottery = minibusTargetService.lottery(uId);
        // 发短信
        logger.info("给⽤户 {} 发送短信通知(短信):{}", uId, lottery);
        // 发MQ消息
        logger.info("记录⽤户 {} 摇号结果(MQ):{}", uId, lottery);
        // 结果
        return new LotteryResult(uId, lottery, new Date());
    }
}
- 从以上的⽅法实现中可以看到,整体过程包括三部分;摇号、发短信、发MQ消息,⽽这部分都是顺序调⽤的。

- 除了 摇号 接⼝调⽤外,后⾯的两部分都是⾮核⼼主链路功能,⽽且会随着后续的业务需求发展⽽不断的调整和扩充,在这样的开发⽅式下就⾮常不利于维护。

3. 测试验证

package com.codertl;

import com.alibaba.fastjson.JSON;
import com.codertl.pojo.LotteryResult;
import com.codertl.serivce.Impl.LotteryServiceImpl;
import com.codertl.serivce.LotteryService;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ObserverDesignDemo201ApplicationTests {

    private static final Logger logger = LoggerFactory.getLogger(ObserverDesignDemo201ApplicationTests.class);

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

}

4. 结果

2023-02-03 17:34:48.215  INFO 15764 --- [           main] c.c.serivce.Impl.LotteryServiceImpl: 给⽤户 2765789109876 发送短信通知(短信):很遗憾,编 码2765789109876在本次摇号未中签或摇号资格已过期
2023-02-03 17:34:48.217  INFO 15764 --- [           main] c.c.serivce.Impl.LotteryServiceImpl: 记录⽤户 2765789109876 摇号结果(MQ):很遗憾,编 码2765789109876在本次摇号未中签或摇号资格已过期
2023-02-03 17:34:48.356  INFO 15764 --- [           main] .c.ObserverDesignDemo201ApplicationTests : 测试结果:{"UId":"2765789109876","date":1675416888217,"lottery":"很遗憾,编 码2765789109876在本次摇号未中签或摇号资格已过期"}

从测试结果上是符合预期的,也是平常开发代码的⽅式,还是⾮常简单的。

2.3 观察者模式重构代码

接下来使⽤观察者模式来进⾏代码优化,也算是⼀次很⼩的重构。

1. 工程结构

image

观察者模式模型结构:

image

- 从上图可以分为三⼤块看; 事件监听 、 事件处理 、 具体的业务流程 ,另外在业务流程中LotteryService 定义的是抽象类,因为这样可以通过抽象类将事件功能屏蔽,外部业务流程开发者不需要知道具体的通知操作。
- 右下⻆圆圈图表示的是核⼼流程与⾮核⼼流程的结构,⼀般在开发中会把主线流程开发完成后,再使⽤通知的⽅式处理辅助流程。他们可以是异步的,在MQ以及定时任务的处理下,保证最终⼀致性。

2. 代码实现

2.1 事件监听接⼝定义

package com.codertl.event.listener;

import com.codertl.pojo.LotteryResult;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public interface EventListener {

    void doEvent(LotteryResult result);
}

接⼝中定义了基本的事件类,这⾥如果⽅法的⼊参信息类型是变化的可以使⽤泛型

2.2 两个监听事件的实现

短消息事件:

package com.codertl.event.listener;

import com.codertl.pojo.LotteryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class MessageEventListener implements EventListener{

    private static final Logger logger = LoggerFactory.getLogger(MessageEventListener.class);

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

MQ发送事件:

package com.codertl.event.listener;

import com.codertl.pojo.LotteryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class MQEventListener implements EventListener {

    private static final Logger logger = LoggerFactory.getLogger(MQEventListener.class);

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

以上是两个事件的具体实现,相对来说都⽐较简单。如果是实际的业务开发那么会需要调⽤外部接⼝以及控制异常的处理。

同时我们上⾯提到事件接⼝添加泛型,如果有需要那么在事件的实现中就可以按照不同的类型进⾏包装事件内容。

2.3 事件处理类

package com.codertl.event;

import com.codertl.event.listener.EventListener;
import com.codertl.pojo.LotteryResult;

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

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class EventManager {

    Map<Enum<EventType>, List<EventListener>> listeners = new HashMap<>();

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

    public enum EventType {
        MQ, MESSAGE
    }

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

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

    public void notify(Enum<EventType> eventType, LotteryResult result) {
        List<EventListener> users = listeners.get(eventType);
        for (EventListener listener : users) {
            listener.doEvent(result);
        }
    }
}
- 整个处理的实现上提供了三个主要⽅法;订阅( subscribe )、取消订阅( unsubscribe )、通知( notify )。这三个⽅法分别⽤于对监听时间的添加和使⽤。
- 另外因为事件有不同的类型,这⾥使⽤了枚举的⽅式进⾏处理,也⽅便让外部在规定下使⽤事件,⽽不⾄于乱传信息( EventType.MQ 、 EventType.Message )。

2.4 业务抽象类接⼝

package com.codertl.service;

import com.codertl.event.EventManager;
import com.codertl.event.listener.MQEventListener;
import com.codertl.event.listener.MessageEventListener;
import com.codertl.pojo.LotteryResult;

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);
}
- 这种使⽤抽象类的⽅式定义实现⽅法,可以在⽅法中扩展需要的额外调⽤。并提供抽象类 abstract LotteryResult doDraw(String uId) ,让类的继承者实现。
- 同时⽅法的定义使⽤的是 protected ,也就是保证将来外部的调⽤⽅不会调⽤到此⽅法,只有调⽤到 draw(String uId) ,才能让我们完成事件通知。
- 此种⽅式的实现就是在抽象类中写好⼀个基本的⽅法,在⽅法中完成新增逻辑的同时,再增加抽象类的使⽤。⽽这个抽象类的定义会有继承者实现。
- 另外在构造函数中提供了对事件的定义; eventManager.subscribe(EventManager.EventType.MQ, newMQEventListener()) 。
- 在使⽤的时候也是使⽤枚举的⽅式进⾏通知使⽤,传了什么类型EventManager.EventType.MQ ,就会执⾏什么事件通知,按需添加。

2.5 业务接⼝实现类

package com.codertl.service.impl;

import com.codertl.MinibusTargetService;
import com.codertl.pojo.LotteryResult;
import com.codertl.service.LotteryService;

import java.util.Date;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
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());
    }
}

现在再看业务流程的实现中可以看到已经⾮常简单了,没有额外的辅助流程,只有核⼼流程的处理。

3. 测试验证

package com.codertl;

import com.alibaba.fastjson.JSON;
import com.codertl.pojo.LotteryResult;
import com.codertl.service.impl.LotteryServiceImpl;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ObserverDesignDemo202ApplicationTests {


    private static final Logger logger = LoggerFactory.getLogger(ObserverDesignDemo202ApplicationTests.class);

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

}

从调⽤上来看⼏乎没有区别,但是这样的实现⽅式就可以⾮常⽅便的维护代码以及扩展新的需求

4. 测试结果

2023-02-03 21:34:42.198  INFO 6952 --- [           main] c.c.event.listener.MQEventListener       : 记录用户 2765789109876 摇号结果(MQ): 很遗憾,编 码2765789109876在本次摇号未中签或摇号资格已过期
2023-02-03 21:34:42.199  INFO 6952 --- [           main] c.c.event.listener.MessageEventListener  : 给用户 2765789109876 发送短信通知(短信): 很遗憾,编 码2765789109876在本次摇号未中签或摇号资格已过期
2023-02-03 21:34:42.347  INFO 6952 --- [           main] .c.ObserverDesignDemo202ApplicationTests : 测试结果: {"date":1675431282198,"msg":"很遗憾,编 码2765789109876在本次摇号未中签或摇号资格已过期","uid":"2765789109876"}

从测试结果上看满⾜我们的预期,虽然结果是⼀样的,但只有我们知道了设计模式的魅⼒所在。

三、建造者模式

image

建造者模式所完成的内容就是通过将多个简单对象通过⼀步步的组装构建出⼀个复杂对象的过程
那么,哪⾥有这样的场景呢?
例如你玩王者荣耀的时的初始化界⾯;有三条路、有树⽊、有野怪、有守卫塔等等,甚⾄依赖于你的⽹络情况会控制清晰度。⽽当你换⼀个场景进⾏其他不同模式的选择时,同样会建设道路、树⽊、野怪等等,但是他们的摆放和⼤⼩都有不同。这⾥就可以⽤到建造者模式来初始化游戏元素。

⽽这样的根据相同的 物料 ,不同的组装所产⽣出的具体的内容,就是建造者模式的最终意图,也就是;

将⼀个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。

3.1 案例场景模拟

这⾥我们模拟装修公司对于设计出⼀些套餐装修服务的场景。

很多装修公司都会给出⾃家的套餐服务,⼀般有;欧式豪华、轻奢⽥园、现代简约等等,⽽这些套餐的后⾯是不同的商品的组合。例如;⼀级&⼆级吊顶、多乐⼠涂料、圣象地板、⻢可波罗地砖等等,按照不同的套餐的价格选取不同的品牌组合,最终再按照装修⾯积给出⼀个整体的报价。

这⾥我们就模拟装修公司想推出⼀些套餐装修服务,按照不同的价格设定品牌选择组合,以达到使⽤建造者模式的过程

1. 工程结构

image

在模拟⼯程中提供了装修中所需要的物料; ceilling(吊顶) 、 coat(涂料) 、 floor(地板) 、tile(地砖) ,这么四项内容。(实际的装修物料要⽐这个多的多)

2. 场景简述

2.1 物料接口

package com.codertl.design;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public interface Matter {
    String scene(); // 场景;地板、地砖、涂料、吊顶

    String brand(); // 品牌

    String model(); // 型号

    BigDecimal price(); // 价格

    String desc(); // 描述
}

物料接⼝提供了基本的信息,以保证所有的装修材料都可以按照统⼀标准进⾏获取

2.2 吊顶(ceiling)

一级顶:

package com.codertl.design.ceilling;

import com.codertl.design.Matter;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class LevelOneCeiling implements Matter {
    @Override
    public String scene() {
        return "吊顶";
    }

    @Override
    public String brand() {
        return "装修公司自带";
    }

    @Override
    public String model() {
        return "一级顶";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal(260);
    }

    @Override
    public String desc() {
        return "造型只做低⼀级,只有⼀个层次的吊顶,⼀般离顶120-150mm";
    }
}

二级顶:

package com.codertl.design.ceilling;

import com.codertl.design.Matter;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class LevelTwoCeiling implements Matter {
    @Override
    public String scene() {
        return "吊顶";
    }

    @Override
    public String brand() {
        return "装修公司自带";
    }

    @Override
    public String model() {
        return "二级顶";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal(850);
    }

    @Override
    public String desc() {
        return "两个层次的吊顶,⼆级吊顶⾼度⼀般就往下吊20cm,要是层⾼很⾼,也可增加每级的厚度";
    }

}

2.3 涂料(coat)

多乐士:

package com.codertl.design.coat;

import com.codertl.design.Matter;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class DuluxCoat implements Matter {
    @Override
    public String scene() {
        return "涂料";
    }

    @Override
    public String brand() {
        return "多乐⼠(Dulux)";
    }

    @Override
    public String model() {
        return "第二代";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal(719);
    }

    @Override
    public String desc() {
        return "多乐⼠是阿克苏诺⻉尔旗下的著名建筑装饰油漆品牌,产品畅销于全球100个国家,每年全球有5000万户家庭使⽤多乐⼠油漆。";
    }
}

立邦:

package com.codertl.design.coat;

import com.codertl.design.Matter;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class LiBangCoat implements Matter {
    @Override
    public String scene() {
        return "涂料";
    }

    @Override
    public String brand() {
        return "⽴邦";
    }

    @Override
    public String model() {
        return "默认级别";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal(650);
    }

    @Override
    public String desc() {
        return "⽴邦始终以开发绿⾊产品、注᯿⾼科技、⾼品质为⽬标,以技术⼒量不断推进科研和开发,满⾜消费者需求。";
    }
}

2.4 地板(floor)

德尔:

package com.codertl.design.floor;

import com.codertl.design.Matter;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class DerFloor implements Matter {
    @Override
    public String scene() {
        return "地板";
    }

    @Override
    public String brand() {
        return "德尔(Der)";
    }

    @Override
    public String model() {
        return "A+";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal(119);
    }

    @Override
    public String desc() {
        return "DER德尔集团是全球领先的专业⽊地板制造商,北京2008年奥运会家装和公装地板供应商";
    }
}

圣象:

package com.codertl.design.floor;

import com.codertl.design.Matter;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class ShengXiangFloor implements Matter {

    @Override
    public String scene() {
        return "地板";
    }

    @Override
    public String brand() {
        return "圣象";
    }

    @Override
    public String model() {
        return "一级";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal(318);
    }

    @Override
    public String desc() {
        return "圣象地板是中国地板⾏业著名品牌。圣象地板拥有中国驰名商标、中国名牌、国家免检、中国环境标志认证等多项荣誉。";
    }
}

2.5 地砖(tile)

东鹏:

package com.codertl.design.tile;

import com.codertl.design.Matter;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class DongPengTile implements Matter {
    @Override
    public String scene() {
        return "地砖";
    }

    @Override
    public String brand() {
        return "东鹏瓷砖";
    }

    @Override
    public String model() {
        return "10001";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal(102);
    }

    @Override
    public String desc() {
        return "东鹏瓷砖以品质铸就品牌,科技推动品牌,⼝碑传播品牌为宗旨,2014年品牌价值132.35亿元,位列建陶⾏业榜⾸。";
    }
}

⻢可波罗

package com.codertl.design.tile;

import com.codertl.design.Matter;

import java.math.BigDecimal;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class MarcoPoloTile implements Matter {
    @Override
    public String scene() {
        return "地砖";
    }

    @Override
    public String brand() {
        return "⻢可波罗(MARCO POLO)";
    }

    @Override
    public String model() {
        return "缺省";
    }

    @Override
    public BigDecimal price() {
        return new BigDecimal(140);
    }

    @Override
    public String desc() {
        return "“⻢可波罗”品牌诞⽣于1996年,作为国内最早品牌化的建陶品牌,以“⽂化陶瓷”占领市场,享有“仿古砖⾄尊”的美誉。";
    }
}

以上就是本次装修公司所提供的 装修配置单 ,接下我们会通过案例去使⽤不同的物料组合出不同的套餐服务。

3.2 用一坨坨代码实现

讲道理没有ifelse解决不了的逻辑,不⾏就在加⼀⾏!

我们使⽤这样很直⽩的⽅式去把功能实现出来,在通过设计模式去优化完善。这样的代码结构也都是⾮常简单的,没有复杂的类关系结构,都是直来直去的代码。除了我们经常强调的这样的代码不能很好的扩展外,做⼀些例⼦demo⼯程还是可以的。

1. 工程结构

image

⼀个类⼏千⾏的代码你是否⻅过,嚯?那今天就让你⻅识⼀下有这样潜质的类!

2. if else 实现需求

package com.codertl.controller;

import com.codertl.design.Matter;
import com.codertl.design.ceilling.LevelOneCeiling;
import com.codertl.design.ceilling.LevelTwoCeiling;
import com.codertl.design.coat.DuluxCoat;
import com.codertl.design.coat.LiBangCoat;
import com.codertl.design.floor.ShengXiangFloor;
import com.codertl.design.tile.DongPengTile;
import com.codertl.design.tile.MarcoPoloTile;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class DecorationPackageController {

    public String getMatterList(BigDecimal area, Integer level) {
        List<Matter> list = new ArrayList<Matter>(); // 装修清单
        BigDecimal price = BigDecimal.ZERO; // 装修价格
        // 豪华欧式
        if (1 == level) {
            LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling(); // 吊顶,⼆级顶
            DuluxCoat duluxCoat = new DuluxCoat(); // 涂料,多乐⼠
            ShengXiangFloor shengXiangFloor = new ShengXiangFloor(); // 地板,圣象
            list.add(levelTwoCeiling);
            list.add(duluxCoat);
            list.add(shengXiangFloor);
            price = price.add(area.multiply(new
                    BigDecimal("0.2")).multiply(levelTwoCeiling.price()));
            price = price.add(area.multiply(new
                    BigDecimal("1.4")).multiply(duluxCoat.price()));
            price = price.add(area.multiply(shengXiangFloor.price()));
        }
        // 轻奢⽥园
        if (2 == level) {
            LevelTwoCeiling levelTwoCeiling = new LevelTwoCeiling(); // 吊顶,⼆级顶
            LiBangCoat liBangCoat = new LiBangCoat(); // 涂料,⽴邦
            MarcoPoloTile marcoPoloTile = new MarcoPoloTile(); // 地砖,⻢可波罗
            list.add(levelTwoCeiling);
            list.add(liBangCoat);
            list.add(marcoPoloTile);
            price = price.add(area.multiply(new
                    BigDecimal("0.2")).multiply(levelTwoCeiling.price()));
            price = price.add(area.multiply(new
                    BigDecimal("1.4")).multiply(liBangCoat.price()));
            price = price.add(area.multiply(marcoPoloTile.price()));
        }
        // 现代简约
        if (3 == level) {
            LevelOneCeiling levelOneCeiling = new LevelOneCeiling(); //吊顶,⼆级顶
            LiBangCoat liBangCoat = new LiBangCoat(); //涂料,⽴邦
            DongPengTile dongPengTile = new DongPengTile(); //地砖,东鹏
            list.add(levelOneCeiling);
            list.add(liBangCoat);
            list.add(dongPengTile);
            price = price.add(area.multiply(new
                    BigDecimal("0.2")).multiply(levelOneCeiling.price()));
            price = price.add(area.multiply(new
                    BigDecimal("1.4")).multiply(liBangCoat.price()));
            price = price.add(area.multiply(dongPengTile.price()));
        }
        StringBuilder detail = new StringBuilder("\r\n-------------------- -----------------------------------\r\n" +
                "装修清单" + "\r\n" +
                "套餐等级:" + level + "\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();
    }
}

⾸先这段代码所要解决的问题就是接收⼊参;装修⾯积(area)、装修等级(level),根据不同类型的装修等级选择不同的材料。

其次在实现过程中可以看到每⼀段 if 块⾥,都包含着不通的材料(吊顶,⼆级顶、涂料,⽴邦、地砖,⻢可波罗),最终⽣成装修清单和装修成本。

最后提供获取装修详细信息的⽅法,返回给调⽤⽅,⽤于知道装修清单。

3. 测试验证

package com.codertl;

import com.codertl.controller.DecorationPackageController;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.math.BigDecimal;

@SpringBootTest
class BuildDesignDemo101ApplicationTests {

    @Test
    void contextLoads() {
        DecorationPackageController decoration = new
                DecorationPackageController();
        // 豪华欧式
        System.out.println(decoration.getMatterList(new
                BigDecimal("132.52"),1));
        // 轻奢⽥园
        System.out.println(decoration.getMatterList(new
                BigDecimal("98.25"),2));
        // 现代简约
        System.out.println(decoration.getMatterList(new
                BigDecimal("85.43"),3));
    }

}

结果:

-------------------- -----------------------------------
装修清单
套餐等级:1
套餐价格:198064.39 元
房屋⾯积:132.52 平⽶
材料清单:
吊顶:装修公司自带、二级顶、平⽶价格:850 元。
涂料:多乐⼠(Dulux)、第二代、平⽶价格:719 元。
地板:圣象、一级、平⽶价格:318 元。


-------------------- -----------------------------------
装修清单
套餐等级:2
套餐价格:119865.00 元
房屋⾯积:98.25 平⽶
材料清单:
吊顶:装修公司自带、二级顶、平⽶价格:850 元。
涂料:⽴邦、默认级别、平⽶价格:650 元。
地砖:⻢可波罗(MARCO POLO)、缺省、平⽶价格:140 元。


-------------------- -----------------------------------
装修清单
套餐等级:3
套餐价格:90897.52 元
房屋⾯积:85.43 平⽶
材料清单:
吊顶:装修公司自带、一级顶、平⽶价格:260 元。
涂料:⽴邦、默认级别、平⽶价格:650 元。
地砖:东鹏瓷砖、10001、平⽶价格:102 元。

看到输出的这个结果,已经很有装修公司提供报价单的感觉了。以上这段使⽤ ifelse ⽅式实现的代码,⽬前已经满⾜的我们的也许功能。但随着⽼板对业务的快速发展要求,会提供很多的套餐针对不同的户型。那么这段实现代码将迅速扩增到⼏千⾏,甚⾄在修修改改中,已经像膏药⼀样难以维护。

3.3 建造者模式进行重构

接下来使⽤建造者模式来进⾏代码优化,也算是⼀次很⼩的重构。

建造者模式主要解决的问题是在软件系统中,有时候⾯临着"⼀个复杂对象"的创建⼯作,其通常由各个部分的⼦对象⽤⼀定的过程构成;由于需求的变化,这个复杂对象的各个部分经常⾯临着重⼤的变化,但是将它们组合在⼀起的过程却相对稳定。

这⾥我们会把构建的过程交给 创建者 类,⽽创建者通过使⽤我们的 构建⼯具包 ,去构建出不同的 装修套餐 。

1. ⼯程结构

image

建造者模型结构

image

⼯程中有三个核⼼类和⼀个测试类,核⼼类是建造者模式的具体实现。与 ifelse 实现⽅式相⽐,多出

来了两个⼆外的类。具体功能如下;

  • Builder ,建造者类具体的各种组装由此类实现。

  • DecorationPackageMenu ,是 IMenu 接⼝的实现类,主要是承载建造过程中的填充器。相当于这是⼀套承载物料和创建者中间衔接的内容。

2. 代码实现

2.1 定义装修包接口

package com.codertl.service;

import com.codertl.design.Matter;

public interface IMenu {

    IMenu appendCeiling(Matter matter); // 吊顶

    IMenu appendCoat(Matter matter); // 涂料

    IMenu appendFloor(Matter matter); // 地板

    IMenu appendTile(Matter matter); // 地砖

    String getDetail(); // 明细
}
  • 接⼝类中定义了填充各项物料的⽅法; 吊顶 、 涂料 、 地板 、 地砖 ,以及最终提供获取全部明细的⽅法。

2.2 装修包的实现

package com.codertl.service.impl;

import com.codertl.design.Matter;
import com.codertl.service.IMenu;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class DecorationPackageMenu implements IMenu {

    // 装修清单
    private List<Matter> list = new ArrayList<Matter>();

    // 装修价格
    private BigDecimal price = BigDecimal.ZERO;

    // ⾯积
    private BigDecimal area;

    // 装修等级;豪华欧式、轻奢⽥园、现代简约
    private String grade;

    public DecorationPackageMenu() {
    }

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

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

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

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

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

    @Override
    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();
    }
}
  • 装修包的实现中每⼀个⽅法都会了 this ,也就可以⾮常⽅便的⽤于连续填充各项物料。

  • 同时在填充时也会根据物料计算平⽶数下的报价,吊顶和涂料按照平⽶数适量乘以常熟计算。

  • 最后同样提供了统⼀的获取装修清单的明细⽅法。

2.3 建造者⽅法

package com.codertl.build;

import com.codertl.design.ceilling.LevelOneCeiling;
import com.codertl.design.ceilling.LevelTwoCeiling;
import com.codertl.design.coat.DuluxCoat;
import com.codertl.design.coat.LiBangCoat;
import com.codertl.design.floor.ShengXiangFloor;
import com.codertl.design.tile.DongPengTile;
import com.codertl.design.tile.MarcoPoloTile;
import com.codertl.service.IMenu;
import com.codertl.service.impl.DecorationPackageMenu;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
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());
    }
}

建造者的使⽤中就已经⾮常容易了,统⼀的建造⽅式,通过不同物料填充出不同的装修⻛格; 豪华欧式 、 轻奢⽥园 、 现代简约 ,如果将来业务扩展也可以将这部分内容配置到数据库⾃动⽣成。但整体的思想还可以使⽤创建者模式进⾏搭建。

3. 测试验证

package com.codertl;

import com.codertl.build.Builder;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class BuildDesignDemo102ApplicationTests {

    @Test
    void contextLoads() {
        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());
    }

}

结果:

-------------------- -----------------------------------
装修清单
套餐等级:豪华欧式
套餐价格:198064.39 元
房屋⾯积:132.52 平⽶
材料清单:
吊顶:装修公司自带、二级顶、平⽶价格:850 元。
涂料:多乐⼠(Dulux)、第二代、平⽶价格:719 元。
地板:圣象、一级、平⽶价格:318 元。


-------------------- -----------------------------------
装修清单
套餐等级:轻奢⽥园
套餐价格:119865.00 元
房屋⾯积:98.25 平⽶
材料清单:
吊顶:装修公司自带、二级顶、平⽶价格:850 元。
涂料:⽴邦、默认级别、平⽶价格:650 元。
地砖:⻢可波罗(MARCO POLO)、缺省、平⽶价格:140 元。


-------------------- -----------------------------------
装修清单
套餐等级:现代简约
套餐价格:90897.52 元
房屋⾯积:85.43 平⽶
材料清单:
吊顶:装修公司自带、一级顶、平⽶价格:260 元。
涂料:⽴邦、默认级别、平⽶价格:650 元。
地砖:东鹏瓷砖、10001、平⽶价格:102 元。

测试结果是⼀样的,调⽤⽅式也基本类似。但是⽬前的代码结构却可以让你很⽅便的很有调理的进⾏扩展业务开发。⽽不是以往⼀样把所有代码都写到 ifelse ⾥⾯。

当: ⼀些基本物料不会变,⽽其组合经常变化的时候 ,就可以选择这样的设计模式来构建代码。

四、工厂模式

image

⼯⼚模式⼜称⼯⼚⽅法模式,是⼀种创建型设计模式,其在⽗类中提供⼀个创建对象的⽅法, 允许⼦类决定实例化对象的类型。

这种设计模式也是 Java 开发中最常⻅的⼀种模式,它的主要意图是定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。

简单说就是为了提供代码结构的扩展性,屏蔽每⼀个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调⽤即可,同时,这也是去掉众多 ifelse 的⽅式。当然这可能也有⼀些缺点,⽐如需要实现的类⾮常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使⽤中,逐步降低。

4.1 模拟发奖多种商品案例

image

- 为了可以让整个学习的案例更加贴近实际开发,这⾥模拟互联⽹中在营销场景下的业务。由于营销场景的复杂、多变、临时的特性,它所需要的设计需要更加深⼊,否则会经常⾯临各种紧急CRUD操作,从⽽让代码结构混乱不堪,难以维护。
- 在营销场景中经常会有某个⽤户做了⼀些操作;打卡、分享、留⾔、邀请注册等等,进⾏返利积分,最后通过积分在兑换商品,从⽽促活和拉新。
- 那么在这⾥我们模拟积分兑换中的发放多种类型商品,假如现在我们有如下三种类型的商品接⼝;
序号 类型 接口
1 优惠券 CouponResult sendCoupon(String uId, StringcouponNumber, String uuid)
2 实物商品 Boolean deliverGoods(DeliverReq req)
3 第三⽅爱奇艺兑换卡 void grantToken(String bindMobileNumber, String cardId)

从以上接⼝来看有如下信息:

  • 三个接⼝返回类型不同,有对象类型、布尔类型、还有⼀个空类型。
  • ⼊参不同,发放优惠券需要仿᯿、兑换卡需要卡ID、实物商品需要发货位置(对象中含有)。
  • 另外可能会随着后续的业务的发展,会新增其他种商品类型。因为你所有的开发需求都是随着业务对市场的拓展⽽带来的。

4.2 用非工厂方式实现(用一坨坨代码实现)

如果不考虑任何扩展性,只为了尽快满⾜需求,那么对这么⼏种奖励发放只需使⽤ifelse语句判断,调⽤不同的接⼝即可满⾜需求。可能这也是⼀些刚⼊⻔编程的⼩伙伴,常⽤的⽅式。接下来我们就先按照这样的⽅式来实现业务的需求。

1. 工程结构

image

  • ⼯程结构上⾮常简单,⼀个⼊参对象 AwardReq 、⼀个出参对象 AwardRes ,以及⼀个接⼝类PrizeController

2. ifelse实现需求

package com.codertl.controller;

import com.alibaba.fastjson.JSON;
import com.codertl.pojo.AwardReq;
import com.codertl.pojo.AwardRes;
import com.codertl.pojo.CouponResult;
import com.codertl.pojo.DeliverReq;
import com.codertl.service.CouponService;
import com.codertl.service.GoodsService;
import com.codertl.service.IQiYiCardService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class PrizeController {

    private Logger logger =
            LoggerFactory.getLogger(PrizeController.class);
    public AwardRes awardToUser(AwardReq req) {
        String reqJson = JSON.toJSONString(req);
        AwardRes awardRes = null;
        try {
            logger.info("奖品发放开始{}。req:{}", req.getUid(), reqJson);
            // 按照不同类型⽅法商品[1优惠券、2实物商品、3第三⽅兑换卡(爱奇艺)]
            if (req.getAwardType() == 1) {
                CouponService couponService = new CouponService();
                CouponResult couponResult =
                        couponService.sendCoupon(req.getUid(), req.getAwardNumber(),
                                req.getBizId());
                if ("0000".equals(couponResult.getCode())) {
                    awardRes = new AwardRes("0000", "发放成功");
                } else {
                    awardRes = new AwardRes("0001",
                            couponResult.getInfo());
                }
            } else if (req.getAwardType() == 2) {
                GoodsService goodsService = new GoodsService();
                DeliverReq deliverReq = new DeliverReq();
                deliverReq.setUserName(queryUserName(req.getUid()));

                deliverReq.setUserPhone(queryUserPhoneNumber(req.getUid()));deliverReq.setSku(req.getAwardNumber());
                deliverReq.setOrderId(req.getBizId());

                deliverReq.setConsigneeUserName(req.getExtMap().get("consigneeUserName"))
                ;

                deliverReq.setConsigneeUserPhone(req.getExtMap().get("consigneeUserPhone"
                ));

                deliverReq.setConsigneeUserAddress(req.getExtMap().get("consigneeUserAddress"));
                Boolean isSuccess = goodsService.deliverGoods(deliverReq);
                if (isSuccess) {
                    awardRes = new AwardRes("0000", "发放成功");
                } else {
                    awardRes = new AwardRes("0001", "发放失败");
                }
            } else if (req.getAwardType() == 3) {
                String bindMobileNumber =
                        queryUserPhoneNumber(req.getUid());
                IQiYiCardService iQiYiCardService = new
                        IQiYiCardService();
                iQiYiCardService.grantToken(bindMobileNumber,
                        req.getAwardNumber());
                awardRes = new AwardRes("0000", "发放成功");
            }
            logger.info("奖品发放完成{}。", req.getUid());
        } catch (Exception e) {
            logger.error("奖品发放失败{}。req:{}", req.getUid(), reqJson,
                    e);
            awardRes = new AwardRes("0001", e.getMessage());
        }
        return awardRes;
    }
    private String queryUserName(String uId) {
        return "花花";
    }
    private String queryUserPhoneNumber(String uId) {
        return "15200101232";
    }
}
- 如上就是使⽤ ifelse ⾮常直接的实现出来业务需求的⼀坨代码,如果仅从业务⻆度看,研发如期甚⾄提前实现了功能。
- 那这样的代码⽬前来看并不会有什么问题,但如果在经过⼏次的迭代和拓展,接⼿这段代码的研发将⼗分痛苦。᯿构成本⾼需要理清之前每⼀个接⼝的使⽤,测试回归验证时间⻓,需要全部验证⼀次。这也就是很多⼈并不愿意接⼿别⼈的代码,如果接⼿了⼜被压榨开发时间。那么可想⽽知这样的 ifelse 还会继续增加。

3. 测试验证

写⼀个单元测试来验证上⾯编写的接⼝⽅式,养成单元测试的好习惯会为你增强代码质量。

package com.codertl;

import com.alibaba.fastjson.JSON;
import com.codertl.controller.PrizeController;
import com.codertl.pojo.AwardReq;
import com.codertl.pojo.AwardRes;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.HashMap;
import java.util.Map;

@SpringBootTest
class FactoryDesignDemo101ApplicationTests {

    private Logger logger =
            LoggerFactory.getLogger(FactoryDesignDemo101ApplicationTests.class);

    @Test
    void contextLoads() {
        PrizeController prizeController = new PrizeController();
        System.out.println("\r\n模拟发放优惠券测试\r\n");
        // 模拟发放优惠券测试
        AwardReq req01 = new AwardReq();
        req01.setUid("10001");
        req01.setAwardType(1);
        req01.setAwardNumber("EGM1023938910232121323432");
        req01.setBizId("791098764902132");
        AwardRes awardRes01 = prizeController.awardToUser(req01);
        logger.info("请求参数:{}", JSON.toJSON(req01));
        logger.info("测试结果:{}", JSON.toJSON(awardRes01));
        System.out.println("\r\n模拟⽅法实物商品\r\n");
        // 模拟⽅法实物商品
        AwardReq req02 = new AwardReq();
        req02.setUid("10001");
        req02.setAwardType(2);
        req02.setAwardNumber("9820198721311");
        req02.setBizId("1023000020112221113");
        Map<String,String> extMap = new HashMap<String,String>();
        extMap.put("consigneeUserName", "谢⻜机");
        extMap.put("consigneeUserPhone", "15200292123");
        extMap.put("consigneeUserAddress", "吉林省.⻓春市.双阳区.XX街道.檀溪苑⼩区.#18-2109");
        req02.setExtMap(extMap);
        AwardRes awardRes02 = prizeController.awardToUser(req02);
        logger.info("请求参数:{}", JSON.toJSON(req02));
        logger.info("测试结果:{}", JSON.toJSON(awardRes02));
        System.out.println("\r\n第三⽅兑换卡(爱奇艺)\r\n");
        AwardReq req03 = new AwardReq();
        req03.setUid("10001");
        req03.setAwardType(3);
        req03.setAwardNumber("AQY1xjkUodl8LO975GdfrYUio");
        AwardRes awardRes03 = prizeController.awardToUser(req03);
        logger.info("请求参数:{}", JSON.toJSON(req03));
        logger.info("测试结果:{}", JSON.toJSON(awardRes03));
    }

}

4. 结果

模拟发放优惠券测试

2023-02-03 15:49:40.582  INFO 15876 --- [           main] com.codertl.controller.PrizeController   : 奖品发放开始10001。req:{"awardNumber":"EGM1023938910232121323432","awardType":1,"bizId":"791098764902132","uid":"10001"}
2023-02-03 15:49:40.585  INFO 15876 --- [           main] com.codertl.controller.PrizeController   : 奖品发放完成10001。
2023-02-03 15:49:40.596  INFO 15876 --- [           main] c.c.FactoryDesignDemo101ApplicationTests : 请求参数:{"uid":"10001","bizId":"791098764902132","awardNumber":"EGM1023938910232121323432","awardType":1}
2023-02-03 15:49:40.600  INFO 15876 --- [           main] c.c.FactoryDesignDemo101ApplicationTests : 测试结果:{"code":"0000","info":"发放成功"}

模拟⽅法实物商品

2023-02-03 15:49:40.601  INFO 15876 --- [           main] com.codertl.controller.PrizeController   : 奖品发放开始10001。req:{"awardNumber":"9820198721311","awardType":2,"bizId":"1023000020112221113","extMap":{"consigneeUserName":"谢⻜机","consigneeUserPhone":"15200292123","consigneeUserAddress":"吉林省.⻓春市.双阳区.XX街道.檀溪苑⼩区.#18-2109"},"uid":"10001"}
2023-02-03 15:49:40.602  INFO 15876 --- [           main] com.codertl.controller.PrizeController   : 奖品发放完成10001。
2023-02-03 15:49:40.602  INFO 15876 --- [           main] c.c.FactoryDesignDemo101ApplicationTests : 请求参数:{"extMap":{"consigneeUserName":"谢⻜机","consigneeUserPhone":"15200292123","consigneeUserAddress":"吉林省.⻓春市.双阳区.XX街道.檀溪苑⼩区.#18-2109"},"uid":"10001","bizId":"1023000020112221113","awardNumber":"9820198721311","awardType":2}
2023-02-03 15:49:40.602  INFO 15876 --- [           main] c.c.FactoryDesignDemo101ApplicationTests : 测试结果:{"code":"0000","info":"发放成功"}

第三⽅兑换卡(爱奇艺)

2023-02-03 15:49:40.603  INFO 15876 --- [           main] com.codertl.controller.PrizeController   : 奖品发放开始10001。req:{"awardNumber":"AQY1xjkUodl8LO975GdfrYUio","awardType":3,"uid":"10001"}
2023-02-03 15:49:40.603  INFO 15876 --- [           main] com.codertl.controller.PrizeController   : 奖品发放完成10001。
2023-02-03 15:49:40.603  INFO 15876 --- [           main] c.c.FactoryDesignDemo101ApplicationTests : 请求参数:{"uid":"10001","awardNumber":"AQY1xjkUodl8LO975GdfrYUio","awardType":3}
2023-02-03 15:49:40.603  INFO 15876 --- [           main] c.c.FactoryDesignDemo101ApplicationTests : 测试结果:{"code":"0000","info":"发放成功"}

运⾏结果正常,满⾜当前所有业务产品需求,写的还很快。但!实在难以为维护!

4.3 工厂模式优化代码

接下来使⽤⼯⼚⽅法模式来进⾏代码优化,也算是⼀次很⼩的重构。整理重构后你会发现代码结构清晰了、也具备了下次新增业务需求的扩展性。但在实际使⽤中还会对此进⾏完善,⽬前的只是抽离出最核⼼的部分体现到你⾯前,⽅便学习。

1. 工程结构

image

⾸先,从上⾯的⼯程结构中你是否⼀些感觉,⽐如;它看上去清晰了、这样分层可以更好扩展了、似乎可以想象到每⼀个类做了什么。

如果还不能理解为什么这样修改,也没有关系。因为你是在通过这样的⽂章,来学习设计模式的魅⼒。并且再获取源码后,进⾏实际操作⼏次也就慢慢掌握了 ⼯⼚模式 的技巧。

2. 代码实现

2.1 定义发奖接口

package com.codertl.service;

import java.util.Map;

public interface ICommodity {
    void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception;
}
- 所有的奖品⽆论是实物、虚拟还是第三⽅,都需要通过我们的程序实现此接⼝进⾏处理,以保证最终⼊参出参的统⼀性。
- 接口的入参包括:⽤户ID 、 奖品ID 、 业务ID 以及 扩展字段 ⽤于处理发放实物商品时的收获地址。

2.2 实现奖品发放接⼝

优惠券:

package com.codertl.service.impl;

import com.alibaba.fastjson.JSON;
import com.codertl.pojo.CouponResult;
import com.codertl.service.ICommodity;
import com.codertl.service.old.CouponService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class CouponCommodityService implements ICommodity {

    private Logger logger =
            LoggerFactory.getLogger(CouponCommodityService.class);
    private CouponService couponService = new CouponService();

    @Override
    public void sendCommodity(String uId, String commodityId, String bizId, 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());
        }
    }
}

实物商品:

package com.codertl.service.impl;

import com.alibaba.fastjson.JSON;
import com.codertl.pojo.DeliverReq;
import com.codertl.service.ICommodity;
import com.codertl.service.old.GoodsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class GoodsCommodityService implements ICommodity {

    private Logger logger =
            LoggerFactory.getLogger(GoodsCommodityService.class);
    private GoodsService goodsService = new GoodsService();

    @Override
    public void sendCommodity(String uId, String commodityId, String bizId, 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";
    }
}


第三方兑换卡:

package com.codertl.service.impl;

import com.alibaba.fastjson.JSON;
import com.codertl.service.ICommodity;
import com.codertl.service.old.IQiYiCardService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class CardCommodityService implements ICommodity {

    private Logger logger =
            LoggerFactory.getLogger(CardCommodityService.class);
    // 模拟注⼊
    private IQiYiCardService iQiYiCardService = new IQiYiCardService();

    @Override
    public void sendCommodity(String uId, String commodityId, String bizId, 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";
    }
}
- 从上⾯可以看到每⼀种奖品的实现都包括在⾃⼰的类中,新增、修改或者删除都不会影响其他奖品功能的测试,降低回归测试的可能。
- 后续在新增的奖品只需要按照此结构进⾏填充即可,⾮常易于维护和扩展。
- 在统⼀了⼊参以及出参后,调⽤⽅不在需要关⼼奖品发放的内部逻辑,按照统⼀的⽅式即可处理。

2.3 创建商店⼯⼚

package com.codertl.factory;

import com.codertl.service.ICommodity;
import com.codertl.service.impl.CardCommodityService;
import com.codertl.service.impl.CouponCommodityService;
import com.codertl.service.impl.GoodsCommodityService;

/**
 * @Author: codertl
 * @Description:
 * @Date: 2023/2/3
 */
public class StoreFactory {

    public ICommodity getCommodityService(Integer commodityType) {
        if (null == commodityType) {
            return null;
        }
        if (1 == commodityType) {
            return new CouponCommodityService();
        }
        if (2 == commodityType) {
            return new GoodsCommodityService();
        }
        if (3 == commodityType) {
            return new CardCommodityService();
        }
        throw new RuntimeException("不存在的商品服务类型");
    }
}
- 这⾥我们定义了⼀个商店的⼯⼚类,在⾥⾯按照类型实现各种商品的服务。可以⾮常⼲净整洁的处理你的代码,后续新增的商品在这⾥扩展即可。如果你不喜欢 if 判断,也可以使⽤ switch 或者 map 配置结构,会让代码更加⼲净。
- 另外很多代码检查软件和编码要求,不喜欢if语句后⾯不写扩展,这⾥是为了更加⼲净的向你体现逻辑。在实际的业务编码中可以添加括号。

3. 测试验证

package com.codertl;

import com.codertl.factory.StoreFactory;
import com.codertl.service.ICommodity;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.HashMap;
import java.util.Map;

@SpringBootTest
class FactoryDesignDemo101ApplicationTests {

    private Logger logger =
            LoggerFactory.getLogger(FactoryDesignDemo101ApplicationTests.class);

    @Test
    void contextLoads() throws Exception {
        StoreFactory storeFactory = new StoreFactory();
        // 1. 优惠券
        ICommodity commodityService_1 = storeFactory.getCommodityService(1);
        commodityService_1.sendCommodity("10001", "EGM1023938910232121323432",
                "791098764902132", null);
        // 2. 实物商品
        ICommodity commodityService_2 = storeFactory.getCommodityService(2);

        Map<String,String> extMap = new HashMap<String,String>();
        extMap.put("consigneeUserName", "谢⻜机");
        extMap.put("consigneeUserPhone", "15200292123");
        extMap.put("consigneeUserAddress", "吉林省.⻓春市.双阳区.XX街道.檀溪苑⼩ 区.#18-2109");
        commodityService_2.sendCommodity("10001","9820198721311","102300002011222 1113", extMap);
        // 3. 第三⽅兑换卡(爱奇艺)
        ICommodity commodityService_3 = storeFactory.getCommodityService(3);
        commodityService_3.sendCommodity("10001","AQY1xjkUodl8LO975GdfrYUio",null,null);
    }
}

4. 结果

2023-02-03 16:18:30.054  INFO 15456 --- [           main] c.c.service.impl.CouponCommodityService  : 请求参数[优惠券] => uId:10001 commodityId:EGM1023938910232121323432 bizId:791098764902132 extMap:null
2023-02-03 16:18:30.074  INFO 15456 --- [           main] c.c.service.impl.CouponCommodityService  : 测试结果[优惠券]:{"code":"0000","info":"发放成功"}
2023-02-03 16:18:30.079  INFO 15456 --- [           main] c.c.service.impl.GoodsCommodityService   : 请求参数[实物商品] => uId:10001 commodityId:9820198721311 bizId:102300002011222 1113 extMap:{"consigneeUserName":"谢⻜机","consigneeUserPhone":"15200292123","consigneeUserAddress":"吉林省.⻓春市.双阳区.XX街道.檀溪苑⼩ 区.#18-2109"}
2023-02-03 16:18:30.079  INFO 15456 --- [           main] c.c.service.impl.GoodsCommodityService   : 测试结果[实物商品]:true
2023-02-03 16:18:30.081  INFO 15456 --- [           main] c.c.service.impl.CardCommodityService    : 请求参数[爱奇艺兑换卡] => uId:10001 commodityId:AQY1xjkUodl8LO975GdfrYUio bizId:null extMap:null
2023-02-03 16:18:30.081  INFO 15456 --- [           main] c.c.service.impl.CardCommodityService    : 测试结果[爱奇艺兑换卡]:success

- 运⾏结果正常,既满⾜了业务产品需求,也满⾜了⾃⼰对代码的追求。这样的代码部署上线运⾏,内⼼不会恐慌,不会觉得半夜会有电话。
- 另外从运⾏测试结果上也可以看出来,在进⾏封装后可以⾮常清晰的看到⼀整套发放奖品服务的完整性,统⼀了⼊参、统⼀了结果。
posted @ 2023-02-03 21:40  CoderTL  阅读(74)  评论(0编辑  收藏  举报