【项目实践】依赖注入用得好,设计模式轻松搞

以项目驱动学习,以实践检验真知java

前言

设计模式是咱们编程道路上绕不开的一环,用好了设计模式可以让代码拥有良好的维护性、可读性以及扩展性,它仿佛就是“优雅”的代名词,各个框架和库也都能见到它的身影。git

正是由于它有种种好处,因此不少人在开发时总想将某个设计模式用到项目中来,然而每每会用得比较别扭。其中一部分缘由是业务需求并不太符合所用的设计模式,还有一部分缘由就是在Web项目中咱们对象都是交由Spring框架的Ioc容器来管理,不少设计模式没法直接套用。那么在真正的项目开发中,咱们就须要对设计模式作一个灵活的变通,让其可以和框架结合,在实际开发中发挥出真正的优点。github

当项目引入IoC容器后,咱们通常是经过依赖注入来使用各个对象,将设计模式和框架结合的关键点就在于此!本文会讲解如何经过依赖注入来完成咱们的设计模式,由浅入深,让你在了解几个设计模式的同时掌握依赖注入的一些妙用。web

本文全部代码都放在Github上了,克隆下来便可运行查看效果。编程

实战

单例模式

单例应该是不少人接触的第一个设计模式,相比其余设计模式来讲单例的概念很是简单,即在一个进程中,某个类从始至终只有一个实例对象。不过概念就算再简单,仍是须要一点编码才能实现,R 以前的文章 回字有四种写法,那你知道单例有五种写法吗 就有详细的讲解,这里对该设计模式就不过多介绍了,我们直接来看看在实际开发中如何运用该模式。设计模式

交由Spring IoC容器管理的对象称之为Bean,每一个Bean都有其做用域(scope),这个做用域能够理解为Spring控制Bean生命周期的方式。建立和销毁是生命周期中必不可少的节点,单例模式的重点天然是对象的建立。而Spring建立对象的过程对于咱们来讲是无感知的,即咱们只需配置好Bean,而后经过依赖注入就可使用对象了:bash

@Service //和@Component功能同样,将该类声明为Bean交由容器管理
public class UserServiceImpl implements UserService{
}

@Controller
public class UserController {
    @Autowired // 依赖注入
    private UserService userService;
}
复制代码

那这个对象的建立咱们该如何控制呢?微信

其实,Bean默认的做用范围就是单例的,咱们无需手写单例。要想验证Bean是否为单例很简单,咱们在程序各个地方获取Bean后打印其hashCode就能够看是否为同一个对象了,好比两个不一样的类中都注入了UserServicewebsocket

@Controller
public class UserController {
    @Autowired
    private UserService userService;
    
    public void test() {
        System.out.println(userService.hashCode());
    }
}

@Controller
public class OtherController {
    @Autowired
    private UserService userService;
    
    public void test() {
        System.out.println(userService.hashCode());
    }
}
复制代码

打印结果会是两个相同的hashCodemarkdown

为何Spring默认会用单例的形式来实例化Bean呢?这天然是由于单例能够节约资源,有不少类是不必实例化多个对象的。

若是咱们就是想每次获取Bean时都建立一个对象呢?咱们能够在声明Bean的时候加上@Scope注解来配置其做用域:

@Service
@Scope("prototype")
public class UserServiceImpl implements UserService{
}
复制代码

这样当你每次获取Bean时都会建立一个实例。

Bean的做用域有如下几种,咱们能够根据需求配置,大多数状况下咱们用默认的单例就行了:

名称 说明
singleton 默认做用范围。每一个IoC容器只建立一个对象实例。
prototype 被定义为多个对象实例。
request 限定在HTTP请求的生命周期内。每一个HTTP客户端请求都有本身的对象实例。
session 限定在HttpSession的生命周期内。
application 限定在ServletContext的生命周期内。
websocket 限定在WebSocket的生命周期内。

这里要额外注意一点,Bean的单例并不能彻底算传统意义上的单例,由于其做用域只能保证在IoC容器内保证只有一个对象实例,可是不能保证一个进程内只有一个对象实例。也就是说,若是你不经过Spring提供的方式获取Bean,而是本身建立了一个对象,此时程序就会有多个对象存在了:

public void test() {
    // 本身new了一个对象
    System.out.println(new UserServiceImpl().hashCode());
}
复制代码

这就是须要变通的地方,Spring能够说在咱们平常开发中覆盖了每个角落,只要本身不故意绕开Spring,那么保证IoC容器内的单例基本就等同于保证了整个程序内的单例。

