设计模式
七大原则
单一责任原则(SRP)
类的责任要单一,不能将过多的职责放在一个类中
换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。
开闭原则(OCP)
开闭原则就是说对扩展开放,对修改关闭。
即在不修改一个软件实体的基础上去拓展其功能。
在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔(是在不关闭系统电源的情况下,将模块、板卡插入或拔出系统而不影响系统正常工作)的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。使用接口和抽象类来实现这种效果。
举个例子:比如我有一个拖拉机的工厂,他是生产拖拉机,现在我要引入一个新的产品 口罩,那我是修改拖拉机生产线让他生产口罩好呢还是直接新建一个口罩工厂好呢,那肯定是新建。这也表达了一个观点,扩展优于修改。
抽象化是开闭原则的关键,
https://blog.csdn.net/zhangerqing/article/details/8194653
https://zhuanlan.zhihu.com/p/239952896
https://www.zhihu.com/question/20367734
里氏代换原则(LSP)
https://www.cnblogs.com/o-andy-o/p/10315188.html
在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象
子类对象必须能够替换掉所有父类对象。
继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。
任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用
因为继承带来的侵入性,增加了耦合性,也降低了代码灵活性,父类修改代码,子类也会受到影响,此时就需要里氏替换原则。
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
依赖倒置原则(DIP)
https://www.jianshu.com/p/c3ce6762257c
针对抽象层编程,而不要针对具体类编程
高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
抽象不应该依赖于细节,细节应该依赖于抽象。
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
依赖于抽象意味着:
- 任何变量都不应该持有一个指向具体类的指针或者引用;
- 任何类都不应该从具体类派生;
- 任何方法都不应该覆写它的任何基类中的已经实现的方法。
接口隔离原则(ISP)
不应该强迫客户依赖于它们不用的方法。
因此使用多个专门的接口比使用单一的总接口要好。
使用多个专门的接口来取代一个统一的接口
合成复用原则(CRP)
http://c.biancheng.net/view/1333.html
在复用功能时,应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系
迪米特法则(LOD)
就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。
一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互
创建型
简单工厂模式
概述
简单工厂把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个具体子类来实例化。
定义一个工厂类,根据不同的参数返回不同的实例,被创建的实例通常具有相同的父类,这也说明了简单工厂模式是创建某一“大类”下面不同类的实例的一种模式。
创建实例的方法通常为静态方法,所以简单工厂模式又被称为静态工厂方法(Static Factory Method)
类图
结构
①Factory:核心部分,负责创建所有产品,可以被外界直接调用。
②Product:抽象产品,工厂类创建的所有实例的父类,封装了产品对象的公共方法。
③ProductA/B:具体产品,工厂类的创建目标
优点
实现对象的创建和使用分离,创建交给工厂类负责,客户端不需要知道怎么创建产品,知道怎么使用就可以了。
缺点
工厂类不够灵活,如果新增一个产品我们就需要修改它的判断逻辑,如果产品数量很多的话,判断逻辑将会变得非常复杂。不符合OCP原则。
工厂类集中了所有产品的创建逻辑,职责过重,一旦发生异常整个系统将受影响。
适用环境
①工厂类创建的对象数量比较少
②客户端只知道传入参数,对如何创建对象不关心
应用:
java中格式化日期的类DateFormat,通过不同的参数getDateInstance来得到不同的对象。
工厂方法模式
概述
定义一个用于创建对象的接口,让子类决定实例化哪个类,工厂方法使一个类的实例化延迟到其子类。
类图
结构
①Factory:抽象工厂,定义创建方法
② FactoryA/B: 具体创建方法
②Product:抽象产品,工厂类创建的所有实例的父类,封装了产品对象的公共方法。
③ProductA/B:具体产品,工厂类的创建目标
优点
克服了简单工厂模式的缺点,在引进新产品时不用修改工厂类,只需添加相应的具体工厂子类对象,很好的符合了开闭原则,方便拓展。
缺点
每增加一个产品相应的就要增加一个子工厂,会加大额外的开发量。
适用环境
客户端不需要知道具体产品类的类名,只需要知道对应的工厂即可,具体的产品的对象由具体的工厂类创建;客户端需要知道创建具体产品的工厂类。
应用:
Collection类中具体的集合类可以通过实现Iterator()方法来返回一个具体的迭代器对象。例如:ListIterator
JDBC也大量用到了工厂模式,例如通过DriverManager来获得Connection,通过connection来获得statemen,而通过statement来获得resultset
BeanFactory来获得Bean实例
抽象工厂模式
概述
提供一个接口,用于创建 相关的对象家族
工厂方法模式只考虑生产同等级的产品,如电器厂只生产电视机,而抽象工厂是考虑多等级产品的生产,如电器厂不止生产电视机,还生产空调。工厂子类里可以生产不同等级的产品
结构
类图
优点
- 一个工厂子类可以管理多级产品,而不用像工厂模式一样去引入多个新的工厂子类
- 当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品族。
- 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。
缺点
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。
适用环境
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
- 系统一次只可能消费其中某一族产品,即同族的产品一起使用。
应用
java中的AWT(抽象窗口工具包)就是用了该模式,实现在不同操作系统中呈现一致的外观界面。
单例模式
概述
确保一个类只有一个实例,并向整个系统提供这个实例。
使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
确保一个类只有一个实例,那么它的构造方法得是private,不能被外界实例化,并且拥有一个当前类的静态成员变量,在类里面用静态方法实例化。
主要作用是确保一个类只有一个实例存在,比如序列号生成器、Web页面计数器等
原则
1.构造私有。(阻止类被通过常规方法实例化)
2.以静态方法或者枚举返回实例。(保证实例的唯一性)
3.确保实例只有一个,尤其是多线程环境。(确保在创建实例时的线程安全)
4.确保反序列化时不会重新构建对象。(在有序列化反序列化的场景下防止单例被莫名破坏,造成未考虑到的后果)
漏洞:
https://www.cnblogs.com/shangxinfeng/p/6754345.html
通过反射来破坏单例模式
通过序列化和反序列化破解单例
避免漏洞(枚举可以避免)
(1)避免反射
反射是通过它的Class对象来调用构造器创建出新的对象,我们只需要在构造器中手动抛出异常,导致程序停止就可以达到目的了,看下面代码
(2)避免序列化
这个方法是基于回调的,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要在创建新的对象!
类图
实例化时机
1、懒汉式,线程不安全
在多线程情况下,多个线程能够同时进入if语句,导致uniqueInstance被实例化多次
public class Singleton1{
//构造方法私有保证不会被外界实例化
private Singleton1(){}
//静态私有保证唯一性
private static Singleton1 uniqueInstance;
public static Singleton1 getInstance(){
if(uniqueInstance==null){
uniqueInstance=new Singleton1();
}
return uniqueInstance;
}
}
2、饿汉式,线程安全
直接实例化避免了线程不安全,但是与此同时丢失了延迟实例化带来的节约资源的好处
public class Singleton2{
private Singleton2(){}
private static Singleton2 uniqueInstance=new Singleton2();
public static Singleton2 getUniqueInstance(){
return uniqueInstance;
}
}
3、懒汉式,线程安全
加锁,避免被实例化多次,线程安全。但是因为锁的是整个方法,所以即使实例化已经结束,也会有线程在阻塞等待,浪费性能、
public class Singleton3{
private Singleton3(){}
private static Singleton3 uniqueInstance;
public static synchronized Singleton3 getUniqueInstance(){
if(uniqueInstance==null){
uniqueInstance=new Singleton3();
}
return uniqueInstance;
}
}
4、懒汉式,双重锁DCL
(Double Check Lock)
第一个if语句是防止实例化后重复实例化,第二个if语句是防止前面同时进来的线程重复实例化,这个加了锁,保证只有一个线程能进入。
volatitle防止指令重排序,通过四种内存屏障实现,LL,LS,SS,SL实现。
public class Singleton4{
private Singleton4(){}
private volatitle static Singleton4 uniqueInstance;
public staticSingleton4 getUniqueInstance(){
if(uniqueInstance==null){
synchronized(Singleton4.class){
if(uniqueInstance==null){
uniqueInstance=new uniqueInstance();
}
}
}
return uniqueInstance;
}
}
5、静态内部类
https://www.cnblogs.com/zouxiangzhongyan/p/10762540.html
静态内部类并不是外部类加载的时候就加载进内存,当被调用到时才会触发加载和初始化。这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。
public class Singleton5{
private Singleton5(){}
private static class Singleton5Holder{
private static final Singleton5 instance=new Singleton5();
}
public static Singleton5 getUniqueInstance(){
return Singleton5Holder.instance;
}
}
6、枚举类
可以防止反射和序列化攻击
public enum Singleton6{
INSTANCE;
public Singleton6 getInstance(){
return INSTACE;
}
}
优点
系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
缺点
- 因为单例模式中没有抽象层,所以单例类的扩展有很大的困难。
- 单例类的职责太重,在必定程度上违背了”单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库链接池对象设计为的单例类,可能会致使共享链接池对象的程序过多而出现链接池溢出
适用场景
• 需要频繁的进行创建和销毁的对象;
• 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
• 工具类对象;
• 频繁访问数据库或文件的对象。
原型模式
概述
使用原型实例来指定要创建对象的类型,并通过复制这个原型来创建新对象。
类图
优点
- 可以简化对象的创建过程,通过复制一个已有的实例来提高创建新实例的效率。
- 简化了创建结构,原型模式中产品的创建是封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。
缺点
- 需要对每一个类配置一个克隆方法,而且该方法位于类的内部,当对已有的类进行改造时,需要修改源码,违反开闭原则。
- 当对象之间有多重嵌套引用时,为了实现深克隆需要每一层对象都支持深克隆,实现起来繁琐
应用:
ctrl CV通过原型模式来大大提高对象的创建效率
spring也可以通过原型模式来创建Bean实例
浅拷贝和深拷贝
对象拷贝:
Student studentB = studentA
对象拷贝后没有生成新的对象,二者的对象地址是一样的;而浅拷贝的对象地址是不一样的。
浅拷贝:
创建一个新对象对源对象进行拷贝,如果属性是基本类型,则直接拷贝它的值,如果属性是引用类型,则拷贝的是它的内存地址,因此如果一个对象修改了引用类型,就会影响另一个对象。而修改基本类型则不会影响另外一个。浅拷贝会带来数据安全方面的隐患
实现:直接实现cloneable接口,重写clone()方法进行浅拷贝
深拷贝:
深拷贝,拷贝基本类型时跟浅拷贝一样,直接赋值。而在拷贝引用类型成员变量时,它会新建一个对象空间,然后拷贝其中的内容,所以它跟源对象指向的是不同的内存空间。深拷贝相比于浅拷贝速度较慢并且花销较大。深拷贝后,不管是基础数据类型还是引用类型的成员变量,修改其值都不会相互造成影响。
实现:
- 对每一层对象都实现cloneable接口
- 通过对象序列化实现深拷贝
结构型
组合模式
概述
将对象组合成树形结构来表示“整体/部分”的层次关系,组合类聚合了组件,允许用户以相同的方式处理单独对象和组合对象。
结构
组件类(Component)是叶子类(Leaf)和组合类(Composite)的父类,可以把组合类看成是树的中间结点
组合对象拥有一个或者多个组件对象,因此组合对象的操作可以委托给组件对象去处理,而组件对象可以是另一个组合对象或者叶子对象。
·
优点:
- 可以清楚地定义分层次的复杂对象,方便对层次结构进行控制,增加新的组合类也容易。
- 客户端调用简单,客户端可以一致地使用组合结构或其中单个对象,用户不必关心是组合结构还是单个对象,简化了客户端代码
- 符合开闭原则,客户端不必因为加入了新的对象组合类而改变原有的代码
缺点:
- 使设计变得抽象,如果业务复杂,组合模式设计将具有很大挑战
- 很难对容器中的构件类型进行限制,因为他们都来自于同一个父类。必须通过在运行时进行类型检查 , 这样就变得比较复杂 。
应用:
- 因为XML文档本身是一个树形结构,所以很多xml解析工具都是用组合模式对xml进行解析
- 操作系统的目录结构是一个树形结构,因此杀毒软件在查毒和杀毒时应用组合模式可以对一个目录或者一个具体文件进行查杀。
适配器模式
概述:
把一个类的接口转换为客户希望的另一个接口,使原本由于接口不兼容而不能一起工作的那些类能一起工作
类适配器是用单继承多实现来实现,对象适配器是用关联和继承来实现
结构:
优点:
- 通过引入适配器类来将目标类和适配者进行解耦,无需改动原有的代码。
- 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端是透明的,而且提高了适配者类的复用性
- 灵活性和拓展性都非常好,可以在不改动源码的情况下增加新的适配器类,符合开闭原则
缺点:
像java这种不能多继承的语言,一次最多只能适配一个适配者类,使用有一定的局限性。而对于对象类型的适配器模式,因为适配器类和适配者的关系是关联,所以要改动时比较复杂。
应用:
- jdbc接口和数据引擎之间是通过适配器来连接
- springAOP中的BeforeAdvice、AfterAdvice和ThrowsAdvice是通过适配器模式来实现
外观模式
概述:
提供了一个统一的接口,用来访问子系统中的一群接口,从而使子系统更容易使用。
结构:
优点:
- 降低子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户端屏蔽了子系统的组件,减少了客户处理的对象数目
缺点:
- 不能很好的限制客户端使用子系统类,容易带来未知的风险
- 增加新的子系统可能需要改变外观类或客户端的源代码,违背了“开闭原则”。
应用:
装饰模式
概述
指在不改变现有对象结构的情况下,动态的给该对象增加一些职责的模式。
结构
优点
- 装饰器模式可以提供比继承更多的灵活性,在拓展功能时只需要增加新的装饰类即可,而继承则需要继承原有的类,再进行拓展
行为型
命令模式
http://c.biancheng.net/view/1380.html
概述
将一个请求封装成一个对象,将发出请求的责任和执行请求的责任分割开,这样两者之间通过命令对象进行沟通。
在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。
再比如,我们去餐厅吃饭,菜单不是等到客人来了之后才定制的,而是已经预先配置好的。这样,客人来了就只需要点菜,而不是任由客人临时定制。餐厅提供的菜单就相当于把请求和处理进行了解耦,这就是命令模式的体现
结构
优点:
- 引入中间件(抽象接口)降低系统的耦合度
- 扩展性良好,增加和删除命令非常方便
缺点:
- 可能产生大量具体的命令类,因为每一个操作都需要设计一个具体的命令类,这会增加系统的复杂性。
- 额外增加类的数量,增加了理解上的困难。