谈谈Java经常使用类库中的设计模式 - Part Ⅰ

背景

最近一口气看完了Joshua Bloch大神的Effective Java(下文简称EJ)。书中以tips的形式罗列了Java开发中的最佳实践,每一个tip都将其意图和要点压缩在了标题里,这种作法我很喜欢:一来比较亲切,比起难啃的系统书,EJ就像是一本Java的《俚语指南》;二来记忆起来十分方便,整本书过一遍就能望标题生义。html

在通读这本书时,我发现做者屡次列举现有类库中的实现的设计模式,我有意将其收集起来,这些实现至关经典,我以为有必要落成一篇文章。随着之后对类库的理解愈来愈深,我也会持续追加上本身发现的Pattern。java

概述

因为篇幅限制,本主题会作成一个系列,每一个系列介绍3-4个模式。
本文介绍的设计模式(可跳转):程序员

建造者
工厂方法
享元
桥接面试

Here We Go

建造者 (Builder)

定义:将一个复杂对象的构建与它的表示分离,使得一样的构建过程能够建立不一样的表示。算法

场景:建立复杂对象的算法独立于该对象的组成部分以及它们的装配方式时;对象内部结构复杂;对象内部属性相互依赖。spring

类型:建立型数据库

建造者模式在Java中最普遍的用途就是复杂对象建立。比起类构造器或Getter/Setter,它同时保证了建立过程的可读性(和属性名一致的设参方法)安全性(未建立完毕的对象不会逸出),同时它还有:参数可选、可在类继承层次中复用、对集合类字段更加友好等等优势 。对于复杂的对象均可使用建造者模式,代价是必定的性能开销与编写工做量,好在后者能够用Lombok这样的代码生成插件来解决。设计模式

借助Lombok生成类的建造者:api

import lombok.*;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Foo {


    private String name;

  
    private Integer height;

   
    private Integer length;


     public static void main(String[] args) {
        Foo f = Foo.builder().name("SQUARE").height(10),length(10).build();
    }

}

除了使用建造者建立普通Java Bean以外,许多类库中配置类对象也照葫芦画瓢。好比SpringBoot中对Swagger2的简单配置,使其在生产环境下关闭。安全

@Configuration
public class SwaggerConfig {

    @Value("${spring.profiles.active}")
    private String prop;

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .enable(!prop.contains("prod"))
                .select()
                .apis(RequestHandlerSelectors.any()).build();
    }

}



工厂方法 (Factory Method)

定义:定义一个建立对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到其子类。

场景:明确计划不一样条件下建立不一样实例时。

类型:建立型

若要找工厂方法在JAVA原生类库中最贴切的对照物,非 Supplier 莫属。这个来自JAVA 8 Function包的函数式接口,将工厂方法模式的编写成本降到极低。

package java.util.function;

/**
 * Represents a supplier of results.
 *
 * <p>There is no requirement that a new or distinct result be returned each
 * time the supplier is invoked.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #get()}.
 *
 * @param <T> the type of results supplied by this supplier
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

若是将工厂类做为方法入参,将保证对象会在最迫切的时机才会建立:

public class StreamTest {

    public static void main(String[] args) {
        Stream.generate(()->{
            System.out.println("creating a new object.");
            return new Object();
        }).limit(3);
    }
    
}
==============================
输出:

Stream.generate(Supplier<T> s)建立了一个流,并声明了这个流的元素来源:一个由Lambda表达式编写的工厂。编译器将其推导为一个Supplier实例。在对象建立时将打印日志,然而结果代表没有任何对象建立成功。由于在未声明终结函数时,赋予Stream的任何中间函数都不会执行。

使用工厂方法,生产数据的时机将由消费者把握,这是一种懒汉思想。

再回到 Supplier<T> 的定义,它是一个泛型,按照EJ的建议,当使用泛型做为方法入参和返回值时,最好遵循 PECS 规则。

Producer-Extends Consumer-Super

Supplier一般是放在方法入参的生产者,因此应该这么声明:

public void generate(Supplier<T extends Shape> supplier) {}

这样Shape的全部子类工厂都能传入到此方法中,加强其拓展性。对应了工厂方法定义当中 让子类决定实例化哪个类 的部分。




享元 (Flyweight)

定义:运用共享技术有效地支持大量细粒度的对象。

场景:应用使用大量对象,形成庞大的存储开销;对象中的大多数状态能够移至外部,剩下的部分能够共享。

类型:结构型

JDK类库中使用了大量的静态工厂(泛指建立对象的静态类/静态方法),这些静态工厂有一个重要的做用:为重复的调用返回相同的对象。使类成为实例受控的类(instance-controlled),这实际上就是享元的思想。
举个例子,下面是 Boolean.valueOf(boolean b) 的代码片断。

/**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code true}.
     */
    public static final Boolean TRUE = new Boolean(true);

    /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code false}.
     */
    public static final Boolean FALSE = new Boolean(false);


    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

Boolean 做为布尔类型的包装类,进行了实例控制。 由于布尔类型的值只有 True 和 False ,除此以外没有其余状态字段,因此类库设计者选择在类加载时初始化两个不可变实例,在静态工厂中不建立对象。
这也代表 Boolean.valueOf 返回的实例在执行==和equals时结果一致
其余包装类型如Byte,Short,Integer也有相似的设计:使用名为XCache(X表示类型名)的私有内部类中存储值在 -128 ~ 127 之间共256个实例,并在静态工厂中使用。在面试中常常遇见的数值包装类“==”问题,考点就在这里。




桥接(Bridge)

定义:将抽象部分与他的实现部分分离,使它们均可以独立地变化。

场景:实现系统可能有多个角度分类,每一种角度均可能变化;在构件的抽象化和具体化之间增长更多的灵活性,避免两个层次之间的静态继承关系;控制系统中继承层次过多过深。

类型:结构型

首先了解一个概念:服务提供者框架(Service Provider Framework)(下文简称SPF)。

服务提供者框架指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。
它由4种组件组成:

服务接口:需被实现的接口或抽象类
提供者注册API:实现类用来注册本身到SPF中的
服务访问API:客户端用来获取实现的
服务提供者接口:实现类的工厂对象,用来建立实现类的实例,是可选的

SPF模式的应用是如此普遍,其实现的变体也有不少。如Java 6提供的标准实现ServiceLoader,还有Spring、Guice这样的依赖注入框架。但我选择举一个你们更为熟悉的例子:JDBC

使用JDBC与数据库交互是每一个Java程序员的必经之路,而它的设计实际上也是SPF模式:

服务接口 -> Connection
提供者注册API:DriverManager.registerDriver
服务访问API:DriverManager.getConnection
服务提供者接口:Driver

想要使用不一样的数据库链接实现,只需经过服务访问API切换便可。这体现了桥接中将抽象与实现分离的精神。

(实际上若是加载多个数据库驱动,DriverManager会逐个尝试链接,并返回链接成功的实例。并不能人为选择提供者,但能够经过更改提供者注册代码来实现。)

Druid、Hikari等现代链接池的实现每每比JDBC定义的服务接口更加丰富,如监控、插件链、SQL日志等等,这体现了桥接当中的独立变化。




参考:

[1] Play With Java ServiceLoader And Forget About Dependency Injection Frameworks - (2016/10/02)
https://dzone.com/articles/play-with-java-serviceloader-forget-about-dependen

[2] Effective Java - 机械工业出版社 - Joshua Bloch (2017/11)

[3] 《大话设计模式》 - 清华大学出版社 - 陈杰 (2007/12)

相关文章
相关标签/搜索