文章目录

  • 1. 六大设计原则
    • 1.1 单一职责
    • 1.2 开闭原则
    • 1.3 接口隔离原则
    • 1.4 依赖倒置原则
    • 1.5 迪米特法则
    • 1.6 里式替换原则
    • 1.7 组合优于继承原则
    • 1.8 设计原则总结
  • 2. 创建型模式
  • 设计模式的类型
    • 2.1 工厂设计模式
      • 2.1.1 简单工厂模式
      • 2.1.2 工厂方法模式
      • 2.1.3 抽象工厂
      • 2.1.4 工厂模式总结
      • 2.1.5 Spring中的工厂模式
      • 2.1.6 工作中工厂方法使用
    • 2.2 原型模式
      • 2.2.1 深拷贝浅拷贝
      • 2.2.2 封装深拷贝工具
      • 2.2.3 原型模式小结
    • 2.3 建造者模式
      • 2.3.1 建造者与工程模式的区别
      • 2.3.2建造者模式总结
  • 3. 结构型模式
    • 3.1 装饰器模式
      • 3.1.1 装饰器模式案例
      • 3.1.2 jdk中的装饰器模式
      • 3.1.3 装饰器模式总结
    • 3.2 适配器模式
      • 3.2.1 jdk中的适配器模式
      • 3.2.2 Spring中的适配器模式
      • 3.2.3 适配器模式总结
    • 3.3 代理模式
      • 3.3.1 问题引入
      • 3.3.2 jdk动态代理
      • 3.3.3 封装jdk动态代理帮助类
      • 3.3.4 Cglib动态代理
      • 3.3.5 JDK动态代理和CGLIB代理的区别
      • 3.3.6 Spring中的代理模式
      • 3.3.7 Dubbo中的代理模式
      • 3.3.8 代理模式总结
    • 3.4 桥接模式
  • 4. 行为型模式
    • 4.1 策略模式
      • 4.1.1 问题引入
      • 4.1.2 策略模式解决问题的核心
      • 4.1.3 策略模式原理
      • 4.1.4 策略模式在JDK中的应用
      • 4.1.5 策略模式总结
      • 3.3.5 JDK动态代理和CGLIB代理的区别
      • 3.3.6 Spring中的代理模式
      • 3.3.7 Dubbo中的代理模式
      • 3.3.8 代理模式总结
    • 3.4 桥接模式
  • 4. 行为型模式
    • 4.1 策略模式
      • 4.1.1 问题引入
      • 4.1.2 策略模式解决问题的核心
      • 4.1.3 策略模式原理
      • 4.1.4 策略模式在JDK中的应用
      • 4.1.5 策略模式总结

思维导图下载

我们知道只有运用好设计原则和设计模式,才能让我们写出更加优秀的代码或者设计更好软件架构,在实际开发中,由于许多不遵守设计原则和设计模式硬编码,导致项目代码及其晦涩难懂的例子比比皆是,作为一个软件开发人员,写出高质量、易懂、已维护的代码应该是我们每个人应该做到的

1. 六大设计原则

我们知道设计模式一共有23种,虽然模式很多,但是都是为了遵守和实现六大设计原则而诞生的,如果我们能够理解六大设计原则,相信我们也能像令狐冲修炼独孤九剑一样无招胜有招,毕竟功夫的最高境界就是忘记招式,下意识使用的就是最合理的招式

其中我们要注意的是,其实六大设计模式主要是为了满足,这个字,可能是需求变更、可能是场景变更,但是运用好六大设计模式后我们写出的代码就能很好的应对不断变化的场景,做到任他东南西北风我自岿然不动的境界

1.1 单一职责

单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。

简单的来讲:就是小到一个方法、一个类,大到一个框架,都只负责一件事情

例如:

  • Math.round(),只负责完成四舍五入的功能,其他的不负责
  • Reader类,只负责读取字符(文本文件)
  • SpringMVC,只负责简化MVC的开发(框架)

举个栗子,我们知道,中国有四大发明,分别是造纸术、指南针、火药、活字印刷术,这里要注意的是活字印刷术,而不是印刷术,我们思考一下为什么印刷术出现的比活字印刷术早,但是并没有被列入四大发明呢?活字印刷术看起来不应该只是印刷术的改良版本吗?

我们来设想一个场景,在没有活字印刷术之前如果现在需要批量印刷一篇文章,我们需要怎么做?找人抄吗?高效的方法是拿块木板,用篆刀雕刻出一篇文章,然后搞点墨水,一下就能印出一篇文章,这样的速度非常快!

但是现在变化来了,前面提到,设计原则主要就是为了应对变化的。现在有个师傅发现好不容易雕刻的木板上有个字写错了!那现在怎么办呢?只能重新篆刻。那如果下次又发现需要更换句子怎么办呢?我们发现雕版印刷术并不能很好的应对变化

再让我们看看活字印刷术是如何处理的?首先我们现在不再直接篆刻一块大大的木板了,我们将原本很大的职责拆分成一个一个的汉字,再通过组合的方式将我们需要的文章拼起来,这样下次字写错了只需要修改一个字即可

这就是单一职责的核心:通过高内聚、低耦合的设计方案,刚庞大的系统拆成单一职责的小功能,再通过灵活组合的方式完成功能,这样做最大的好处就是可以通过不断的组合,应对不断变化的场景

举个我们在编码中最容易遇到的情况,我们有的时候看别人的函数实现,有的恶心的代码可能一个函数中就有大几百行,然后你们全部都是业务逻辑,比如支付功能的函数中有查询商品库存 -> 查询用户余额 -> 确认订单 -> 调用支付接口这五个步骤,有的同学写代码上去就是搜哈,一股脑全写完,下次遇到个退货的功能需要用到查询商品库存、查询用户余额的代码,直接copy过去,造成代码臃肿,可读性差,正确的方式应该是将其封装成一个个的方法或函数,这样可以做到减少重复代码的效果

关于单一职责,这里就不用代码举例了,大家记住在平时编码中记住单一职责、不断组合就行

