不带废话地说一遍设计模式(一)

本文假设读者对于设计模式有一定的理解,对代码抽象化有一定的认知,还有,本文 java。

限于篇幅,本文不会做非常深入的对设计模式的介绍,本人也不是严格设计模式的狂热拥护者。

我会介绍我们工作中常用到的设计模式,看来这也适合那些为面试而准备设计模式的读者。

我本来想一篇文章说所有的设计模式的,工作都完成一半了,不过篇幅大了些,所以拆一下,分为三篇分别介绍创建型模式、结构型模式和行为型模式,毕竟它们之间也是有本质的区别的,这也有利于读者对各类设计模式进行更深入的思考。

写这篇文章,我发现要把各个设计模式说清楚,就要说到其关键的点,这样读者才能永远记住,不然各个设计模式太相近了,对于初学者而言,看过就忘了。所以,如果你是设计模式的初学者,建议看完后,自己也可以把各个设计模式试着和别人说清楚。

我很想给大家贴生产环境中符合各个设计模式的代码,可是纯的完全遵守各个设计模式的代码本身不是很多,大部分都是各个设计模式的变种,其次就是大部分的代码量都比较大,文章篇幅实在有限,对此只能抱歉了。

目录

设计模式是对大家实际工作中写的各种代码进行高层次抽象的总结,其中最出名的当属 Gang of Four (GoF) 的分类了,他们将设计模式分类为 23 种经典的模式,根据用途又分为三大类,分别为创建型模式、结构型模式和行为型模式。

有一些重要的设计原则在开篇和大家分享下,这算是对于设计模式的共识:

  1. 面向接口编程,而不是面向实现。这个很重要,也是优雅的、可扩展的代码的第一步,这就不需要多说了吧。
  2. 职责单一原则。每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来
  3. 对修改关闭,对扩展开放。对修改关闭是说,我们辛辛苦苦加班写出来的代码,该实现的功能和该修复的 bug 都完成了,别人可不能说改就改;对扩展开放就比较好理解了,也就是说在我们写好的代码基础上,很容易实现对功能进行增强。

创建一个对象 – 创建型模式

说到创建一个对象,最熟悉的就是 new 一个对象,然后 set 相关属性。可是,如果我想要更加简单的办法,我想让代码更加优雅怎么办?那就需要引入我们的创建型模式了。

简单工厂模式

和名字一样简单,非常简单,本文不是说废话的,上代码:

public class FruitFactory {
    public static Fruit getFruit(String name) {
        if (name.equals("apple")) {
            Fruit apple = new Apple();
            apple.setSomething("");
            return apple;
        } else if (name.equals("orange")) {
            Fruit orange = new Orange();
            orange.xyz();
            return orange;
        } else {
            return null;
        }
    }
}

其中,Apple 和 Orange 都继承自 Fruit。

简单地说,简单工厂模式通常就是这样,一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象。

我们强调职责单一原则,一个类干一件事情,所以这里才说简单工厂里面只有一个对外的静态方法,FruitFactory 只要负责生产 Fruit。

工厂模式

简单工厂模式很简单,如果它能满足我们的需要,我觉得就不要折腾了。之所以需要引入工厂模式,是因为我们往往需要使用两个或两个以上的工厂。

public interface FoodFactory {
    Food getFood(String name);
}
public class ChineseFoodFactory implements FoodFactory {
    @Override
    public Food getFood(String name) {
        if (name.equals("A")) {
            return new ChineseFoodA();
        } else if (name.equals("B")) {
            return new ChineseFoodB();
        } else {
            return null;
        }
    }
}
public class AmericanFoodFactory implements FoodFactory {
    @Override
    public Food getFood(String name) {
        if (name.equals("A")) {
            return new AmericanFoodA();
        } else if (name.equals("B")) {
            return new AmericanFoodB();
        } else {
            return null;
        }
    }
}

其中,ChineseFoodA、ChineseFoodB、AmericanFoodA、AmericanFoodB 都派生自 Food。

客户端调用:

public class APP {
    public static void main(String[] args) {
        // 先选择一个具体的工厂
        FoodFactory factory = new ChineseFoodFactory();
        // 由第一步的工厂产生具体的对象,不同的工厂造出不一样的对象
        Food food = factory.getFood("A");
    }
}

第一步,我们需要选取合适的工厂,然后第二步基本上和简单工厂一样。

核心在于,我们需要在第一步选好我们需要的工厂。比如,我们有 LogFactory 接口,实现类有 FileLogFactory 和 KafkaLogFactory,分别对应将日志写入文件和写入 Kafka 中,显然,我们客户端第一步就需要决定到底要实例化 FileLogFactory 还是 KafkaLogFactory,这将决定之后的所有的操作。

抽象工厂模式

当涉及到产品族的时候,就需要引入抽象工厂模式了。

