编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,复用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好的
设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础
设计模式常用的七大原则有:
大白话解释:一个类就是一个最小的功能单位
描述 一个类应该只负责一项职责。 如类A负责两个不同职责:职责1,职责2。 当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为 A1,A2。
实例 假设有一个“交通工具”类,他的作用只有一个,就是“运行交通工具”,假设它只有一个run方法,打印“交通工具 xx 在地上跑”这句话。
如果我们的交通工具只是车,这个类没有问题,如果交通工具加上“飞机”、“船”,那么“交通工具 飞机 在地上跑”、“交通工具 船 在地上跑”就不符合实际。
由于交通工具有多个,因此这个类不符合“单一职责原则”。 我们可以将其改为3个类,“水上交通工具”、“空中交通工具”、“陆地交通工具”,分别对海陆空负责(单一职责)。
此外,由于这个类的功能比较单一,只有run方法,我们也可以在方法级别上实现单一职责原则,即为该类创建“水上运行”、“空中运行”、“陆地运行”方法。
注意事项与细节
如果类中方法数量足够少,可以在方法级别保持单一职责原则
大白话解释:实现接口的所有类都应当觉得接口中没有多余的方法
Interface Segregation Principle 描述 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
实例
A通过调用B,需要操作1、2、3, C通过调用D,需要操作1、4、5, 但是B、D都实现了1、2、3、4、5,显然B、D都实现了多余的方法。
若要符合“接口隔离原则”,只需要让B、D实现必需的接口即可。
然而,实际中我们不一定能确定B是否真的不需要4、5方法,也不能确定D是否真的不需要2、3方法。 因此,不是说学好设计模式就万事大吉的。 实际还得多方面考虑。
大白话解释:我们都是用接口声明一个变量,而不是直接使用具体类(如声明一个ArrayList,最左边用的是List接口;声明一个HashMap,最左边用的是Map)
Dependence Inversion Principle 描述
面向接口编程
以抽象为基础搭建的架构比以细节为基础的架构要稳定的多
。(在java中,抽象指的是接口或抽象类,细节就是具体的实现类)就是一句话,前期设计应当从最基本、最核心的抽象入手,构建接口。
实例 用户(User类)通过receive方法接收信息(Message类)。 如果用户接受的消息包括Email、QQ、WeChat…等多种方式,那么我们就需要在User类中写多个receive重载函数分别接收不同的消息类。 然而,谁也不知道以后还会有什么消息类,这就导致每次增加一个消息类,我们都得对User类进行修改。
如果Message不是类,而是一个接口,receive接收的是Message接口,就能解决问题。 只需要让各种不同的消息类实现Message接口,receive就能够接收他们;如果出现新的消息类,也只需要增加该消息类并实现Message接口即可,不需要对原有代码进行更改。
依赖关系传递的方式
注意事项和细节
声明类型尽量是抽象类或接口
, 这样我们的变量引用和实际对象间,就存在 一个缓冲层,利于程序扩展和优化大白话解释:少继承、别重写父类方法
OO中的继承,产生的问题
解决以上问题,考虑里氏替换原则。
描述 Liskov Substitution Principle
所有引用基类的地方必须能透明地使用其子类的对象
。尽量不要重写父类的方法
聚合
,组合
,依赖
来解决问题。.实例 类A的fun1是减法器,类B继承了类A;但是类B不小心将fun1重写成了加法器。 假设极端情况,类A就只有fun1方法,那么类B继承类A就没有必要了,把A唯一的方法都重写了。
实际编程中常常重写父类的方法,但是整个继承体系的复用性、稳定性较差。 一般可以这么做:让原来的父类A和子类B都继承一个更通俗的基类,取消AB继承关系,AB直接采用聚合、组合、依赖的关系实现方法调用。
比如上述实例,A和B都继承一个基础类Base(为了保证一些基本方法),然后B中声明一个成员类A,此时B可以写一个方法调用A的专有方法即可。
大白话解释:写的系统易于扩展,不允许修改。
Open Closed Principle 描述
对扩展开放,对修改关闭
。用抽象构建框架,用实现扩展细节大白话解释:减少类之间的依赖
描述
迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部
。对外除了提供的public 方法,不泄露任何信息
迪米特法则还有个更简单的定义:只与直接的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。 耦合的方式很多,依赖,关联,组合,聚合 等。其中,我们称出现
成员变量
,方法参数
,方法返回值
中的类
为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部
。
此外,有时候我们也会引入了“陌生的朋友”而不自知。 比如较长的调用链,调用链中可能生成了多个陌生的类,这也是不被允许的。
迪米特法则的目的在于降低类之间的耦合度。
大白话解释:多用组合、聚合,少用继承
描述 尽量使用合成/聚合的方式,而不是使用继承。
如果仅仅是为了让B类使用A类的方法,就让B继承A,只是徒增耦合。 我们只需要在B中聚合一个A的对象,或者将A作为B的某个方法参数。
设计模式分为三种类型,共23种
大白话解释: 创建型模式:
结构型模式:
行为型模式:
单例模式有八种实现方法(有一种是错误示范):
AbstractProduct:抽象类产品,比如说披萨 ConcreteProduct:具体的产品,比如说中国披萨,美国披萨,巴西披萨等 SimpleFactory:用于创建披萨类,依赖于抽象披萨 FacrotyClient:工厂的使用者,通过FactoryClient,调用SimpleFactory生成不同的披萨。
原型模式(Prototype Pattern)是用于创建重复的对象
,同时又能保证性能。
Spring的bean.xml中配置的bean,scope可以选择单例,也可以选择prototype,即原型模式创建。
如果你的bean中有初始化信息,那么通过prototype模式创建的bean,都会带上这些初始化信息
原理:实现Cloneable接口,利用Object的clone,实现克隆
浅拷贝:使用clone克隆的对象,基本类型是值传递(新的对象),引用类型只是地址传递(依旧是指向旧的对象,如果此时对该对象修改,也会影响原有对象)。
深拷贝:
对于拷贝对象中的引用类型,也实现Cloneable接口,然后对其单独处理。但是这种方式局限性太大,不推荐,直接学习另一种方式,利用反序列化
原理:序列化之后,能够保存所有“值信息” 反序列化得到的是一个新的对象。
public class PrototypePattern {
public static void main(String[] args) {
Sheep s1 = new Sheep("duoli","black");
Sheep friend = new Sheep("friend","red");
s1.setFriend(friend);
Sheep s2 = (Sheep) s1.deepClone();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1.getFriend()==s2.getFriend());
}
}
class Sheep implements Serializable {
private String name;
private String color;
// 深拷贝测试对象
private Sheep friend;
Sheep(String name,String color){
this.name=name;
this.color=color;
}
@Override
public String toString() {
return "Sheep{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
", friend=" + friend +
'}';
}
Object deepClone() {
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
ByteArrayInputStream bis = null;
try {
// 序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
return (Sheep) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}finally {
try {
if (bos != null) {
bos.close();
}
if (oos != null) {
oos.close();
}
if (bis != null) {
bis.close();
}
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void setFriend(Sheep friend) {
this.friend = friend;
}
public Sheep getFriend() {
return friend;
}
}
动态地获得对象运行时的状态
BuilderPattern 盖房子,需要逐步完成一系列工序才能得到最终的房子。 建造者模式,就是将产品(房子)与产品构造过程(一系列工序)进行解耦。
涉及角色: 产品:Product,比如一栋房子 抽象建造者:Builder,大家一直认为施工队该有的基本操作 具体建造者:ConcreteBuilder,实际开工的工人,只提供动作,指挥者让干嘛就干嘛 指挥者:Director,工头(隔离了老板与工人直接接触,负责控制房子的生产过程)
调用过程: (客户端)老板告诉(指挥者)工头,我要“这种”房子(指定建造者, 工人),工头对这类房子的建造方式已经记住了,然后告诉工人开始搭建,最后工头将房子交给老板。
此处老板指定建造者,而不是老板定制(指定)房子,是因为: 如果定制房子,老板就得对房子的参数非常熟悉才行 如果有新的建造者加入,就得更改客户端代码
package creationMode._4_builderPattern;
public class BuilderPattern {
public static void main(String[] args) {
// 老板跟包工头说要一个小平房
Director director = new Director(new ConcreteHouseBuilderOne());
House result1 = director.getResult();
System.out.println(result1);
// 老板跟包工头说要一栋高楼
director = new Director(new ConcreteHouseBuilderTwo());
House result2 = director.getResult();
System.out.println(result2);
}
}
// 产品:房子
class House{
private String name;
private Integer height;
House(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getHeight() {
return height;
}
public void setHeight(Integer height) {
this.height = height;
}
@Override
public String toString() {
return "House{" +
"name='" + name + '\'' +
", height=" + height +
'}';
}
}
// 抽象建造者
interface AbstractHouseBuilder{
void buildStepOne();
void buildStepTwo();
House getResule();
}
// 具体建造者:工人1号
class ConcreteHouseBuilderOne implements AbstractHouseBuilder{
private House house = new House();
@Override
public void buildStepOne() {
this.house.setName("小平房");
}
@Override
public void buildStepTwo() {
this.house.setHeight(10);
}
@Override
public House getResule() {
return house;
}
}
// 具体建造者:工人2号
class ConcreteHouseBuilderTwo implements AbstractHouseBuilder{
private House house = new House();
@Override
public void buildStepOne() {
this.house.setName("大高楼");
}
@Override
public void buildStepTwo() {
this.house.setHeight(100);
}
@Override
public House getResule() {
return house;
}
}
// 指挥者:包工头
class Director{
private AbstractHouseBuilder builderOne;
Director(AbstractHouseBuilder builderOne){
this.builderOne = builderOne;
}
House getResult(){
builderOne.buildStepOne();
builderOne.buildStepTwo();
return builderOne.getResule();
}
}
StringBuilder使用的建造者模式
特点
如果产品之间的差异性很大,则不适合使用建造者模式
,因此其使用范围受到一定的限制。抽象工厂模式VS建造者模式
:抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程
,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图
建造产品,它的主要目的是通过完成一系列工序而产生一个新产品描述
Adapter
Pattern)将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性
,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper
)类适配器模式
、对象适配器模式
、接口适配器模式
工作原理
Adapter类,通过继承
src类,实现 dst 类接口,完成src->dst的适配。
实例
国家只提供了220V电压,我们的手机需要5V电压,因此需要一个适配器。
适配器继承220V电压(为了拿到电压),然后实现5V的接口(相当于适配器变压标准)。 接着手机充电的时候,只需要遵循5V接口就行了。 然后用户Client左拿适配器(充电器),右拿手机,就可以充电了。
注意事项
Adapter类,通过持有
src类,实现 dst 类接口, 完成src->dst的适配。
细节
实例
如上图,接口提供了4个方法,如果我们直接实现接口,将不得不实现全部方法。 但是我们加一个适配器,实现接口中的全部方法(但是都是空方法,没有写具体实现), 然后A继承适配器,就可以有选择的重写自己想要的方法。
实现逻辑:前端发起请求,DispatchServlet调用doDispatch()方法,调用controller方法。
为什么应用适配器模式:前端发起的请求有多种,可能需要HttpController进行处理,也可能是SimpleController或者AnnotationController进行处理,但是我们并不知道要用哪个;最简单的方式就是在处理的时候用if-else对请求类别进行甄别,然后调用对应controller;但是,显然这种模式违背了OCP原则。
适配器模式: 处理请求的控制器接口; 分别处理请求的三个具体控制器; 每个具体的控制器都有一个对应的适配器,这些适配器都遵循同一个适配器接口。 dispatchServlet拿到请求之后,通过适配器判断控制器类型,并调用控制器方法。
模拟运行轨迹: 前端发起Http请求==》DispatcherServlet拿到请求,调用doDispatch() 》doDispatch()拿到该请求,交给HandlerAdapter适配器进行类别判断》判断结果为Http请求,此时用HttpHandlerAdapter调用HTTPController中的方法,处理请求。
适配器模式注意事项
继承
对象适配器:以对象给到,在Adapter里,将src作为一个对象,持有
接口适配器:以接口给到,在Adapter里,将src作为一个接口,实现
考虑这么一个问题:有“圆形”“正方形”“三角形”三个形状,然后有“红色”“绿色”“蓝色”三种颜色,现在我们需要“红色正方形”、“绿色正方形”、“蓝色正方形”、“红色三角形”、“绿色三角形”、“蓝色三角形”、“红色圆形”、“绿色圆形”、“蓝色圆形”,需要几个类? 如果是按x色x形分别建类,那么显然需要3*3个类。 这种实现快速简单,但是却难以扩展。比如我们现在多了一个绿色,那么就得再建三个“绿色xx形”,形成类爆炸。
这就是理解桥接模式的方式——属性维度。 将每一维,都单独抽出来,然后通过组合聚合的形式放在一个桥接类中。
这个“抽象”与“实现”的区别,网上并没有找到结论。 个人觉得,没啥区别,抽象接口直接跟Client交互,并且组合实现类,因此,可以将重要的“属性”作为抽象。 或者说,抽象与实现是主从关系,实现属于抽象,比如说“颜色属于形状,因此形状是抽象,颜色是实现”。
此外,个人觉得这个二维维度可以扩展成多维。
定义 在不改变原有对象的基础之上,将功能附加到对象上。提供了比继承更有弹性的替代方案(扩展原有对象功能)
优点
类图特点:
可装饰属性
相关方法 比如说用调味品装饰咖啡,那么咖啡的描述和价格就是可装饰属性,此时该顶级接口应当带上描述和价格,只有这样,才能在装饰后对这些属性进行动态修改。
常见这种形式:
这种模式,可以抽象为一棵树
。
学校是一个根节点,同学是叶子节点。
这就是组合模式。
注意:
叶子节点也包含的行为
作为抽象方法,让叶子也能实现;将叶子节点不包含的行为
(如移除子节点和增加子节点)写一个默认实现,抛出不支持调用的异常,让其他子节点自己实现。其他细节:
不用考虑整体部分或者节点叶子
的问题。如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
划重点:树形结构适合、能帮助客户端忽略节点和叶子的差异性、如果节点和叶子本身存在较大差异则不适合使用组合模式(比如学校和学生的操作肯定存在较大差异,建议将学生、班级从这个树状结构排除,让系作为叶子节点)
考虑: 家庭影院系统
看电影的步骤: 准备躺椅 打开幕布 打开投影仪,调节亮度,选择节目
结束看电影的步骤: 关闭投影仪 关闭幕布 收起躺椅
如果是用户直接面对躺椅、幕布、投影仪,那他就得一步一步操作(尽管这里看起来不复杂),假设是一个子系统非常多的大系统,那么这些一步一步操作就会非常复杂。
可以这么做:
弄一个Facade类,组合
幕布、投影仪、躺椅,用户只需要输入一个电影名,这个影院系统就自动完成所有准备工作。
外观模式的注意事项和细节
分层设计
时,可以考虑使用Facade模式维护一个遗留
的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade类,来提供遗留系统的比较清晰简单的接口, 让新系统与Facade类交互,提高复用性让系统有层次,利于维护为目的
。