1.2 开闭原则

开闭原则(Open Close Principle)

开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类

举个栗子,我现在有一个刮胡刀,刮胡刀的功能应该就是刮胡子,但是我现在想要它拥有吹风机的能力

  • 违法开闭原则的做法是,把吹风机的功能加上了,可能就不能刮胡子了
  • 符合开闭原则的做法是,把吹风功能加上,且没有影响之前刮胡子的功能

例如我现在有一个商品类Goods,这个类之前有一个方法是获取它的价格,例如:

public class Goods {private BigDecimal price;public void setPrice(BigDecimal price) {this.price = price;}public BigDecimal getPrice() {return this.price;}
}

现在变化来了,当前商品需要打8折进行销售,不符合开闭原则的做法就是直接进原来的代码中进行修改,例如直接在getter方法中修改

public BigDecimal getPrice() {// BigDecimal.multiply就是乘法,BigDecimal可以防止精度丢失return this.price.multiply(new BigDecimal("0.8"));
}

这样显然就是不满足开闭原则的,因为我们对源代码进行了修改,如果下次是打七折,那是不是又要去改源代码呢

正确的做法应该是写一个子类DiscountGoods来拓展父类的功能,再在子类上进行修改,这样就不会破坏父类的功能,又能满足需求

public class DiscountGoods extends Goods{@Overridepublic BigDecimal getPrice() {return super.getPrice().multiply(new BigDecimal("0.8"));}
}

这就叫对扩展开发,对修改关闭。我们在用设计模式编码时应该时刻注意的是,改源码是一件非常危险的事情,因为一个功能并不是只有你在使用,很容易造成牵一发而动全身的效果

但是如果我们因为要遵守开闭原则,每次对功能进行修改的时候,都去新写一个类,这样的会很繁琐,所以我们的准则是:

  • 如果一个类是自己写的,自己修改不会影响该类在其他地方的效果(不会牵一发而动全身),那你就可以随意修改
  • 如果不是自己写的,自己不清楚修改后会带来什么样的影响,那就不要修改,而要符合开闭原则

1.3 接口隔离原则

接口隔离原则(Interface Segregation Principle)

使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口

接口隔离原则在我们设计接口的时候也是非常容易忽略从而造成问题的的一个原则,例如我现在要要设计一个动物的接口,统一动物的行为,我们可能会这样写:

public interface Animal{void eat();void swim();void fly();
}

我们看这三个行为,分别是吃、游泳和飞,我们定义的是动物的接口,这样好像并没有什么问题,动物确实拥有这三个行为,但是问题就在于动物这个接口范围太大了,并不是所以的动物都同时拥有这三个行为

例如下面的小狗类中,狗由于不会非,所以不应该有方法fly() 的实现!

public class Dog implements Animal {@Overridepublic void eat() {System.out.println("小狗在吃东西");}@Overridepublic void swim() {System.out.println("小狗会狗刨");}@Overridepublic void fly() {throw new UnsupportedOperationException("小狗不会飞");}
}

我们现在将这个大接口拆分一下:

interface Eatable{void eat();
}interface Swimable{void swim();
}interface Flyable{void fly();
}

再不断的组合,实现不同的接口,其实核心思想还是高内聚,低耦合,通过不断组合不可分割的功能完成最终需要的功能

是不是现在有一点点无招胜有招的感觉了,感觉习惯之后自然而然就好感觉这块设计有问题,然后设计更好能应对变化的方案

public class Dog implements Eatable, Swimable {@Overridepublic void eat() {System.out.println("小狗在吃东西");}@Overridepublic void swim() {System.out.println("小狗会狗刨");}
}

1.4 依赖倒置原则

依赖倒置原则(Dependence Inversion Principle)

这个是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。实际中开发的实践就是,面向接口编程

  • 上层不应该依赖于下层
  • 它们都应该依赖于抽象

依赖倒置在实际编码中通常采取的是:上层不能依赖于下层,他们都应该依赖于抽象

这里区分上下层的方法为:调用别的方法的就是上层,被调用的就是下层

举个栗子:我们现在有三个类,互相有依赖关系

class Person {public void feed(Dog dog) {System.out.println("开始喂dog...");}
}class Dog {public void eat() {System.out.println("狗啃骨头");}
}// ================================================================
public class AppTest {public static void main(String[] args) {Person person = new Person();Dog dog = new Dog();person.feed(dog);}
}

首先我们在依赖倒置原则里面非常重要的一点是,要区分依赖中的上层和下层,我们时刻要注意调用别的方法的就是上层,被调用的就是下层,所以这里的层级关系为:AppTest是Person的上层,Person是Dog的上层

我们来仔细思考一下上面的代码,这里好像没什么问题,但是我一直强调的是设计模式是为了应对变化,现在变化来了,现在客户端Person不仅需要喂狗,还需要喂猫,我们很容易直接添加一个Cat

class Cat {public void eat() {System.out.println("小猫吃鱼");}
}
public class AppTest {public static void main(String[] args) {Person person = new Person();Dog dog = new Dog();Cat cat = new Cat();// 喂狗person.feed(dog);// 喂猫person.feed(cat);}
}

这样明显会报错,因为之前的代码中只能喂狗,不能喂猫!

那怎么办呢?我直接重载一个方法,让Person类可以喂猫不就好了吗?

class Person {public void feed(Dog dog) {System.out.println("开始喂dog...");}public void feed(Cat dog) {System.out.println("开始喂Cat...");}
}

好家伙,这是不是为了应对变化直接改源码了?首当其冲的就是破坏了开闭原则,其次如果每次要多喂养一种动物就要去重载一个方法,这显然也不合理叭,这就是因为上层依赖于下层

读者可以先自己试着将这几个类的UML类图画出来,可能有的读者不太会画,这里补充一下UML类图的画法

我们知道类和类之间的关系有:关联、依赖、泛化、实现(空心三角箭头的虚线表示实现,实现接口)

其中关联又可以分为组合 + 聚和,如果没有细分,可以画成实线 + 箭头,不用画菱形