一个经典的例子是造一台电脑,因为电脑是由许多的构件组成的,我们将 CPU 和主板进行抽象,然后 CPU 由 CPUFactory 生产,主板由 MainBoardFactory 生产,然后,我们再将 CPU 和主板搭配起来组合在一起,如下图:

《不带废话地说一遍设计模式(一)》

我们可以认为上图中左右分别是一个工厂模式的使用,如果 CPU 和主板以及电脑中的其他组件都遵守同一套标准,CPU 和主板之间可以随意组合(这也就是所谓的标准),那么此模式非常成功,因为它非常好扩展。比如我们要加个硬盘,那么我们需要一个 HardDiskFactory 接口,然后有很多的硬盘工厂,这些工厂都可以造出它们自己的 HardDisk 的具体实现。

引入抽象工厂模式为了解决此问题:IntelCPU + AmdMainBoard 不工作,AmdCPU + IntelMainBoard 也不工作,它们之间不兼容,Intel 公司和 AMD 公司都自己制定标准,都想要垄断整个市场,那怎么办呢?

这只是例子。PC 世界标准很重要,所以我们组装 PC 机才能如此简单。

再多一点点废话,这里就会引入产品族的概念,CPU + 主板 + 硬盘 + … 它们是一个产品族,凑在一起组成一台电脑:

《不带废话地说一遍设计模式(一)》

对于消费者来说,大家都不希望我们买了 CPU、主板、硬盘,可是它们居然是不兼容的,一种就是我们说的标准,碰到类似代码问题的时候,我们先想想朝着这个方向能不能走通。另一种就是选定一个产品族,然后再进行购买。总不至于 IntelCPU 和自家的 IntelMainBoard 不兼容吧,这肯定不会出现对吧。

所以,上面的两个工厂模式(也可以是多个)变一下就成了抽象工厂模式了:

《不带废话地说一遍设计模式(一)》

区别就是,现在 Intel 的工厂和 AMD 的工厂都变成了大厂,不再是只生产 CPU 或主板或硬盘这种独立的部件了。至于考虑到扩展,如果要增加硬盘,那么每个工厂都要进行相应的改动,都需要增加 makeHardDisk() 方法。

最后,进行客户端演示:

public class APP {
    public static void main(String[] args) {
        // 第一步就要选定一个“大厂”
        ComputerFactory cf = new AmdFactory();
        // 从这个大厂造 CPU
        CPU cpu = cf.makeCPU();
        // 从这个大厂造主板
        MainBoard board = cf.makeMainBoard();
        // 将同一个厂子出来的 CPU 和主板组装在一起
        Computer result = new Computer(cpu, board);
    }
}

单例模式

单例模式用得最多,错得最多。

饿汉模式最简单:

public class Singleton {
    // 首先,将 new Singleton() 堵死
    private Singleton() {};
    // 创建私有静态实例,意味着这个类第一次使用的时候就会进行创建
    private static Singleton instance = new Singleton();
    
    public static Singleton getInstance() {
        return instance;
    }
    // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getDate(...),
    // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
    public static Date getDate(String mode) {return new Date();}
}

很多人都能说出饿汉模式的缺点,可是我觉得生产过程中,很少碰到这种情况:你定义了一个单例的类,不需要其实例,可是你却把一个或几个你会用到的静态方法塞到这个类中。

饱汉模式最容易出错:

