23种设计模式 这栏文章让你拿捏得死死的——构建型模式
写在前面
为什么小付突然会想要出一篇关于23种设计模式的博文呢?因为我发现在学习很多框架(时看源码,不是简单地调用API)时,你会发现有很多巧妙的设计方式,但其实你看得懂它是咋个回事,但你不知道它的具体学名这就很尴尬了,所以此时小付就萌生出了这个念头了,况且在Java基础知识面试中这23种设计模式可谓是必考,说的更简单就是,这是基础,如果你连基础都不过关的话,谈何晋级一名稍微厉害一丢丢的程序员呢?故此,开始了今天的故事…
部分案例来自力扣《深入浅出设计模式》——冲冲冲!
前言
设计模式是什么?为什么要学?
面向对象的特点是什么?毋庸置疑,只要学过软件工程的崽子都知道:可维护性
、可重用性
、可扩展性
、灵活性
。
而面向对象的三大特性是:封装
、继承
、多态
。这里不再赘述。
面向对象设计得强大之处在于——随着业务变得越发复杂,面向对象设计依然能够使得程序结构良好
,而面向过程设计会导致程序越发臃肿
。
而设计模式就是让面向对象设计结构保持良好的秘诀,当面向对象结合设计模式
,才可以真正的体会到程序变得
:维护性好,重用率高,拓展性强、灵活多变
。设计模式对程序猿们并不陌生,多多少少有所听过,而且每位程序猿在深夜敲代码时没准也会在自己无意中用到或接触到设计模式,不过并没有察觉而已,为了更好地了解咱们现在使用的流行框架
,源码中
都有设计模式的思想
在维持着这个架构设计的开发,所以学好设计模式,我们刻不容缓,冲冲冲~
23种设计模式下的六大原则
无论何种设计模式,都是基于六大设计原则:
- 开放封闭原则:一个软件实体如类、
模块和函数应该对修改封闭,对扩展开放
。 - 单一职责原则:
一个类就干一件事
,一个类应该有有一个引起它修改的原因。 - 里氏替换原则:子类应该可以完全替换掉父类。换句话来说就是在使用继承时,
只扩展新的功能,而不要破坏父类原有的功能
。 - 依赖倒置原则:细节应该依赖于抽象,抽象
不应该依赖于细节
。把抽象层放在程序设计的高层,并保持稳定,程序的细节变化由底层的实现层
来完成。 - 接口分离原则:客户端不该依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让
实现类只需要以来自己需要的接口方法
。 - 迪米特法则:最少知道原则,尽量降低类与类之间的耦合;
一个对象应该对其他对象有最少的了解
。
构建型模式
工厂模式Factory
设计原因
在平时编码中,经常会通过new来实例化一个对象
,乍一看这种做法没什么不好,但实际上这是一种属于硬编码
的方式,如果你用过Spring的DI(依赖注入)
,你就会知道当我们需要的时候才进行创建
,不需要的时候就不创建
,这就是一种软编码
方式。 当咱们new了一个对象后,相当于调用者多知道了一个类
,增加了类与类之间的联系
,不利于
程序解耦
。其实构建过程可以进行封装,工厂模式便是用于封装对象的设计模式
。
简单工厂模式
用代码来举例,直接通过new来创建一个实例就好比当我们需要一台苹果手机的时候,我们需要知道苹果手机的构建方法,需要一台华为手机时,需要知道华为手机的构造方法。更好的实现方式是有个手机制造工厂
,我们只需要告知他我们需要什么样式的手机
,这个工厂就会把我们需要的手机造出来
(例如某强北去买手机不知道手机咋来的,你只要这款手机就对了)。我们不需要知道不同牌子的手机如何造出来
,我们只需要和工厂说明我们要啥就好了
。
MobilePhoneFactory.java
public class MobilePhoneFactory {
public MobilePhone create (String type){
switch (type){
case "IPHONE":return new IPHONE();
case "HUAWEIPhone":return new HUAWEIPhone();
default: throw new IllegalArgumentException("暂时不出售此类手机!");
}
}
}
Customer.java
public class Customer {
public void buy(){
System.out.println("/*************通过简单工厂模式进行获取用户所需要的产品**************");
MobilePhoneFactory mobilePhoneFactory = new MobilePhoneFactory();
MobilePhone iphone = mobilePhoneFactory.create("IPHONE");
MobilePhone huaweiPhone = mobilePhoneFactory.create("HUAWEIPhone");
iphone.showInfo();
huaweiPhone.showInfo();
}
}
注意
建构建过程封装不仅可以降低耦合度的同时,如果某个产品构造方法十分复杂,使用工厂模式可以大大减少代码重复。比如生产一个苹果手机需要屏幕、电池、主板,就将其修改如下:
public class MobilePhoneFactory {
public MobilePhone create (String type){
switch (type){
case "IPHONE":
Screen screen = new Screen();
Battery battery = new Battery();
Mainboard mainboard = new Mainboard();
return new IPHONE(battery,screen,mainboard);
case "HUAWEIPhone":return new HUAWEIPhone();
default: throw new IllegalArgumentException("暂时不出售此类手机!");
}
}
}
调用者的代码则完全不需要进行变化(即Customer无需改变),而且调用者不需要在每次需要手机的时候,自己去构建屏幕、电池、主板以获得想要的手机。手机的生产过程再怎么复杂,也只是工厂来帮我们创建,这就是封装的具体体现。如果手机需要更新换代更换新的技术,如4G变为5G手机,这也只是科学家们把相应的技术带到工厂进行零件加工,与我们无关,我们不需要注意它是咋样来的,我们只需要关心我们需要什么。
工厂模式一共有三种:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
注:简单工厂模式是让一个工厂类承担所有对象的职责。调用者需要什么,工厂就生产什么,缺点由此可见:
- 一:如果要生产的产品过多,会导致工厂类过于庞大,
承担过多的职责,就会变成超级类
,当过程需要进行修改时,都要来到这个工厂类找到指定位置进行加工修改
,违背了单一职责原则。(一个类就干一件事) - 二:当要生产新的产品时,必须
在工厂类下添加新的分支
。而开放封闭原则告诉我们:类应该对修改封闭,对扩展开放。我们希望在添加新的功能的时候,只需要增加新的类
,而并不是修改已经存在的类
,这就违背了开放封闭原则。
工厂方法模式
为了解决上述简单工厂模式出现的两个问题,工厂方法模式应运而生,它规定每个产品都有一个专属工厂。比如苹果手机有自己的工厂生产,华为手机有华为工厂进行生产。
苹果手机工厂:
public class IPHONEFactory {
public IPHONE create (){
return new IPHONE();
}
}
华为手机工厂:
public class HUAWEIFactory {
public HUAWEIPhone create (){
return new HUAWEIPhone();
}
}
用户:
public class Customer {
public void buy(){ System.out.println("/*************通过工厂模式进行获取用户所需要的产品**************");
IPHONEFactory iphoneFactory = new IPHONEFactory();
HUAWEIFactory huaweiFactory = new HUAWEIFactory();
MobilePhone iphone1 = iphoneFactory.create();
MobilePhone huaweiPhone1 = huaweiFactory.create();
iphone1.showInfo();
huaweiPhone1.showInfo();
}
}
或许有些人读到这里时就觉得不对劲来了,这样和直接通过new一个对象获取实例有区别么?前文提到工厂模式是为了减少类之间的耦合度,让调用者尽可能的少的和其他类打交道。用简单工厂模式,我们只需要知道手机工厂,无需知道他们如何制作的
,大大降低了耦合度
,但是工厂方法模式,虽然调用者无需知道苹果手机与华为手机是如何构造的,但是我们却需要知道这些制作工厂。有多少种产品就要知道多少种制作工厂,耦合度并没有下降,甚至使得代码臃肿冗余
。
请各位静静思考,工厂模式下的第二个优点是存在的,及时构建过程十分复杂时
,工厂也可将其封装,用户还是可以很方便的使用。
public class IPHONEFactory {
public IPHONE create (){
Screen screen = new Screen();
Battery battery = new Battery();
Mainboard mainboard = new Mainboard();
return new IPHONE(battery,screen,mainboard);
}
}
调用者无需知道知道生产细节,当生产过程需要修改时也无需更改调用端。同时,工厂方法模式解决了简单工厂模式的两个弊端。
- 当生产的产品种类越来越多的时候,工厂类不会变成超级类,虽然工厂类会越来越多,但是依旧保持灵活性,不会因为其越发庞大,而变得臃肿。如果
复杂的生产过程
需要修改的时候
,只需要修改其中的工厂中的构建方法
。符合单一职责原则。 - 当需要生产新的产品时,无需更改已经有的工厂,
只需要添加一个新的工厂
即可。保持
了面向对象的可扩展性
。符合开放封闭原则。
实例场景
现存SurgicalMask与N95Mask两种产品,都继承一个抽象类Mask
public abstract class Mask {
}
public class SurgicalMask extends Mask{
@Override
public String toString() {
return "SurgicalMask~";
}
}
public class N95Mask extends Mask{
@Override
public String toString() {
return "N95Mask!";
}
}
请使用简单工厂模式
的方法完成以下代码:
public class MaskFactory {
public Mask create(String type){
//使用简单工厂模式实现此处的逻辑
}
}
使其通过以下客户端的测试:
public class Client {
@Test
public void test() {
MaskFactory factory = new MaskFactory();
// 输出:SurgicalMask~
System.out.println(factory.create("Surgical"));
// 输出:N95Mask!
System.out.println(factory.create("N95"));
}
}
解析:
public class MaskFactory {
public Mask create(String type){
switch (type){
case "N95":return new N95Mask();
case "Surgical":return new SurgicalMask();
default: throw new IllegalArgumentException("没有这种口罩哦~");
}
}
}
如何使用工厂方法模式实现呢?
客户端测试代码:
public class Client {
@Test
public void test2() {
SurgicalMaskFactory surgicalMaskFactory = new SurgicalMaskFactory();
//输出:SurgicalMask~
System.out.println(surgicalMaskFactory.create());
N95MaskFactory N95MaskFactory = new N95MaskFactory();
//输出:N95Mask!
System.out.println(N95MaskFactory.create());
}
}
方案:
public class N95MaskFactory {
public Mask create(){
return new N95Mask();
}
}
public class SurgicalMaskFactory {
public Mask create(){
return new SurgicalMask();
}
}
抽象工厂模式Abstract Factory
工厂方法模式的进一步优化可以进一步进行优化,提取公共的工厂接口
//公共接口create方法
public interface Factory {
public Mask create();
}
而此时的工厂类实现公共接口修改如下:
public class N95MaskFactory implements Factory{
@Override
public Mask create(){
return new N95Mask();
}
}
public class SurgicalMaskFactory implements Factory{
@Override
public Mask create(){
return new SurgicalMask();
}
}
当二者均实现了其公共接口后就可以将二者同意作为Factory对象使用:
@Test
public void test3() {
Factory surgicalMaskFactory = new SurgicalMaskFactory();
//输出:SurgicalMask~
System.out.println(surgicalMaskFactory.create());
Factory N95MaskFactory = new N95MaskFactory();
//输出:N95Mask!
System.out.println(N95MaskFactory.create());
}
由此我们在创建了制定了具体的工厂类后,在使用时就无需关心是哪个工厂类,只需要将此工厂当做抽象的Factory接口使用就可以了。这种将工厂抽象的方法被称之为抽象工厂模式
。
由于此时用户只需要和抽象工厂进行打交道了,调用的也是接口中的方法,使用时也根本不需要知道在哪个具体工厂中实现的这些方法,这就使得替换工厂非常的容易。
例题
@Test
public void test4() {
Factory factory = new SurgicalMaskFactory();
Mask mask = factory.create();
mask.showInfo();
}
如果我们需要生产N95口罩只需要更改一处就可以了
@Test
public void test4() {
Factory factory = new N95MaskFactory();
Mask mask = factory.create();
mask.showInfo();
}
当抽象工厂(即公共接口)只有一个抽象方法时,或许你还无法理解其这样设计的好处,实际上抽象工厂的模式主要是用来替换一系列方法的,例如将程序中的MySQL数据库更换为Oracle数据库,使用抽象方法模式的话,只需要在抽象工厂DatabaseFactory接口中定义增删改查四个抽象方法
,让两个工厂分别去实现这个接口,调用时直接使用DatabaseFactory中的抽象方法就okk啦,我们不用知道调用的是什么数据库,也不用去知道不同数据库之间的差别,我们就可以直接利用此方式替换程序的数据库
,并且作为用户的我们也不知道其内部发生了,依旧可以按照往常一样查数据等。
注意
抽象工厂模式适用于增加同类工厂这样的横向扩展需求,不适合新增功能这样的纵向扩展。因为在工厂里增加新方法就对所以已经实现工厂的类都有影响。这也是其主要的缺点。
单例模式Singleton(重点!)
单例模式非常的常见,所以也经常被面试官作为校招基础的考察点,没事就让你手撕个单例模式玩玩,所以说作为重点来细致理解,绝对没错。
当某个对象全局只需要出现一个实例的时候
,就可以使用单例模式
。它的优点也是显而易见的:
- 它能够避免对象的重复创建,节约空间并且提升效率
- 避免由于操作不同实例导致的逻辑错误
单例模式咱们由浅入深来讲解:不过归根结底还是懒汉式与饿汉式。
饿汉式
- 饿汉式:变量在声明时便初始化。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
强烈建议手写!!!
可以看到,我们将构造方法定义为 private,这就保证了其他类无法实例化此类(万恶之源反射除外),必须通过 getInstance 方法才能获取到唯一的 instance 实例,非常直观。但饿汉式有一个弊端,那就是即使这个单例不需要使用,它也会在类加载之后立即创建出来,占用一块内存,并增加类初始化时间。就好比电脑坏了,先把所有工具拿出来,不管是不是所有的工具都用得上。就像一个饥不择食的饿汉,所以称之为饿汉式
。
懒汉式
饿汉式 是最简单的单例模式的写法,保证了线程安全
的同时也足够简单让我们第一时间想到,不过随着开发的进行,你会发现一个严重的问题,当如果我们设计的这个单例模式中有很多初始化的数据时
,代码一运行,数据被初始化就会占用大量的空间
,如果长时间没有来获取实例的时候,就不需要单例模式的对象,这样就会造成严重的空间浪费
,而我们学过了上两节内容,就更应该有需要用的时候才创建的思想,不需要就呆着这种做法,所以第二种单例模式;懒汉式就此诞生。
-
懒汉式
:先声明一个空变量,需要用时才初始化。例如:
public class Singleton {
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
我们声明了一个初始值为null的实例instance变量,当我们需要这个实例的时候,先进行一次判断是否已经经过初始化
,如果没有就new一个出来,就和我们之前的思想差不多了,需要的时候再拿,不需要的时候就干等着。
懒汉式解决了饿汉式浪费初始化空间内存的问题,避免空间浪费,减少类的初始化时间
。
但是这段代码在多线程并发操作下是不安全的
,当多个线程同时调用getInstance方法的时候,instance变量可能被实例化很多次了,为了线程安全,最简单的就是给判空条件加上同步锁:
public class Singleton {
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
return instance;
}
}
这样的话就可以解决线程不安全的问题了,一次最多只有一个线程能够执行判空条件并且创建新的实例对象的操作,所以instance只会被实例化一次,当多个线程调用getInstance方法时
,每次都会执行同步方法
,这样严重的影响了程序的执行效率
。所以更好的做法是在同步化之前
,再加上一层检查其当前实例是否已经被创建了
,就不用进入到同步方法当中了:
这就是被我们常常说道的双重检测锁DCL单例模式
:
public class Singleton {
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
//双重检测锁DCL单例模式
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
DCL懒汉式的单例,保证了线程的安全性
,又符合了懒加载,只有在用到的时候,才会去初始化,调用 效率
也比较高,但是这种写法在极端情况还是可能会有一定的问题。因为:
instance = new Singleton()
不是原子性操作,至少经历了三个步骤:(为什么?当你编译了java程序后得到的class文件里会告诉你答案qwq)
- 分配对象的内存空间
- 执行构造方法初始化对象
- 设置instance指向刚才分配的内存地址,此时instance!=null
但是由于指令重排的影响(多线程并发编程中讲到JMM模型时会有一个叫做volatile的关键字用来禁止指令重拍,不保证原子性,保证可见性或者说操作系统与计算机组成原理中会讲到屏蔽中断那一块也可以解释禁止指令重拍):导致A线程执行 instance = new Singleton();
的时候,可能先执行了第三步
(还没执行第 二步),此时线程B又进来了
,发现instance已经不为空了,直接返回了instance
,并且后面使用了返回 的
instance,由于线程A还没有执行第二步,导致此时instance还不完整
,可能会有一些意想不到的错 误,所以就有了下面一种单例模式。
增加了volatile关键字的DCL单例设计模式:
package com.alascanfu.SingletonModel;
public class Singleton {
//这个变量进行的任何修改都会直接 从本地内存 提交到 Java主内存当中
private volatile static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
那我们什么时候用懒汉式什么时候用饿汉式呢?
-
对于构建不复杂,加载完成后会立即使用的单例对象,推荐使用饿汉式。
-
对于构建过程耗时较长,并不是所有使用此类都会用到的单例对象,推荐使用懒汉式。
问:双检锁单例模式中,volatile 主要用来防止哪几条指令重排序?如果发生了重排序,会导致什么样的错误?
答案:上述代码中的 instance = new Singleton();可能会发生指令重排的操作,在这个过程中,如果第二条指令和第三条指令发生了重排序,可能导致 instance 还未初始化时,其他线程提前通过双检锁外层的 null 检查,获取到“不为 null,但还没有执行初始化”的 instance 对象,发生空指针异常。
但是单例模式一定就创建出来的是单例么?反射说:有我在你就不可能!
如果反射的基础使用还没有掌握,那么下面这篇文章可以让你快速入门反射。
Java基础——反射难道可以这么学 室友一把英雄联盟的时间 就能快速入门反射学不会来打我(框架的灵魂)
此处不再赘述了哦,咱们继续…
利用反射咱们就可以破除单例模式,你看:
public class Test {
public static void main(String[] args) {
try {
Singleton singleton1 = Singleton.getInstance();
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Singleton singleton2 = declaredConstructor.newInstance();
System.out.println(singleton1 == singleton2);//false
} catch (Exception e) {
e.printStackTrace();
}
}
}
因为反射的存在,当Single.class已经加载到运行时数据内存中,我们可以通过反射破除权限修饰符,private就形同虚设了。所以此处
singleton 1 == singleton 2 结果为:false
也就是说明此时不存在单例了。
那么如何解决反射来破除访问权限进行创建实例呢?
来,看代码:
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){
synchronized (Singleton.class){
if (instance != null){
throw new RuntimeException("不要试图用反射破坏单例模式!!!");
}
}
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
咱们为了预防反射创建,在构造函数中添加了一个同步锁,当构造单例时,必须等待当前实例创建完成后才能进入,通过反射创建实例其实就是通过反射获取构造函数创建实例。所以在其中加了一个判断条件。
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.alascanfu.SingletonModel.Test.main(Test.java:12)
Caused by: java.lang.RuntimeException: 不要试图用反射破坏单例模式!!!
at com.alascanfu.SingletonModel.Singleton.<init>(Singleton.java:8)
... 5 more
此时编辑器就会报错了!
但是该方法还是有不足之处:
上面我们是先正常的调用了getInstance方法,创建了LazyMan对象,所以第二次用反射创建对象,私有 构造函数里面的判断起作用了,反射破坏单例模式失败。但是如果破坏者干脆不先调用getInstance方 法,一上来就直接用反射创建对象,我们的判断就不生效了:
public class Test {
public static void main(String[] args) {
try {
// Singleton singleton1 = Singleton.getInstance();
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Singleton singleton2 = declaredConstructor.newInstance();
Singleton singleton3 = declaredConstructor.newInstance();
// System.out.println(singleton1 == singleton2);//false
System.out.println(singleton3 == singleton2);//false
} catch (Exception e) {
e.printStackTrace();
}
}
}
此时不会throw出异常,只会输出false所以又需要进行改进。
通过添加一个flag变量来维持是否第一次构建实例
public class Singleton {
private volatile static Singleton instance = null;
private static boolean flag = false;
private Singleton(){
synchronized (Singleton.class){
if (flag == false){
flag = true;
}else {
throw new RuntimeException("不要试图用反射破坏单例模式!!!");
}
}
}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
但是!!!!反射他还有方法修改有权限修饰符的属性…啊这…见招拆招来只好咱们再去进行修改了:
可以通过枚举来创建全局唯一的常量:
你在这里可以快速掌握Java枚举的基础使用。
当我们采用枚举去创建实例时获取的实例都是指向同一个物理地址,这就保证了不管多少个这个实例变量其实都是同一个东西。
public enum SingletonEnum {
//枚举的实例
INSTANCE;
public SingletonEnum getInstance(){
return INSTANCE;
}
}
public class Test {
public static void main(String[] args) {
SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;
System.out.println(instance1 == instance2);//true
}
}
那我们能否通过反射来破坏枚举类来完成创建新的实例呢?
答案是不能的,因为你尝试newInstance的时候他会抛出异常!
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.alascanfu.SingletonModel.Test.main(Test.java:14)
public class Test {
public static void main(String[] args) {
SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;
System.out.println(instance1 == instance2);//true
try {
Constructor<SingletonEnum> constructor = SingletonEnum.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
SingletonEnum instance3 = constructor.newInstance();
SingletonEnum instance4 = constructor.newInstance();
System.out.println(instance3 == instance4);
} catch (Exception e) {
e.printStackTrace();
}
}
}
至于这里的构造器为什么是这个类型的又要提及编译与反编译看源码才能知晓:
利用jad来进行反编译才能看见!javap都不行!
jad.exe的下载链接:链接:https://pan.baidu.com/s/1K3Mmm2Grlv5x2Y_NPEfS2Q 提取码:HHXF --来自百度网盘超级会员V3的分享
先把jad.exe放到jdk的bin目录下这样才能使用
然后通过指令jad -sjava xxxxx.class
进行反编译
这样你就知道为什么枚举的构造器是上述类型了。
单例模式这么学完保证你学到家了~
建造者模式Builder
建造者模式用于创建过程稳定,但是配置多变的对象。
建造者模式主要应用于构建过程稳定,可通过不同配置建造出不同对象的场景。
在《设计模式》中给定的定义是:将一个复杂的构建与其表示相分离
,使得同样的构建过程可以创建不同的表示
。
现在“建造者——指挥者”模式已经很少使用了,现在的建造者模式主要是通过链式调用生成不同的配置。
当需要制作一杯珍珠奶茶,他的制作过程是稳定的,除了必须知道种类和规格外,是否加冰,加辅料都是可供选择的,建造者模式如下:
package com.alascanfu.BuilderModel;
public class MilkTea {
private final String type;
private final String size;
private final Boolean pearl;
private final Boolean ice;
private MilkTea(Builder builder){
this.type = builder.type;
this.size = builder.size;
this.pearl = builder.pearl;
this.ice = builder.ice;
}
public static class Builder{
private final String type;
private String size = "中杯";
private Boolean pearl = true;
private Boolean ice = false;
public Builder(String type){
this.type = type;
}
public Builder size(String size){
this.size = size;
return this;
}
public Builder pearl(Boolean pearl){
this.pearl = pearl;
return this;
}
public Builder ice(Boolean ice){
this.ice = ice;
return this;
}
public MilkTea build(){
return new MilkTea(this);
}
}
public String getType() {
return type;
}
public String getSize() {
return size;
}
public Boolean getPearl() {
return pearl;
}
public Boolean getIce() {
return ice;
}
}
可以看到,我们将 MilkTea 的构造方法设置为私有的,所以外部不能通过 new 构建出 MilkTea 实例,只能通过 Builder 构建。对于必须配置的属性,通过 Builder 的构造方法传入,可选的属性通过 Builder 的链式调用方法传入,如果不配置,将使用默认配置,也就是中杯、加珍珠、不加冰。根据不同的配置可以制作出不同的奶茶:
客户需求:
package com.alascanfu.BuilderModel;
public class User {
public static void main(String[] args) {
MilkTea milkTea = new MilkTea.Builder("原味").build();
show(milkTea);
MilkTea chocolate = new MilkTea.Builder("巧克力味").size("大杯").ice(true).pearl(true).build();
show(chocolate);
MilkTea strawberry = new MilkTea.Builder("草莓味").size("小杯").ice(false).pearl(false).build();
show(strawberry);
}
private static void show(MilkTea milkTea) {
String pearl;
if (milkTea.getPearl()) {
pearl = "加珍珠";
}
else{
pearl = "不加珍珠";
}
String ice;
if (milkTea.getIce()) {
ice = "加冰";
} else {
ice = "不加冰";
}
System.out.println("一份" + milkTea.getSize() + "、"
+ pearl + "、"
+ ice + "的"
+ milkTea.getType() + "奶茶");
}
}
原型模式Prototype
原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
举个栗子~
还是奶茶,比如说小付就很喜欢喝原味的不加冰奶茶,
而同行的室友是个选择困难症患者,他说也要一杯跟我一样的
public class MilkTea {
public String type;
public boolean ice;
}
public class Customer {
public void order(){
MilkTea milkTeaOfAlascanfu = new MilkTea();
milkTeaOfAlascanfu.type = "原味";
milkTeaOfAlascanfu.ice = false;
MilkTea yourMilkTea = milkTeaOfAlascanfu;
}
}
这样看起来是没有什么问题的,但是这样他获得的是不是新的一杯奶茶么?
当然不是了,因为Java对于基本类型对象的赋值只是将其地址进行了传递,这样赋值之后,yourMilkTea仍然指向的是小付的那杯奶茶,并不会有新的奶茶产生。
public class Customer {
public void order(){
MilkTea milkTeaOfAlascanfu = new MilkTea();
milkTeaOfAlascanfu.type = "原味";
milkTeaOfAlascanfu.ice = false;
MilkTea yourMilkTea = new MilkTea();
yourMilkTea.type = "原味";
yourMilkTea.ice = false;
}
}
这样才是获得了一份新的奶茶,和小付一模一样的奶茶原味且不加冰。
如果同行的人数很多的话,那按照这个代码就要写很多次,不仅代码冗余而且浪费空间。
而小付喝腻了想换个口味,又想都跟我一样,那是不是每个人都需要进行修改,这下欧后了塞~
大批量的修改无疑是非常丑陋的做法,这就是我们需要 clone 方法的理由!
运用原型模式,在 MilkTea 中新增 clone 方法:
public class MilkTea {
public String type;
public boolean ice;
public MilkTea clone(){
MilkTea milkTea = new MilkTea();
milkTea.ice = this.ice;
milkTea.type = this.type;
return milkTea;
}
}
用户的下单方式也进行修改,调用我们写好的clone方法:
public class Customer {
public void order(){
MilkTea milkTeaOfAlascanfu = new MilkTea();
milkTeaOfAlascanfu.type = "原味";
milkTeaOfAlascanfu.ice = false;
MilkTea clone1 = milkTeaOfAlascanfu.clone();
}
}
这就是原型模式,Java 中有一个语法糖,让我们并不需要手写 clone 方法。这个语法糖就是 Cloneable 接口,我们只要让需要拷贝的类实现此接口即可。
public class MilkTea implements Cloneable{
public String type;
public boolean ice;
@Override
public MilkTea clone(){
MilkTea milkTea = new MilkTea();
milkTea.ice = this.ice;
milkTea.type = this.type;
return milkTea;
}
}
注意
注意Java自带的clone方法是浅拷贝,也就是说调用当前对象的clone方法,只有基本数据类型的参数会进行拷贝,而非基本类型的对象不会被拷贝一份,而是继续使用传递引用的方式,如果需要实现深拷贝,必须要自行手动修改clone方法才可以的哦~
构建型模式——小结
- 工厂方法模式:为每一类对象建立工厂,将对象交由工厂创建,客户端只和工厂打交道。
- 抽象工厂模式:为每一类工厂提取出抽象接口,使得新增工厂、替换工厂变得非常容易。
- 建造者模式:用于创建构造过程稳定的对象,不同的 Builder 可以定义不同的配置。
- 单例模式:全局使用同一个对象,分为饿汉式和懒汉式。懒汉式有双检锁和内部类两种实现方式。(还有枚举类型的单例)
- 原型模式:为一个类定义 clone 方法,使得创建相同的对象更方便。
写在后面
此次总结的是23种设计模式的第一类:构建性模式(CreditionalPatterns)
该类下共有五种设计模式:
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
剩下的设计模式会陆续更新,这也是框架的基础哦~
如果可以请一定要实操。
最后
每天进步点 每天收获点
愿诸君 事业有成 学有所获
如果觉得不错 别忘啦一键三连哦~