  • 组合关系是强关联,失去关联方,两者都不完整,例如大雁和翅膀,就是强关联,大雁不能失去翅膀
  • 聚和是弱关联,失去关联方,被关联方依旧完整,例如雁群和大雁,失去一只大雁,不影响雁群整体

现在我们来画一下上面人喂动物的UML类图,由于动物作为形参传入人类中,所以明显这是依赖关系,我们用虚线三角画即可

我们看出上面代码的问题,就是每当一个新的类需要依赖时,就要重载一个方法,这里就违反了依赖倒置原则,每当下层发生改变时,上层都要一起改变,这样的设计没有拓展性我们不应该依赖于具体的类,而应该依赖于抽象的接口!

我们想要的结果是下层代码发生变化,对于上层来说是无感知的!代码也不需要改动,这就是依赖倒置的核心!

我们回过头来分析问题,我们人类的动作是什么?是喂养动物!不是喂狗、喂猫,狗和猫只是动物的实现!所以我们应该进行依赖倒置,依赖抽象不依赖实现,这里我们只需要依赖一个抽象的动物类或者接口即可

class Person {public void feed(Animal animal) {System.out.println("开始喂dog...");}
}interface Animal {void eat();
}class Dog implements Animal{@Overridepublic void eat() {System.out.println("狗啃骨头");}
}class Cat implements Animal{@Overridepublic void eat() {System.out.println("小猫吃鱼");}
}// ================================================================
public class AppTest {public static void main(String[] args) {Person person = new Person();Dog dog = new Dog();Cat cat = new Cat();// 喂狗person.feed(dog);// 喂猫person.feed(cat);}
}

我们来看一下类图的变化:

这里可能有的读者会有疑问,为什么下层变了,上层不知道要叫依赖倒置,就叫下层变了,上层不知道不行吗?

看上面的图!之前的箭头是向下指的,是依赖具体的实现,现在箭头倒置过来了,大家都依赖于抽象!这就叫依赖倒置,如果看不明白翻到上面再看一遍下来,好好体会这个倒置的作用。这样以来,不论怎么改变,只需要不断添加新的依赖关系依赖抽象即可以不变应万变。这里读者再三强调,所以的设计原则核心思想都是一个字,,都是为了以不变应万变

对上层来说,就是分层,解耦,就是一个

编码中我们时刻要注意的就是:

  • 上层不应该依赖于下层
  • 它们都应该依赖于抽象

其中这种思想在工作中生活中也有很多栗子,很多时候问题都是出现在太依赖某些东西了,当依赖的东西变化,自己就乱了

举几个栗子:

  • 在软件开发公司,老板不能具体依赖一些人或者某些语言开发系统,例如java、go、csharp等等,如果产生依赖了就会导致如果现在会Java的跑了,公司没有会Java的了,只能更换架构体系,但是如果领导只管最终的结果,不管具体实现过程,那么矛盾就转移到下层了

  • 例如在用人体系,老板不应该直接管理众多的员工,如果过度依赖某些员工,势必会造成问题,一个好的公司不是靠人,而是靠制度、靠规范,应该让所有人依赖制度,老板只需要管理好制度,就能管理好所有人!这就是依赖倒置,在生活中这就叫画饼!

    一个大企业不可能去管理每个员工,但是只要管理好抽象的规则制度,让员工都遵守,就可能以不变应万变!!!

  • 例如在教育孩子的时候不应该过分管控孩子的行为,不如给孩子设立目标或者崇高的理想,由外驱力转化为孩子的内驱力,这样才能达到更好的效果。当然这样的例子还有很多,这里就不举例了,再举例下去就变成哲学了

1.5 迪米特法则

迪米特法则(最少知道原则)(Demeter Principle)

为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立

  • 一个类,对于其他类,要知道的越少越好,其实就是封装的思想,封装内部细节,向外暴露提供功能的接口
  • 只和朋友通讯,朋友是指:
  • 类中的字段
  • 方法的参数
  • 方法的返回值
  • 方法中实例化出来的对象
  • 对象本身
  • 集合中的泛型

我们来看对于类的例子,现在我们有一个电脑类,里面有一些电脑关机时的操作,然后还有一个人类,里面有一个方法为关闭电脑,需要组合电脑类,并执行电脑类里面的方法

class Compute {public void saveData() {System.out.println("正在保存数据");}public void killProcess() {System.out.println("正在关闭程序");}public void closeScreen() {System.out.println("正在关闭屏幕");}public void powerOff() {System.out.println("正在断电");}
}class Person {Compute compute = new Compute();public void shutDownCompute() {compute.saveData();compute.killProcess();compute.closeScreen();compute.powerOff();}
}

这样看上去好像也没什么问题,但是现在变化来了,如果现在关机操作的步骤有几十上百项呢?难道我们要在shutDownCompute方法中去调用上百个方法吗?这里的问题就是:

  • 对于Person类而言,知道Compute中细节太多了
  • 其实不需要知道这么多细节,只要知道关机按钮在哪里就行,不需要知道具体的关机流程
  • 如果使用者在调用方法时的顺序出错,例如把关电和保存数据的顺序弄错,就容易导致问题

所以正确的方法就是尽量高内聚设计,隐藏实现细节,只暴露出单独的接口实现单一的功能

class Compute {private void saveData() {System.out.println("正在保存数据");}private void killProcess() {System.out.println("正在关闭程序");}private void closeScreen() {System.out.println("正在关闭屏幕");}private void powerOff() {System.out.println("正在断电");}public void shutDownCompute() {this.saveData();this.killProcess();this.closeScreen();this.powerOff();}
}class Person {Compute compute = new Compute();public void shutDown() {compute.shutDownCompute();}
}

那么这个封装和暴露的火候该怎么掌握呢?我们接下来看看对于朋友而言的最少知道原则