public class Singleton {
    // 首先,也是先堵死 new Singleton() 这条路
    private Singleton() {}
    // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
    private static volatile Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) {
            // 加锁
            synchronized (Singleton.class) {
                // 这一次判断也是必须的,不然会有并发问题
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

双重加锁,volatile 保证了线程间的可见性,synchronized 对可能的并发问题做同步

很多人不知道怎么写,直接就在 getInstance() 方法签名上加上 synchronized,这就不多说了。

嵌套类最经典,以后大家就用它吧:

public class Singleton3 {
    private Singleton3() {}
    // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
    private static class Holder {
        private static Singleton3 instance = new Singleton3();
    }
    public static Singleton3 getInstance() {
        return Holder.instance;
    }
}

注意,很多人都会把这个嵌套类说成是静态内部类,严格地说,内部类和嵌套类是不一样的,它们能访问的外部类权限也是不一样的。

最后,一定有人跳出来说用枚举实现单例,是的没错,枚举类很特殊,它在类加载的时候会初始化里面的所有的实例,而且 JVM 保证了它们不会再被实例化,所以它天生就是单例的。不说了,读者自己看着办吧,不建议使用。

建造者模式

经常碰见的 XxxBuilder 的类,通常都是建造者模式的产物。建造者模式其实有很多的变种,但是对于客户端来说,我们的使用通常都是一个模式的:

Food food = new FoodBuilder().a().b().c().build();
Food food = Food.builder().a().b().c().build();

套路就是先 new 一个 Builder,然后可以链式地调用一堆方法,最后再调用一次 build() 方法,我们需要的对象就有了。

来一个中规中矩的建造者模式:

class User {
    // 下面是“一堆”的属性
    private String  name;
    private String password;
    private String nickName;
    private int age;
    // 构造方法私有化,不然客户端就会直接调用构造方法了
    private User(String name, String password, String nickName, int age) {
        this.name = name;
        this.password = password;
        this.nickName = nickName;
        this.age = age;
    }
	// 静态方法,用于生成一个 Builder,这个不一定要有,不过写这个方法是一个很好的习惯,
    // 有些代码要求别人写 new User.UserBuilder().a()...build() 看上去就没那么好
    public static UserBuilder builder() {
        return new UserBuilder();
    }
   
    public static class UserBuilder {
        // 下面是和 User 一模一样的一堆属性
        private String  name;
        private String password;
        private String nickName;
        private int age;
        private UserBuilder() {
        }
        // 链式调用设置各个属性值,返回 this,即 UserBuilder
        public UserBuilder name(String name) {
            this.name = name;
            return this;
        }
        public UserBuilder password(String password) {
            this.password = password;
            return this;
        }
        public UserBuilder nickName(String nickName) {
            this.nickName = nickName;
            return this;
        }
        public UserBuilder age(int age) {
            this.age = age;
            return this;
        }
        // build() 方法负责将 UserBuilder 中设置好的属性“复制”到 User 中。
        // 当然,可以在 “复制” 之前做点检验
        public User build() {
            if (name == null || password == null) {
                throw new RuntimeException("用户名和密码必填");
            }
            if (age <= 0 || age >= 150) {
                throw new RuntimeException("年龄不合法");
            }
            // 还可以做赋予”默认值“的功能
          	if (nickName == null) {
                nickName = name;
            }
            return new User(name, password, nickName, age);
        }
    }
}

核心是:先把所有的属性都设置给 Builder,然后 build() 方法的时候,将这些属性复制给实际产生的对象。

看看客户端的调用:

public class APP {
    public static void main(String[] args) {
        User d = User.builder()
                .name("foo")
                .password("pAss12345")
                .age(25)
                .build();
    }
}

说实话,建造者模式的链式写法很吸引人,但是,多写了很多“无用”的 builder 的代码,感觉这个模式没什么用。不过,当属性很多,而且有些必填,有些选填的时候,这个模式会使代码清晰很多。我们可以在 Builder 的构造方法中强制让调用者提供必填字段,还有,在 build() 方法中校验各个参数比在 User 的构造方法中校验,代码要优雅一些。

题外话,强烈建议读者使用 lombok,用了 lombok 以后,上面的一大堆代码会变成如下这样:

@Builder
class User {
    private String  name;
    private String password;
    private String nickName;
    private int age;
}

怎么样,省下来的时间是不是又可以干点别的了。

当然,如果你只是想要链式写法,不想要建造者模式,有个很简单的办法,User 的 getter 方法不变,所有的 setter 方法都让其 return this 就可以了,然后就可以像下面这样调用:

User user = new User().setName("").setPassword("").setAge(20);

原型模式

这是我要说的创建型模式的最后一个设计模式了。

原型模式很简单:有一个原型实例,基于这个原型实例产生新的实例,也就是“克隆”了。

Object 类中有一个 clone() 方法,它用于生成一个新的对象,当然,如果我们要调用这个方法,java 要求我们的类必须先实现 Cloneable 接口,此接口没有任何方法,但是不这么做的话,在 clone() 的时候,会抛出 CloneNotSupportedException 异常。

protected native Object clone() throws CloneNotSupportedException;

java 的克隆是浅克隆,碰到对象引用的时候,克隆出来的对象和原对象中的引用将指向同一个对象。通常实现深克隆的方法是将对象进行序列化,然后再进行反序列化。

原型模式了解到这里我觉得就够了,各种变着法子说这种代码或那种代码是原型模式,没什么意义。

创建型模式总结

此总结的目的是告诉读者,我们经常碰到的创建型模式就这些了。

创建型模式总体上比较简单,它们的作用就是为了产生实例对象,算是各种工作的第一步了,因为我们写的是面向对象的代码,所以我们第一步当然是需要创建一个对象了。

简单工厂模式最简单;工厂模式在简单工厂模式的基础上增加了选择工厂的维度,需要第一步选择合适的工厂;抽象工厂模式有产品族的概念,如果各个产品是存在兼容性问题的,就要用抽象工厂模式。单例模式就不说了,为了保证全局使用的是同一对象,一方面是安全性考虑,一方面是为了节省资源;建造者模式专门对付属性很多的那种类,为了让代码更优美;原型模式用得最少,了解和 Object 类中的 clone() 方法相关的知识即可。

我总觉得创建型模式上升到设计模式有点怪怪的,它就是些创建对象的类似最佳实践的代码风格而已😂,不说了,说多会被人批评。感兴趣的读者可以关注后续即将成文的对结构型模式和行为型模式的介绍。

(本文完)

    原文作者:算法小白
    原文地址: https://juejin.im/entry/59db86336fb9a0452206723e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