责任链模式

概念比较简单的单例讲解完后,我们再来看看责任链模式。

模式讲解

该模式并不复杂:一个请求能够被多个对象处理,这些对象链接成一条链而且沿着这条链传递请求,直到有对象处理它为止。该模式的好处是让请求者和接受者解耦,能够动态增删处理逻辑,让处理对象的职责拥有了很是高的灵活性。咱们开发中经常使用的过滤器Filter和拦截器Interceptor就是运用了责任链模式。

光看介绍只会让人云里雾里,咱们直接来看下该模式如何运用。

就拿工做中的请假审批来讲,当咱们发起一个请假申请的时候,通常会有多个审批者,每一个审批者都表明着一个责任节点,都有本身的审批逻辑。咱们假设有如下审批者:

组长Leader:只能审批不超过三天的请假;

经理Manger:只能审批不超过七天的请假;

老板Boss:可以审批任意天数。

我们先定义一个请假审批的对象:

public class Request {
    /** * 请求人姓名 */
    private String name;
    /** * 请假天数。为了演示就简单按成天来算,不弄什么小时了 */
    private Integer day;

    public Request(String name, Integer day) {
        this.name = name;
        this.day = day;
    }
    
    // 省略get、set方法
}
复制代码

按照传统的写法是接受者收到这个对象后经过条件判断来进行相应的处理:

public class Handler {
    public void process(Request request) {
        System.out.println("---");

        // Leader审批
        if (request.getDay() <= 3) {
            System.out.println(String.format("Leader已审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
            return;
        }
        System.out.println(String.format("Leader没法审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));

        // Manger审批
        if (request.getDay() <= 7) {
            System.out.println(String.format("Manger已审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
            return;
        }
        System.out.println(String.format("Manger没法审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));

        // Boss审批
        System.out.println(String.format("Boss已审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));

        System.out.println("---");
    }
}
复制代码

在客户端模拟审批流程:

public class App {
    public static void main( String[] args ) {
        Handler handler = new Handler();
        handler.process(new Request("张三", 2));
        handler.process(new Request("李四", 5));
        handler.process(new Request("王五", 14));
    }
}
复制代码

打印结果以下:

---
Leader已审批【张三】的【2】天请假申请
---
Leader没法审批【李四】的【5】天请假申请
Manger已审批【李四】的【5】天请假申请
---
Leader没法审批【王五】的【14】天请假申请
Manger没法审批【王五】的【14】天请假申请
Boss已审批【王五】的【14】天请假申请
---
复制代码

不难看出Handler类中的代码充满了坏味道!每一个责任节点间的耦合度很是高,若是要增删某个节点,就要改动这一大段代码,很不灵活。并且这里演示的审批逻辑还只是打印一句话而已,在真实业务中处理逻辑可比这复杂多了,若是要改动起来简直就是灾难。

这时候咱们的责任链模式就派上用场了!咱们将每一个责任节点封装成独立的对象,而后将这些对象组合起来变成一个链条,并经过统一入口挨个处理。

首先,咱们要抽象出责任节点的接口,全部节点都实现该接口:

public interface Handler {
    /** * 返回值为true,则表明放行,交由下一个节点处理 * 返回值为false,则表明不放行 */
    boolean process(Request request);
}
复制代码

以Leader节点为例,实现该接口:

public class LeaderHandler implements Handler{
    @Override
    public boolean process(Request request) {
        if (request.getDay() <= 3) {
            System.out.println(String.format("Leader已审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
            // 处理完毕,不放行
            return false;
        }
        System.out.println(String.format("Leader没法审批【%s】的【%d】天请假申请", request.getName(), request.getDay()));
        // 放行
        return true;
    }
}
复制代码

而后定义一个专门用来处理这些Handler的链条类:

public class HandlerChain {
    // 存放全部Handler
    private List<Handler> handlers = new LinkedList<>();

    // 给外部提供一个增长Handler的入口
    public void addHandler(Handler handler) {
        this.handlers.add(handler);
    }

    public void process(Request request) {
        // 依次调用Handler
        for (Handler handler : handlers) {
            // 若是返回为false,停止调用
            if (!handler.process(request)) {
                break;
            }
        }
    }
    
}
复制代码

如今咱们来看下使用责任链是怎样执行审批流程的:

public class App {
    public static void main( String[] args ) {
        // 构建责任链
        HandlerChain chain = new HandlerChain();
        chain.addHandler(new LeaderHandler());
        chain.addHandler(new ManagerHandler());
        chain.addHandler(new BossHandler());
        // 执行多个流程
        chain.process(new Request("张三", 2));
        chain.process(new Request("李四", 5));
        chain.process(new Request("王五", 14));
    }
}
复制代码

打印结果和前面一致。

这样带来的好处是显而易见的,咱们能够很是方便地增删责任节点,修改某个责任节点的逻辑也不会影响到其余的节点,每一个节点只需关注本身的逻辑。而且责任链是按照固定顺序执行节点,按照本身想要的顺序添加各个对象便可方便地排列顺序。

此外责任链有不少变体,好比像Servlet的Filter执行下一个节点时还须要持有链条的引用:

public class MyFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        if (...) {
            // 经过链条引用来放行
            chain.doFilter(req, resp);
        } else {
            // 若是没有调用chain的方法则表明停止往下传递
            ...
        }
    }
}
复制代码

各责任链除了传递的方式不一样,总体的链路逻辑也能够有所不一样。

咱们刚才演示的是将请求交由某一个节点进行处理,只要有一个处理了,后续就不用处理了。有些责任链目的不是找到某一个节点来处理,而是每一个节点都作一些事,至关于一个流水线。

好比像刚才的审批流程,咱们能够将逻辑改成一个请假申请须要每个审批人都赞成才算申请经过,Leader赞成了后转给Manger审批,Manger赞成了后转给Boss审批,只有Boss最终赞成了才生效。

形式有多种,其核心概念是将请求对象链式传递,不脱离这一点就均可以算做责任链模式,无需太死守定义。

配合框架

责任链模式中,咱们都是本身建立责任节点对象,而后将其添加到责任链条中。在实际开发中这样就会有一个问题,若是咱们的责任节点里依赖注入了其它的Bean,那么手动建立对象的话则表明该对象就没有交由Spring管理,那些属性也就不会被依赖注入:

public class LeaderHandler implements Handler{
    @Autowired // 手动建立LeaderHandler则该属性不会被注入
    private UserService userService;
}
复制代码

此时咱们就必须将各个节点对象也交由Spring来管理,而后经过Spring来获取这些对象实例,再将这些对象实例放置到责任链中。其实这种方式大部分人都接触过,Spring MVC的拦截器Interceptor就是这样使用的:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 获取Bean,添加到责任链中(注意哦,这里是调用的方法来获取对象,而不是new出对象)
        registry.addInterceptor(loginInterceptor());
        registry.addInterceptor(authInterceptor());
    }
    
    // 经过@Bean注解将自定义拦截器交由Spring管理
    @Bean
    public LoginInterceptor loginInterceptor() {return new LoginInterceptor();}
    @Bean
    public AuthInterceptor authInterceptor() {return new AuthInterceptor();}
}
复制代码

InterceptorRegistry就至关于链条类了,该对象由Spring MVC传递给咱们,好让咱们添加拦截器,后续Spring MVC会自行调用责任链,咱们无需操心。

别人框架定义的责任链会由框架调用,那咱们自定义的责任链该如何调用呢?这里有一个更为简便的方式,那就是将Bean依赖注入到集合中

咱们平常开发时都是使用依赖注入获取单个Bean,这是由于咱们声明的接口或者父类一般只需一个实现类就能够搞定业务需求了。而刚才咱们自定义的Handler接口下会有多个实现类,此时咱们就能够一次性注入多个Bean!我们如今就来改造一下以前的代码。

首先,将每一个Handler实现类加上@Service注解,将其声明为Bean:

@Service
public class LeaderHandler implements Handler{
    ...
}

@Service
public class ManagerHandler implements Handler{
    ...
}

@Service
public class BossHandler implements Handler{
    ...
}
复制代码

而后咱们来改造一下咱们的链条类,将其也声明为一个Bean,而后直接在成员变量上加上@Autowired注解。既然都经过依赖注入来实现了,那么就无需手动再新增责任节点,因此咱们将以前的添加节点的方法给去除:

@Service
public class HandlerChain {
    @Autowired
    private List<Handler> handlers;

    public void process(Request request) {
        // 依次调用Handler
        for (Handler handler : handlers) {
            // 若是返回为false,停止调用
            if (!handler.process(request)) {
                break;
            }
        }
    }

}
复制代码

没错,依赖注入很是强大,不止可以注入单个对象,还能够注入多个!这样一来就很是方便了,咱们只需实现Handler接口,将实现类声明为Bean,就会自动被注入到责任链中,咱们甚至都不用手动添加。要想执行责任链也特别简单,只需获取HandlerChain而后调用便可:

@Controller
public class UserController {
    @Autowired
    private HandlerChain chain;

    public void process() {
        chain.process(new Request("张三", 2));
        chain.process(new Request("李四", 5));
        chain.process(new Request("王五", 14));
    }
    
}
复制代码

执行效果以下:

---
Boss已审批【张三】的【2】天请假申请
---
Boss已审批【李四】的【5】天请假申请
---
Boss已审批【王五】的【14】天请假申请
复制代码

咦,所有都是Boss审批了,为啥前面两个节点没有生效呢?由于咱们尚未配置Bean注入到集合中的顺序,咱们须要加上@Order注解来控制Bean的装配顺序,数字越小越靠前:

@Order(1)
@Service
public class LeaderHandler implements Handler{
    ...
}

@Order(2)
@Service
public class ManagerHandler implements Handler{
    ...
}

@Order(3)
@Service
public class BossHandler implements Handler{
    ...
}
复制代码

这样咱们自定义的责任链模式就完美融入到Spring中了!

策略模式

乘热打铁,咱们如今再来说解一个新的模式!

模式讲解

咱们开发中常常碰到这样的需求:须要根据不一样的状况执行不一样的操做。好比咱们购物最多见的邮费,不一样的地区、不一样的商品邮费都会不一样。假设如今需求是这样的:

包邮地区:没有超过10KG的货物免邮,10KG以上8元;

邻近地区:没有超过10KG的货物8元,10KG以上16元;

偏远地区:没有超过10KG的货物16元,10KG以上15KG如下24元, 15KG以上32元。

那咱们计算邮费的方法大概是这样的:

// 为了方便演示,重量和金额就简单设置为整型
public long calPostage(String zone, int weight) {
    // 包邮地区
    if ("freeZone".equals(zone)) {
        if (weight <= 10) {
            return 0;
        } else {
            return 8;
        }
    }

    // 近距离地区
    if ("nearZone".equals(zone)) {
        if (weight <= 10) {
            return 8;
        } else {
            return 16;
        }
    }

    // 偏远地区
    if ("farZone".equals(zone)) {
        if (weight <= 10) {
            return 16;
        } else if (weight <= 15) {
            return 24;
        } else {
            return 32;
        }
    }
	
    return 0;
}
复制代码

这么点邮费规则就写了如此长的代码,要是规则稍微再复杂点简直就更长了。并且若是规则有变,就要对这一大块代码缝缝补补,长此以往代码就会变得很是难以维护。

咱们首先想到的优化方式是将每一块的计算封装成方法:

public long calPostage(String zone, int weight) {
    // 包邮地区
    if ("freeZone".equals(zone)) {
        return calFreeZonePostage(weight);
    }

    // 近距离地区
    if ("nearZone".equals(zone)) {
        return calNearZonePostage(weight);
    }

    // 偏远地区
    if ("farZone".equals(zone)) {
        return calFarZonePostage(weight);
    }
	
    return 0;
}
复制代码

这样确实不错,大部分状况下也能知足需求,可依然不够灵活。

由于这些规则都是写死在咱们方法内的,若是调用者想使用本身的规则,或者常常修改规则呢?总不能动不动就修改咱们写好的代码吧。要知道邮费计算只是订单价格计算的一个小环节,咱们当然能够写好几种规则定式来提供服务,但也得容许别人自定义规则。此时,咱们更应该将邮费计算操做高度抽象成一个接口,有不一样的计算规则就实现不一样的类。不一样规则表明不一样策略,这种方式就是咱们的策略模式!咱们来看下具体写法:

首先,封装一个邮费计算接口:

public interface PostageStrategy {
    long calPostage(int weight);
}
复制代码

而后,咱们将那几个地区规则封装成不一样的实现类,拿包邮地区示例:

public class FreeZonePostageStrategy implements PostageStrategy{
    @Override
    public long calPostage(int weight) {
        if (weight <= 10) {
            return 0;
        } else {
            return 8;
        }
    }
}
复制代码

最后,要应用策略的话咱们还须要一个专门类:

public class PostageContext {
    // 持有某个策略
    private PostageStrategy postageStrategy = new FreeZonePostageStrategy();
    // 容许调用方设置新的策略
    public void setPostageStrategy(PostageStrategy postageStrategy) {
        this.postageStrategy = postageStrategy;
    }
    // 供调用方执行策略
    public long calPostage(int weight) {
        return postageStrategy.calPostage(weight);
    }
}
复制代码

这样,调用方既可使用咱们已有的策略,也能够很是方便地修改或自定义策略:

public long calPrice(User user, int weight) {
    PostageContext postageContext = new PostageContext();
    // 自定义策略
    if ("RudeCrab".equals(user.getName())) {
        // VIP客户,20KG如下一概包邮,20KG以上只收5元
        postageContext.setPostageStrategy(w -> w <= 20 ? 0 : 5);
        return postageContext.calPostage(weight);
    }
    // 包邮地区策略
    if ("freeZone".equals(user.getZone())) {
        postageContext.setPostageStrategy(new FreeZonePostageStrategy());
        return postageContext.calPostage(weight);
    }
    // 邻近地区策略
    if ("nearZone".equals(user.getZone())) {
        postageContext.setPostageStrategy(new NearZonePostageStrategy());
        return postageContext.calPostage(weight);
    }
    
    ...
    
    return 0;
}
复制代码

能够看到,简单的逻辑直接使用Lambda表达式就完成了自定义策略,若逻辑复杂的话也能够直接新建一个实现类来完成。

这就是策略模式的魅力所在,容许调用方使用不一样的策略来获得不一样的结果,以达到最大的灵活性!

尽管好处不少,但策略模式缺点也很明显:

  • 可能会形成策略类过多的状况,有多少规则就有多少类
  • 策略模式只是将逻辑分发到不一样实现类中,调用方的if、else一个都没减小。
  • 调用方须要知道全部策略类才能使用现有的逻辑。

大部分缺点能够配合工厂模式或者反射来解决,但这样又增长了系统的复杂性。那有没有既能弥补缺点又不复杂的方案呢,固然是有的,这就是我接下来要讲解的内容。在策略模式配合Spring框架的同时,也能弥补模式自己的缺点!

配合框架

通过责任链模式我们就能够发现,其实所谓的配合框架就是将咱们的对象交给Spring来管理,而后经过Spring调用Bean便可。策略模式中,我们每一个策略类都是手动实例化的,那我们要作的第一步毫无疑问就是将这些策略类声明为Bean:

@Service("freeZone") // 注解中的值表明Bean的名称,这里为何要这样作,等下我会讲解
public class FreeZonePostageStrategy implements PostageStrategy{
	...
}

@Service("nearZone")
public class NearZonePostageStrategy implements PostageStrategy{
	...
}

@Service("farZone")
public class FarZonePostageStrategy implements PostageStrategy{
	...
}
复制代码

而后咱们就要经过Spring获取这些Bean。有人可能会天然联想到,咱们仍是将这些实现类都注入到一个集合中,而后遍历使用。这确实能够,不过太麻烦了。依赖注入但是很是强大的,不只能将Bean注入到集合中,还能将其注入到Map中

来看具体用法:

@Controller
public class OrderController {
    @Autowired
    private Map<String, PostageStrategy> map;

    public void calPrice(User user, int weight) {
        map.get(user.getZone()).calPostage(weight);
    }
}
复制代码

大声告诉我,清不清爽!简不简洁!优不优雅!

依赖注入可以将Bean注入到Map中,其中Key为Bean的名称,Value为Bean对象,这也就是我前面要在@Service注解上设置值的缘由,只有这样才能将让调用方直接经过Map的get方法获取到Bean,继而使用该Bean对象。

咱们以前的PostageContext类能够不要了,何时想调用某策略,直接在调用处注入Map便可。

经过这种方式,咱们不只让策略模式彻底融入到Spring框架中,还完美解决了if、else过多等问题!咱们要想新增策略,只需新建一个实现类并将其声明成Bean就好了,原有调用方无需改动一行代码便可生效。

小贴士:若是一个接口或者父类有多个实现类,但我又只想依赖注入单个对象,可使用@Qualifier("Bean的名称")注解来获取指定的Bean。

总结

本文介绍了三种设计模式,以及各设计模式在Spring框架下是如何运用的!这三种设计模式对应的依赖注入方式以下:

  • 单例模式:依赖注入单个对象
  • 责任链模式:依赖注入集合
  • 策略模式:依赖注入Map

将设计模式和Spring框架配合的关键点就在于,如何将模式中的对象交由Spring管理。这是本文的核心,这一点思考清楚了,各个设计模式才能灵活使用。

讲解到这里就结束了,本文全部代码都放在Github,克隆下来便可运行。若是对你有帮助,能够点赞关注,我会持续更新更多原创【项目实践】的!

微信上转载请联系公众号【RudeCrab】开启白名单,其余地方转载请标明原地址、原做者!

相关文章
相关标签/搜索