  • 如果对于作为返回类型、方法参数、成员属性、局部变量的类,不需要过多的封装,应该提供应有的细节,由调用者自己弄清楚细节并承担异常的后果,这样由我们直接创造的对象,我们就能把它称为我们的朋友

  • 但是如果这个对象不是我们自己获得的,而是由被人提供的,就不是朋友,即朋友的朋友并不是自己的朋友

    public class AppTest {public void func() {AppBean appBean = BeanFactory.getAppBean();// 朋友的朋友就不是朋友了appBean.getStr();}}class BeanFactory {public static AppBean getAppBean() {return new AppBean();}
    }class AppBean {public String getStr() {return "";}
    }
    

    那么想要和这个AppBean做朋友该怎么办呢?需要在系统里面造出许多小方法,将朋友的朋友变成自己的朋友,例如:

    public class AppTest {public void func() {AppBean appBean = BeanFactory.getAppBean();// 朋友的朋友就不是朋友了this.getStr(appBean);}/* 将朋友的朋友的细节转换为自己熟悉的方法 */public String getStr(AppBean appBean){return appBean.getStr();}
    }
    

    有的同学可能觉得有点鸡肋这样,确实迪米特法则的缺点就是会制造出很多小方法,让代码结构混乱,所以有的时候适当违反一下也是可以的,但是封装和暴露的思想我们一定要有,后面我们的门面模式中介者模式其实也是基于迪米特法则的,读者先不要急,看到后面再回顾这一段,相信会有跟好的理解

1.6 里式替换原则

里氏代换原则(Liskov Substitution Principle)

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一,里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。

  • 简单的来讲,任何能够用父类实现的地方,都应该可以使用其子类进行透明的替换。替换就是子类对象替换父类对象
  • 子类对象替换父类后,不会有任何影响
  • 是否有is-a的关系
  • 有is-a关系后,要考虑子类替换父类后会不会出现逻辑变化

这里我们来看一下方法重写的定义:

  • 方法重写是指:在子类和父类中,出现了返回类型相同、方法名相同、方法参数相同的方法时,构成方法重写
  • 子类重写父类时,子类的访问修饰符不能比父类更加严格
  • 子类重写父类时,不能比父类抛出更多的异常

如果我们故意在子类中抛出比父类更多的异常会怎么样呢?

如果没有比父类更多的异常,父类现在在执行方法时就会进行catch,并且能够捕获子类中更少的异常,所以这样进行替换时,就不会影响代码的结构,做到透明、无感知

有很多的例子都可以用里式替换进行解释,著名的例子有:

  • 鸵鸟非鸟问题
  • 长方形正方形问题

1.7 组合优于继承原则

组合优于继承原则(Composite Reuse Principle)

组合优于继承原则强调的是在复用时要尽量使用关联关系,少用继承

  • 组合,是一种强关联关系,整体对象和局部对象的生命周期是一样的,类似于大雁和翅膀的关系
  • 整体对象负责局部对象的生命周期
  • 局部对象不能被其他对象共享;
  • 如果整体对象被销毁或破坏,那么局部对象也一定会被销毁或破坏
  • 聚和,它是一种弱关联是 【整体和局部】之间的关系,且局部可以脱离整体独立存在,类似于雁群和其中一只大雁的关系
  • 代表局部的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,甚至代表局部的对象的生命周期可以超越整体
  • 总而言之,组合是值的关联(Aggregation by Value),而聚合是引用的关联(Aggregation by Reference)

我们在之前又讲过,关联关系有两种,实心菱形的是组合空心菱形的是聚和,如果不区分就用虚线指向,组合是作为成员变量作为另一个类的引用,聚和是作为形参或者局部变量作为另一个类的引用

组合大家在平时编码的时候一定经常使用,举一个简单的例子,如果我们现在要有链表实现队列应该怎么做呢?队列的特点就是先进先出,完全可以用链表实现,我们可以用继承关系来做:

public class Queue <E> extends LinkedList<E> {/*** 入队*/public void enQueue(E element){this.add(element);}/*** 出队*/public E deQueue(){return this.remove(0);}}

我们发现这样并没有什么问题,队列类继承自链表类,并暴露自己提供给外界的方法,但是当我们调用这个Queue时就会发现问题:

好家伙,我的Queue本来只需要入队和出队两个方法,但是居然有这么多细节的方法供我使用,这就违背了迪米特法则,一个类的内部实现应该不要提供给外界,只暴露该提供的方法,这就是继承的问题,继承复用破坏包装,因为继承将基类的实现都暴露给派生类

如果我们换成组合该怎么做呢?

public class Queue<E> {// 成员变量 -> 组合关系LinkedList<E> list = new LinkedList<>();/*** 入队*/public void enQueue(E element) {list.add(element);}/*** 出队*/public E deQueue() {return list.remove(0);}
}

所以如果我们仅仅只是为了复用代码,可以优先考虑组合,如果是为了实现多态,可以优先继承

我们也来看一个反例叭,其实在Java中有很多不合理的设计,例如Serializable接口,Date类等等,这里就讲一个java.util.Stack的糟糕设计

点进源码中看我们发现,原来是继承了Vector类,让其拥有了链表的能力,看着这个兄弟设计模式也没学好

官方也意思到了这个设计不合理的地方,推荐我们使用Deque来实现栈

1.8 设计原则总结

其实我们看完了这些设计原则,就会发现其实都是为了应对不断变化的,在看一些源码中,例如Spring的源码、dubbo的源码、netty的源码中也是非常严谨的遵守这些开发规范的

2. 创建型模式

接下来我们来看看设计原则的最佳实践

设计模式的类型

根据设计模式的参考书 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 中所提到的,总共有 23 种设计模式。这些模式可以分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns)、行为型模式(Behavioral Patterns)。当然,我们还会讨论另一类设计模式:J2EE 设计模式。

序号 模式 & 描述 包括
1 创建型模式 这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。 工厂模式(Factory Pattern)抽象工厂模式(Abstract Factory Pattern)单例模式(Singleton Pattern)建造者模式(Builder Pattern)原型模式(Prototype Pattern)
2 结构型模式 这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。 适配器模式(Adapter Pattern)桥接模式(Bridge Pattern)过滤器模式(Filter、Criteria Pattern)组合模式(Composite Pattern)装饰器模式(Decorator Pattern)外观模式(Facade Pattern)享元模式(Flyweight Pattern)代理模式(Proxy Pattern)
3 行为型模式 这些设计模式特别关注对象之间的通信。 责任链模式(Chain of Responsibility Pattern)命令模式(Command Pattern)解释器模式(Interpreter Pattern)迭代器模式(Iterator Pattern)中介者模式(Mediator Pattern)备忘录模式(Memento Pattern)观察者模式(Observer Pattern)状态模式(State Pattern)空对象模式(Null Object Pattern)策略模式(Strategy Pattern)模板模式(Template Pattern)访问者模式(Visitor Pattern)
4 J2EE 模式 这些设计模式特别关注表示层。这些模式是由 Sun Java Center 鉴定的。 MVC 模式(MVC Pattern)业务代表模式(Business Delegate Pattern)组合实体模式(Composite Entity Pattern)数据访问对象模式(Data Access Object Pattern)前端控制器模式(Front Controller Pattern)拦截过滤器模式(Intercepting Filter Pattern)服务定位器模式(Service Locator Pattern)传输对象模式(Transfer Object Pattern)

2.1 工厂设计模式

工厂设计模式属于创建型模式,创建型模式的核心是隐藏细节,并创建实例,我们看工厂模式中的模式都是能够向外提供类的实例对象的

其中工厂设计模式分为三种:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

在讲工厂设计模式之前,我们先来看看一个合理的运用设计模式设计出来的软件架构应该是怎么样的,这也是面向接口编程的设计方式

我们在面向接口编程中,模块和模块之间不能直接调用具体实现类,而是调用模块提供的接口,我们仔细想想在平时的编码中,有直接service层调用dao层的xxxDaoImpl的吗?显然没有,如果直接调用了也差不多该换下一份工作了,一般都是注入接口,让容器注入一个实现类

工厂模式的核心也是隐藏内部实现,对外暴露接口实现具体逻辑,我们来看看工厂模式中的几个重要概念:

  • 产品:就是具体的产品,例如下面代码中的Hamburger
  • 抽象产品:产品的抽象,例如Food
  • 产品簇:这个概念在抽象工厂中在场景中再解释
  • 产品等级:同上

我们来看一个简单的例子,来理解上面的几个概念,另外在学习设计模式的时候一定要清醒的认识到,为什么要有设计模式,为了应对变化,以不变应万变,这里还有两个概念,就是我们习惯于把功能提供者称为作者,把我们这些API调用工厂师叫做用户

// 作者做的抽象产品
interface Food{void eat();
}
// 作者做的具体产品
class Hamburger implements Food{@Overridepublic void eat() {System.out.println("吃汉堡包...");}
}// 用户的业务逻辑
public class AppTest {public static void main(String[] args) {Food food = new hamburger();food.eat();}
}

本来没有什么问题的,但是我一开始就提过,我们要应对这个不断变化的世界,现在变化来了,由于某些原因,作者把原来提供的类名改了,例如把hamburger改为hamburger2,这时候怎么办,是不是我们也要跟着改!!!

跟我说,什么叫耦合?一个改了另一个也要改,这就叫耦合;什么叫解耦?一个改了另一个不需要改,这就叫解耦!

有人说怎么可能,作者好好地没事会去该类名,闲得慌??你别说,笔者在项目中还经常遇到一些作者瞎改类名又不提示用户的,hutool包是我们常用的工具包,但是有一些类名在包升级的时候直接修改,而且完全没有做兼容处理,有一次团队项目打包失败了,最后定位问题发现,这个作者之前类名单词拼写错误了,然后升级版本的时候直接改了类名,导致我们升级版本导入一个不存在的类,导致项目出现问题,记得当时我们都说这个作者是个坑爹玩意,一看就是设计模式没学好

读者可以看看这个作者违反了什么原则?违反了开闭原则,正确的做法应该是给错误的类加过时的标记,重新写一个

我们来总结一下:

  • 这样的设计非常的脆弱,为什么呢?只要作者修改了具体产品的类名,那么客户端就要一起修改,这样的代码就是耦合的
  • 我们希望的效果是,无论服务端客户端如何修改,客户端代码都应该无感知,而不用去进行修改

那么我们如何进行改进呢?针对服务端代码一旦修改,客户端代码也要修改的问题,我们直接使用简单工厂设计模式

2.1.1 简单工厂模式

工厂模式(Factory Pattern)

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。它的本质就是通过传入不同的参数达到多态的目的

优点:

  1. 一个调用者想创建一个对象,只要知道其名称就可以了。
  2. 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
  3. 屏蔽产品的具体实现,调用者只关心产品的接口。

缺点:

  1. 用户需要去记忆具体产品和常量之间的映射关系,例如:FoodNumberEnum.HAMBURGER -> hamburger
  2. 如果具体产品非常多,在简单工厂里面映射关系会非常多
  3. 最重要的是,当用户需要拓展新产品时,就需要在作者的工厂源码中添加映射关系,违反开闭原则

首先我们要弄清楚一个问题,简单工厂设计模式是谁进行具体编码的?是我们(用户)去写吗?还是作者去写,其实是应该由作者去写

假设我们现在是这个作者,我们发现之前的设计确实不太合理,现在要进行优化,我们就可以这样写:

// 作者做的抽象产品
interface Food {void eat();
}// 作者做的具体产品
class hamburger implements Food {@Overridepublic void eat() {System.out.println("吃汉堡包...");}
}class hamburger2 implements Food {@Overridepublic void eat() {System.out.println("这是作者修改后的汉堡包...");}
}class FoodFactory {public enum FoodNumberEnum {HAMBURGER(1001, "汉堡包"),HAMBURGER2(1002, "修改后的汉堡包"),;private final Integer foodNumber;private final String describe;FoodNumberEnum(Integer foodNumber, String describe) {this.foodNumber = foodNumber;this.describe = describe;}public Integer getFoodNumber() {return foodNumber;}public String getDescribe() {return describe;}public static FoodNumberEnum getByType(int type) {for (FoodNumberEnum constants : values()) {if (constants.getFoodNumber() == type) {return constants;}}return null;}}public static Food getFood(FoodNumberEnum foodNumberEnum) {Food food = null;switch (FoodNumberEnum.getByType(foodNumberEnum.getFoodNumber())) {case HAMBURGER :food = new hamburger();break;case HAMBURGER2 :food = new hamburger2();break;default:break;}return food;}
}// 用户的业务逻辑
public class AppTest {public static void main(String[] args) {Food food = FoodFactory.getFood(FoodFactory.FoodNumberEnum.HAMBURGER);food.eat(); // 输出吃汉堡包...}
}

我们来看这样做的好处是什么:

