[toc]
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了提升可重用代码、代码的可读性以及代码的可靠性。在项目中合理运用设计模式可以近乎完美的解决很多问题,每种设计模式在现实中有与之对应的案例,自然每种设计模式都象征着我们实际生活中不断发生的种种事务的抽离。
概念
单一职责原则,顾名思义就是每一个模块负责每一项职能,对应面向对象的设计原理,具体到一个类只负责某一个职能。指实现了高内聚、低耦合的指导方针。
案例
原本A类负责两项职能(P1职能和P2职能),那么问题来了,如果我们为了修改P1职能的时候,涉及了A类的修改,就会影响到了P2职能的正常运行。
因此为了解决此类问题,我们就只能通过拆分A类的职能,具体拆分为了A1-->P1,A2--->P2来解决问题。
概念
一个软件实体应当对外扩展能力开放,对内修改能力关闭。简单来说就是,软件实体应尽量在不修改原有代码的情况进行扩展。
案例
假设某一个A类提供了show() 方法,用于展示该类的A1结构,具体到开闭原则,现在我们有一个业务需求,需要对A进行展示内部A2结构,这时候show()明显就泵满足业务场景,遵循以上原则,我们可以提供两种方案,第一种就是修改show()方法,通过形参控制来实现展示不同的内容,第二种就是通过扩展专门展示A2结构的方法。很明显两种方法,第二种是最适用的,但是经常我们说的开闭原则,用于方法扩展,也要对原来的方法进行抽离公共方法来避免代码冗余。
概念
所有引用基类(父类)的地方必须能透明地使用其子类的对象。这句话什么意思呢?简单来说就是开闭原则的体现,意思就是子类可以扩展父类的功能,但不能改变父类原有的功能。它包含4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(形参列表)要比父类方法的输入参数更为宽松
- 当子类的方法实现父类的抽象方法时,方法的后置条件(返回值)要比父类更加严格
再回过头来看待里氏替换原则就不难发现,它在强调三个我们需要注意的地方:
- 子类的所有方法必须在父类中声明,或者子类必须实现父类中声明的所有方法,这是保证系统的可扩展性根本前提,如果我们仅仅实在子类中定义一个自定义的方法,并未在父类中声明,那么则无法在以父类的名义去使用该方法。
public class A implements B {
@Override
public void add(String name) {
// TODO some things
}
public void addPerson(String name) {
// 父类B如果被实例化,是无法以父类对象去引用此方法的
}
}
- 通常我们会尽量将父类定位抽象类或者接口,让子类继承或者实现父类接口,并且实现在父类中声明的方法,实例化(Father father = new Son()),这样出现了新的业务需求,我们只要扩展父类的接口,在子类中实现父类扩展的方法即可。
概念
抽象不应该依赖于细节,细节应当依赖于抽象。官方解释太过抽象,我们可以理解为,老板不应该直接和基层员工沟通,而是将通知下达给各级部门的主管,再由各级部门的主管下达给各个小组的组长,再由组长下达给各个基层员工。
概念
使用多个隔离的接口,比使用单个接口要好。该原则还是在强调低依赖、低耦合的理念,从官方的理解为,每个接口都应该执行属于自己那一份职能,不属于自己的职能应该不参与,实际上在现实中开发,我们可以发现,实现接口隔离原则很简单,但是随之问题也会出现很多,比如屁大点事就会扩展一个接口,这样就会造成接口冗余,这时候我们更应该考虑的时候,接口尽可能的大,但是也不能大到干了所有事情,尽可能小,不能小到什么事情都要细分。
概念
原则是尽量使用合成/聚合的方式,而不是使用继承。
概念
一个软件实体应当尽可能少地与其他实体发生相互作用。就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
- 创建型模式-用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
- 结构型模式-用于描述如何将类或对象按某种布局组成更多的结构。
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
- 行为型模式-用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。
策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
某一个类只能生成一个实例,该类提供了一个全局访问点来用于外部获取该实例。单例模式的实现方式有五种方法:懒汉,饿汉,双重校验锁,静态内部类。
单例模式三个特点:
- 单例类确保自己只有一个实例
- 单例类必须自己创建自己的实例
- 单例类必须为其他对象提供唯一的实例
单例模式三个优点:
- 频繁创建类会给系统造成很大的消耗
- 频繁使用new关键词,降低了系统内存的使用频率,减轻GC压力
- 单例类独立实例化,独立控制整个系统流程
单例模式三个缺点:
- 没有抽象层,不能扩展
- 职责过重,违背了单一性原则
懒汉模式
public class Singleton {
// 私有静态实例,防止直接被引用
private static Singleton singleton = null;
// 私有无参构造方法
private Singleton() {}
// 静态工厂,也就是提供对外唯一访问点
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这就是最简单的单例模式实现,其实也不能看出,多线程情况下,A,B同时进入访问点的时候会出现什么问题呢?这个实例是不是被实例化了两遍!那么没我们延申一下,在线程安全情况下,我该如果解决呢?
// 第一种对getInstance() 方法块进行加锁
public static synchronized Singleton getInstance() {.....}
// 第二种对getInstance() 方法中引用的singleton进行加锁
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton) {
singleton = new Singleton();
}
}
return singleton;
}
两种方法的比较相对来说,后者更优于前者,可以这么理解,前者没调用一次getInstance() 都是上锁一次,性能和效率就会降低很多,但是后者就不同了,只会在singleton没有被实例化的才会出现上锁的情况。但是两者还是有先后顺序的差别的,虽然最优,但不一定最好。举一个比较简单的例子:
第一种方法中:A,B两个线程同时到达getInstance(),比如A先拿到了锁,那么B就会等待,假设A现在突然说我不创建了,那么B依旧会拿到锁,然后继续执行,并且使用singleton中的方法等等。看似效率低下,实则帮我们阻挡一个很大的危险。
第二种方法中:A,B两个线程同时到达getInstance(),A先进入了,并且在锁住了singleton这个对象,B在外面徘徊等待中,假设A突然又不创建了,但是已经执行了new Singleton() 就相当于JVM给它分配了内存空间了,只是没有实例化,那么就会给B造成误判,B得知已经实例化了,就会直接去调用singleton中的方法,这这时候就是空指针了。
饿汉模式
相对于懒汉式中几个问题,饿汉式可以起到一定的规避作用,但是没办法进行完美的解决。饿汉式就是在全局加载的同时实际上Singleton已经被实例化了,只是属于私有属性,外部无法调用。
public class Singleton {
// 这里会在全局加载的时候就会被实例化
private static Singleton singleton = new Singleton();
// w无参构造
private Singleton() {}
// 初始化
public static Singleton getInstance() {
return singleton;
}
}
此种方法,是基于JVM中classloader机制避免了懒汉式的多线程同步问题,不过也会造成其他问题,就比如,getInstance() 就形同虚设了,违背了单例自己创建自己的原则不说,单例在加载的时候如果仅仅是实例化还好,伴随着赋值过程就是耗费系统资源。
静态内部类模式
前面我们是围绕一个主题:线程安全问题来描述单例模式在实际运用中可能存在的问题和规避方案。实际上,静态内部类就是借用了JVM加载机制和JVM线程安全总控机制来解决单例可能存在线程安全问题。单例是静态的final变量的时候,当类在全文加载的时候只会被加载一次,而且由JVM对其进行管理。
public class Singleton {
private static final class SingletonLoader {
static final Singleton singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonLoader.singleton;
}
}
相比饿汉模式中,我们使用静态来进行全局加载的情况,这种静态内部类的形式,其他类在引用Singleton的时候,只是新建了一个引用,JVM并没有开辟一个堆空间,而是当我们引用getInstance方法时候,根据JLS规则JVM回去加载一次SingletonLoader.class并且实例化一个Singleton对象。它的缺点也是很明显的,就是JVM在加载class文件时候会给予一个文件存放位置(JPG 永久代内存) ,也是耗费巨大的内存资源。
双重校验锁
其实单例模式23种设计模式种最简单也是最难的模式,简单是因为实现起来非常简单,困难是因为正因为它简单,所以考虑安全的时候必须要充分。我结合网上的案例,大部门人还是倾向于懒汉模式中添加synchronized关键词来实现第二种方案,有时候最好的解决方案就是最实在的方案。
public class Singleton {
public static final Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronize (Singleton.class){
// 这里规避了上述中第二种解决方案中可能会空指针的场景出现
if( singleton == null ) {
singleton = new Singleton();
}
}
return singleton;
}
}
建立一个工厂类,对实现了同一接口的一些类进行实例的创建。
工厂模式分类:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式(细分为一类比较多)
工厂模式的好处:
- 代替new关键词来实例化对象,实现对类的统一管理和控制。
- 降低程序的耦合性,便于后期维护
简单工厂
简单工厂模式又叫做静态工厂方法(Static Factory Method)模式。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。它的流程就是工厂类决定了需要实例化哪种类,换言之,外部只需要得到自己想要的实例化对象,而不需要关系该实例化对象是如何创建完成的。
这里我举个推送消息的案例:推送消息的形式:短信,邮件
// 1、首先我们想要创建一个可以推送消息的方案接口 可以理解为推这个动作
public interface NoticeMessage {
void sendMessage();
}
// 2、定义三种推送通知模式 并且实现推送消息的方法
public class Email implements {
// 邮件形式
@Override
public void sendMessage () {
System.out.println("发送邮件消息。");
}
}
public class SMS implements {
// 短信形式
@Override
public void sendMessage () {
System.out.println("发送短信消息。");
}
}
// 3、定义工厂类 这里就是简单工厂的关键地方 这里外部的唯一访问点
public class MessageFactory {
// type 就是决定了发送那种类型的消息
public NoticeMessage sendMessage (String type) {
NoticeMessage nm = null;
switch (type) {
case "Email":
nm = new Email();
break;
case "SMS":
nm = new SMS();
break;
}
}
}
// 4、测试发送一下
public class Test {
public static void main(String[] args) {
MessageFactory mf = new MessageFactory();
NoticeMessage nm = mf.sendMessage("SMS");
nm.sendMessage();
}
}
工厂方法
相比较于简单工厂,外部已经知道我们有两种发送方式,但是成需要考虑不仅仅是外部知道的情况下,也应该了解外部不知情的。举个简单例子,我们假设短信只能采用手机发送,邮件只能采用电脑发送,也就说,我们仅仅知道有两种发送方式,但不知道通过什么途径去发送。
工厂方法的优点:
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
- 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;
// 1、创建设备类型 手机还是PC
public interface Device {
void send();
}
// 2、创建工厂类 需要获取那种设备类型去发送
public interface DeviceFactory {
Device SelectDevice();
}
// 3、绑定固定设备 手机 或者 PC
public class Phone implements Device {
@Override
void send() {
System.out.println("用手机发送短信");
}
}
public class Computer implements Device {
@Override
void send() {
System.out.println("用PC发送邮件");
}
}
// 4、创建两种设别的对外接口 也就是我们说的具体工厂类
public class PhoneFactory implements DeviceFactory {
@Override
public Device SelectDevice () {
return new Phone();
}
}
public class ComputerFactory implements DeviceFactory {
@Override
public Device SelectDevice () {
return new Computer();
}
}
// 5、测试一下
public class Test {
public static void main(String[] args) {
DeviceFactory phoneDevice = new PhoneFactory();
DeviceFactory computerDevice = new ComputerFactory();
// 手机发送短信
phoneDevice.SelectDevice().send();
// 电脑发送邮件
computerDevice.SelectDevice().send();
}
}
工厂方法中,我们把发送设备也给抽离出来,是不是就更加清晰明朗了,这样外部就拿到了所有的选择权,至于如何创建的流程他们可以不在乎,如果后面还要扩展打电话的功能,首先设备是还是Phone,原来的只有send() 就可直接扩展一个call() 方法,这样工厂模式的工厂方法模式就是最好的理解。
抽象工厂模式是用于创建一组相关或者独立的对象。通过此模式,用户不需要知道具体的对象类型,只需要找到创建的工厂类即可。这样解释可能会被误认为与工厂方法相似,但是两者还是有区别的:
- 工厂方法我们参考上面的案例,一个DeviceFactory对应一个Device,那是不是每创建一个设备就对应一个设备工厂
- 抽象工厂模式它的方式是一个DeviceFactory对应多个Device,也就是我们可以把设备这个概念规整到一起来
简单点描述就是抽象工厂就是最大的工厂(终极工厂模式),工厂里面有许多的DeviceFactory。
具体代码不演示了,实际上工厂模式的初衷就我们学习设计模式的初衷,研发人员无非关注的几个点,效率,性能,安全,维护。工厂模式属于创建型模式中它属于偏重于效率和维护这两个点。
建造者模式是将一个整套流程,根据流程中模块拆分具体的步骤,以此来达到由复杂转向简单,开发者只需要关注其中某一个简单的模块步骤。相比于工厂模式,建造者模式更像是零部件的装配,大家都是同一条流水线上面,只是负责不同零部件装配。
建造者模式的有点:
- 各个模块相互独立,易扩展
- 便于控制细节风险
建造者模式的缺点:
- 产品的类型必须大同小异,属于范围限制
- 如果一个产品的构造过程很多,就会造成一个产品的步骤很冗余
// 模拟一个案例场景:收银机录入商品信息
// 1、建立一个商品信息类
public class Goods {
// 商品名称
private String name;
// 商品价格
private double price;
// 商品规格
private String specifications;
// 这里就不Setter/Getter ,太多了
}
// 2、商品录入接口
public interface GooodsBuilder {
void buildGoodsName();
void buildGoodsPrice();
void buildGoodsSpecifications();
Goods build();
}
// 3、假设两种商品 可乐 和 猪肉
public class CocaCola implements GooodsBuilder {
private Goods goods = new Goods();
@Override
public void buildGoodsName() {
goods.setName("可口可乐");
}
@Override
public void buildGoodsPrice() {
goods.setPrice(3.0);
}
@Override
public void buildGoodsSpecifications() {
goods.setSpecifications("瓶");
}
@Override
public void build() {
return goods;
}
}
public class Pork implements GooodsBuilder {
private Goods goods = new Goods();
@Override
public void buildGoodsName() {
goods.setName("猪肉");
}
@Override
public void buildGoodsPrice() {
goods.setPrice(15.0);
}
@Override
public void buildGoodsSpecifications() {
goods.setSpecifications("斤");
}
@Override
public void build() {
return goods;
}
}
// 4、收银机
public class CashRegister {
private GooodsBuilder gb;
public void setGooodsBuilder(GooodsBuilder gb) {
this.gb = gb;
}
// 开始录入信息
public Goods init() {
this.gb.buildGoodsName();
this.gb.buildGoodsPrice();
this.gb.buildGoodsSpecifications();
return this.gb.build();
}
}
// 5、测试一下
public class Client {
public static void main(String[] args) {
// 收银机开机
CashRegister cashRegister = new CashRegister();
// 首先录入可口可乐
CocaCola cocaCola = new CocaCola();
cashRegister.setGooodsBuilder(cocaCola);
Goods cocaColaGoods = cashRegister.init();
// 录入猪肉
Pork pork = new Pork();
cashRegister.setGooodsBuilder(pork);
Goods porkColaGoods = cashRegister.init();
}
}