  • 之前是我们直接创建对象,依赖于作者的代码;现在是依赖于工厂,由工厂创建对象
  • 如果作者对原来的实现类做出了修改,也必须修改工厂里面的代码,注意,这里是作者进行修改,而不是用户修改,这样就做到了依赖倒置,完成了解耦,这样用户代码不用做出任何修改
  • 之前由于作者修改代码导致用户也要修改其实还违背了迪米特法则,因为我们被迫去了解了作者的实现,其实我们是不关心如何实现的,我们只需要一个接口实现我们想要的功能即可!!!

可能有又有杠精要问了,要是作者把枚举也改了怎么办?这不是还是要改客户端代吗,我我我???直接好家伙

请杠精看看Spring中的工厂模式是怎么做的,后面笔者也会分析源码,我们在Spring中,不是一直写这样的代码吗???

@Component
public class XXX {@Autowired(required = false)private XXXBean xxxBean;
}

请问,这样做不管实现类怎么修改,只要注入IOC容器,我难道不能直接注入接口中吗?这就叫解耦,面向接口编程

我们总结一下简单工厂的优点:

  • 把具体产品的类名,从客户端代码中解构出来了,服务端如果修改了服务端类名,客户端也不知道
  • 这便符合了面向接口编程的思想,这里注意,这里的接口并不特指interface,而是指下层给上层暴露的东西,可以是一个方法、一个接口、甚至是一个工厂,并且这些接口必须稳定,绝对不能改变

**那么缺点呢?**好像也没啥缺点感觉,又解耦了,又隐藏细节了,这里又不得不提一直提到的字了,学习设计模式,我们要将字贯穿整个学习过程

  • 客户端不得不死记硬背那些枚举类和产品之间的映射关系,比如FoodNumberEnum.HAMBURGER -> hamburger
  • 如果有成千上万个产品,那么简单工厂就会变得十分的臃肿,造成映射关系爆炸
  • 最重要的是如果变化来了,如果客户端需要拓展产品,首先我们不能改源代码(违反开闭原则),我们只能搞一个实现类实现自己的逻辑,但是工厂中又没有映射关系让我们创建这个实例,我们又得去修改工厂的源码,又违背了开闭原则,同时最重要的是,你这是去改别人提供的jar包呀我的天,你觉得你能看到被人的源代码吗?你能修改吗?????显然不能,那怎么解决呢?这就引出第二个工厂模式:工厂方法设计模式
  • 有的同学可能会觉得自己拓展的类,自己new一个不就好了,还改什么源代码?讲的有道理,但是如果你也是作者呢?你写的拓展是要给别人使用的呢?难道让读者去new一个你的实现类,这不违背迪米特法则了吗?用户只想要一个具体的实现,你现在要让用户去找你的实现类,这合理吗?这不合理,我们直接看工厂方法设计模式是怎么解决的

最后来画一下类图,我们学设计模式,一定要能熟练画出类图

总结:

2.1.2 工厂方法模式

我们回顾一下上面简单工厂产生的问题,就是简单工厂只能提供作者提供好的产品,我们无法对产品进行拓展

工厂方法

定义一个用于创建产品的接口,由子类决定生产什么产品

  • 坏处: 加一个类,需要加一个工厂,类的个数成倍增加,增加维护成本,增加系统抽象性和理解难度
  • 好处: 符合开闭原则

总结:简单工厂模式 + 开闭原则 = 工厂方法

我们来改造一下上面有问题的代码,我们现在不将工厂写死,而是面向抽象编程,将工厂定义为一个接口,在作者的代码中提供一些基本的实现,例如创建HamburgerRichNoodle的实现

// 作者做的抽象产品
interface Food {void eat();
}// 作者做的具体产品
class Hamburger implements Food {@Overridepublic void eat() {System.out.println("吃汉堡包...");}
}class RichNoodle implements Food {@Overridepublic void eat() {System.out.println("过桥米线");}
}
/** 定义工厂的接口 **/
interface FoodFactory {Food getFood();
}class HamburgerFactory implements FoodFactory {@Overridepublic Food getFood() {return new Hamburger();}
}class RichNoodleFactory implements FoodFactory {@Overridepublic Food getFood() {return new RichNoodle();}
}public class App {public static void main(String[] args) {// 拿到产生产品的工厂FoodFactory foodFactory = new HamburgerFactory();// 创建对应的产品Food food = foodFactory.getFood();food.eat();}
}

我们会发现我们将工厂作为接口暴露之后就有一个好处,如果我们想要新增加一个产品,我们不需要去修改原来的产品,而是通过继承Food创建产品类,再通过暴露的工厂接口创建工厂,再来实例化我们需要的产品,具体实现为:

/** 新的产品 **/
class PorkFeetRice implements Food{@Overridepublic void eat() {System.out.println("吃猪角饭...");}
}
/** 生产猪角饭的工厂 **/
class PorkFeetRiceFactory implements FoodFactory{@Overridepublic Food getFood() {return new PorkFeetRice();}
}public class App {public static void main(String[] args) {// 拿到产生产品的工厂FoodFactory foodFactory = new PorkFeetRiceFactory();Food food = foodFactory.getFood();food.eat();}
}

我们可以看到,我们通过提供的工厂接口,并没有修改之前的工厂逻辑,又进行了拓展,并且新的产品和工厂实现类都是我们自己创建的,符合开闭原则

我们来总结一下工厂方法模式的优点

  • 仍然具有简单工厂的优点,服务端修改了生产产品的逻辑时,用户端无感知
  • 因为产品和工厂都是拓展出来的,所以不需要修改原来的代码,只需要创建一个新的产品和工厂即可

但是我们也有发现这样好像怪怪的,感觉就是暴露了个接口而已,读者可能会有以下的疑问

  • 虽然说好像不管是简单工厂也好,工厂方法也罢,虽然都做到了和具体实现解耦,用户不用关注实现是否发生了改变,但是,反观我们现在的代码,好像还在依赖于具体的工厂,说的就是上面的PorkFeetRiceFactory,如果我们每生产一种产品,都要去知道这种产品对应的工厂,这又违反了迪米特法则,并且如果如果作者如果把工厂名字写错了,又会出现上面的问题
  • 感觉折腾了一圈,又回到了原点,之前是依赖于具体的实现,现在是依赖具体的工厂,还是耦合的关系

我们来解释一下上面的两个问题:

  • 首先,既然作者已经对外暴露了接口,那么作者有义务保存接口的稳定,不能出现改接口名的行为(或弃用)
  • 其次工厂模式还可以隐藏一个实例创建的过程,学过spring等框架的同学就会知道,在框架中一个实例的创建并不只是简简单单new那么简单,可能还会牵涉到容器生命周期以及一些解析bean的操作,显然会复杂很多,所以工厂模式还帮我们隐藏了细节、封装了代码,至于具体要使用那个工厂,是我们应该去了解的,毕竟每个工厂提供产品都会不同
  • 很多问题现在简单的业务下并不是问题,但是请不要忘记这个字,我们的代码为什么要分一层又一层,就是为了能够在之后改变后的业务场景依旧能够使用,如果读者还有问题,请复习复习Spring IOC容器的设计,如果没有工厂,IOC容器如果做到解析那么多注解,如何完成依赖注入,如果完成各种Spring预留的拓展接口,如果完成Bean的声明周期

总结一下缺点

  • 每一个层级的产品的产品都需要对应一个产品,不仅增加了编码的负担,还可能产生类爆炸的现象

我们现在来画一下工厂方法模式的UML类图

最后总结一下

2.1.3 抽象工厂

抽象工厂模式

抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象

优点:解决了工厂方法模式中类爆炸的问题,同时拥有其可拓展性的优点

缺点:产品族扩展非常困难,要增加或删除一个系列的某一产品,既要在抽象的工厂里加代码,又要在具体的里面产品加代码,违背开闭原则

我们来后顾一下前面的两种工厂模式的缺点

  • 简单工厂:耦合与具体的工厂实现,没有良好的拓展性
  • 工厂方法:会产生类爆炸的问题,每一个产品类都需要对应的工厂,代码臃肿

我们现在要通过工厂方法设计量类产品的生产,分别是食物Food饮料Drink,其中食物有三种,饮料有两种,我们会发现食物需要定义一个接口,三个具体产品类,一个抽象工厂类,三个具体工厂类;饮料需要定义一个接口,两个具体产品类,一个抽象工厂类,两个具体工厂类,一共14个类,我就想要产生五种类,好家伙,按照工厂方法的写法,直接写出14个类了

/******************  抽象产品   *********************/
// 食物抽象类
interface Food {void eat();
}
// 饮料抽象类
interface Drink{void drink();
}
/******************  具体产品   *********************/
// 食物的具体产品
class Hamburger implements Food {@Overridepublic void eat() {System.out.println("吃汉堡包...");}
}class RichNoodle implements Food {@Overridepublic void eat() {System.out.println("过桥米线");}
}class Cola implements Drink{@Overridepublic void drink() {System.out.println("喝可口可乐...");}
}class IcePeak implements Drink{@Overridepublic void drink() {System.out.println("喝冰峰...");}
}/******************  抽象工厂类   *********************/
interface FoodFactory {Food getFood();
}interface DrinkFactory{Drink getDrink();
}/******************  实例工厂类   *********************/class HamburgerFactory implements FoodFactory {@Overridepublic Food getFood() {return new Hamburger();}
}class RichNoodleFactory implements FoodFactory {@Overridepublic Food getFood() {return new RichNoodle();}
}class ColaFactory implements DrinkFactory{@Overridepublic Drink getDrink() {return new Cola();}
}class IcePeakFactory implements DrinkFactory{@Overridepublic Drink getDrink() {return new IcePeak();}
}/*************** 新拓展的产品和工厂 **********************/
class PorkFeetRice implements Food{@Overridepublic void eat() {System.out.println("吃猪角饭...");}
}/** 生产猪角饭的工厂 **/
class PorkFeetRiceFactory implements FoodFactory{@Overridepublic Food getFood() {return new PorkFeetRice();}
}public class AbstractApp {public static void main(String[] args) {// 拿到产生产品的工厂FoodFactory foodFactory = new PorkFeetRiceFactory();Food food = foodFactory.getFood();food.eat();}
}

上面的代码就是工厂设计模式,其实工厂设计模式到抽象工厂只有一步之遥,我们会发现上面导致类爆炸的原因就在于太多工厂类的接口了,那我们就将工厂类的接口再进行抽象,例如食物和饮料的工厂接口统一进行抽象,再将各自的实例工厂也进行合并

/******************  抽象工厂类   *********************/
interface AbstractFactory {Food getFood();Drink getDrink();
}/******************  实例工厂类   *********************/class KFCFactory implements AbstractFactory {@Overridepublic Food getFood() {return new Hamburger();}@Overridepublic Drink getDrink() {return new Cola();}
}

显然这样通过抽象 + 组合的方式,这样就可以减少一些类的产生

这样做的优点是就是在拥有工厂方法模式的优点下可以有效减少类的产生

缺点是好像在一个工厂类绑定死了具体的产品,例如KFCFactory -> Hamburger + Cola,我们为什么在这个工厂生成这两种产品

现在我们补充一下上面没有提到的两个概念:

  • 产品簇:多个有内在联系或有逻辑关系的产品,例如上面的KFC套餐就固定为Hamburger + Cola,这里就组成一个产品簇

  • 产品等级:其实要弄清楚产品的等级我们可以看下面的图,产品等级就是指由一个接口或者父类泛化的子类,例如各种饮料、各种电冰箱,而产品簇就是指由一个工厂生产的产品,看下图中美的生产的产品,这些就组成一个产品簇

我们可以看到如果要生产上面的25个具体产品,一共需要多少个抽象产品,多少个抽象工厂?都是五个;如果是工厂方法则需要25个类,可以看到通过不断组合的方式,可以大大减少抽象产品类和抽象工厂类的创建

设计模式学习(汇总版)相关推荐

  1. 设计模式学习笔记汇总目录

    这里的学习笔记包含JavaSE和J2EE两部分,持续更新中! 其中关于学习的参考资料如下: 1.菜鸟设计模式 2.Head First Design Patterns(书.强烈推荐); 3.大话设计模 ...

  2. Java学习第七天 ———— 第一周学习汇总(粗略)

    Java学习第七天 ---- 第一周学习汇总 第一章Java Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承.指针等概念,因此Java语言具有功能强大 ...

  3. Java方向如何准备BAT技术面试答案(汇总版)

    转自:http://www.jianshu.com/p/1f1d3193d9e3 原文链接:Java方向如何准备BAT技术面试答案(汇总版) 这个主题的内容之前分三个篇幅分享过,导致网络上传播的比较分 ...

  4. 【Python】《大话设计模式》Python版代码实现

    <大话设计模式>Python版代码实现 上一周把<大话设计模式>看完了,对面向对象技术有了新的理解,对于一个在C下写代码比较多.偶尔会用到一些脚本语言写脚本的人来说,很是开阔眼 ...

  5. 大数据面试题及答案 汇总版

    版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/albg_boy/article/det ...

  6. 设计模式学习笔记——抽象工厂(Abstract Factory)模式

    设计模式学习笔记--抽象工厂(Abstract Factory)模式 @(设计模式)[设计模式, 设计模式中文名, 设计模式英文名] 设计模式学习笔记抽象工厂Abstract Factory模式 基本 ...

  7. 设计模式——23种设计模式学习总结

    声明:本文为个人笔记,用于学习研究使用非商用,内容为个人研究及综合整理所得,若有违规,请联系,违规必改. 系列文章目录 简单工厂模式 策略模式模式 装饰模式(进行中) 代理模式(进行中) 工厂方法模式 ...

  8. 设计模式学习资料推荐

    设计模式学习资料推荐 学习任何东西都应该去纵向比较,同一种概念从不同的书本看到不同的思路见解,也可以更好的帮助自己理解.重要的是自己要有一个理解思考的过程. 第一部分:网址类 RUNOOB.COM 语 ...

  9. HR64个经典面试问题【汇总版】

    HR64个经典面试问题[汇总版] 简历是敲门砖,而面试则是检验这块砖是否结实有用的最直接途径.应聘时的表现很大程度上决定你是否被录用,让HR告诉你,面试一般会问的问题有哪些?如何回答才能获得一份理想的 ...

  10. 《大话设计模式》Python版代码实现

    <大话设计模式>Python版代码实现 上一周把<大话设计模式>看完了,对面向对象技术有了新的理解,对于一个在C下写代码比较多.偶尔会用到一些脚本语言写脚本的人来说,很是开阔眼 ...

最新文章

  1. function在php中,function
  2. R语言使用ggplot2包使用geom_violin函数绘制分组小提琴图(配置边界颜色)实战
  3. 【转】什么是“对用户友好”
  4. 高性能NIO框架Netty入门篇
  5. 在DataGridView控件中加入ComboBox下拉列表框的实现
  6. Java黑皮书课后题第6章:*6.15(金融应用:打印税表)程序清单3-5给出了计算税款的程序。使用下面的方法体编写一个计算税款的方法。使用这个方法编写程序,打印可征税人从50000到60000间隔
  7. 更优雅的在 Xunit 中使用依赖注入
  8. 对某公司一次弱口令到存储型xss挖掘
  9. 【Elasticsearch】 elasticsearch中 rollover 的用法
  10. Android 自定义View自定义属性的声明
  11. 细说嵌入式Linux文件系统的制作方法
  12. 【渝粤教育】电大中专电商运营实操 (12)作业 题库
  13. 【转】OpenGL Related Development ToolKits
  14. 可计算代数数论(2012-12-09 20:56、2013-03-23 21:39、2013-06-23 20:27、2013-06-23 20:32、2014-05-16 17:49)
  15. java矩阵连乘动态规划_动态规划之矩阵连乘
  16. 与老公的情人同居一室很尴尬
  17. 拯救者R9000p搜不到wifi 解决方法
  18. 设计模式篇:状态模式(一)
  19. 【C/S通信交互之Socket篇】Cocos2dx(Client)使用BSD Socket与Mina(Server)手机网游通信框架!
  20. FileInclude

热门文章

  1. Linux系统安装Anaconda3保姆级教程
  2. 解决C#读取文本文件乱码
  3. [ISP]AT89S52的ISP功能简介
  4. 用Java(APICloud)开发手机APP
  5. 超算对我们的生活有着什么样的影响?
  6. 科目二考试之倒车入库步骤
  7. C语言课设分享:图片加载
  8. Kafka 精妙的高性能设计(上篇)
  9. 基于QT实现的可视化链表(单链表、循环链表、双向链表)
  10. Linux操作系统搭建srs服务器