导论
- 什么是设计原则:判断程序设计质量好坏的准则。
- 什么是设计模式:软件设计过程中重复出现问题的解决方案
- 设计原则的作用:指导抽象、类、类关系设计,相当于指导设计程序基础框架(Rank-分层、Role-角色、Relation-类关系)
- 设计模式的作用:知道对象关系设计
- 如何运用设计原则和设计模式
设计原则
内聚
定义:模块内部元素彼此结合的紧密程度。(模块内部元素的关联性大小)
- 模块:服务、模块、包、类/接口
- 元素:业务模块(服务)、包/命名空间(模块)、类/接口/全局数据(包)、属性/方法(类/接口)
内聚性高低判断:模块内部的元素都忠于模块职责,元素之间紧密联系(模块内部元素都为模块服务)。(若模块内元素都忠于模块职责,即使关联不紧密也不影响内聚性)
内聚分类
-
偶然内聚:模块之间的元素联系不大,因为“巧合”放在一起,实际没有什么内聚性,如:utils包。
-
逻辑内聚:元素逻辑上属于同一个比较
宽泛的类,但元素的职责可能不同。如:鼠标和键盘为输入类,打印机和显示器为输出类。 -
时间内聚:元素在时间上很相近。
-
过程内聚:模块内元素必须按照固定的“过程顺序”进行处理。如:读文件、解析、存储、通知、响应结果等封装在一个函数模块,它们的顺序固定。
*时间内聚和过程内聚区别是过程内聚的顺序是固定的,而时间内聚顺序可变。
-
信息内聚:模块内元素操作相同的数据,如:增删改查某个数据(某个Service内CRUD方法)。
-
顺序内聚:模块内某些元素的输出是另外元素的输入,如:规则引擎一个函数读取配置,将配置转换为指令,另一个函数执行指令。
-
功能内聚:元素都是为了完成同一个单一任务(内聚性最好的一种方式)。
耦合
定义:模块之间的依赖程度。(模块之间的关联性大小)
耦合分类
- 无耦合:模块之间没有任何关系或者交互
问:无耦合是不是最好的?
答:不一定,如果一个模块是最底层模块,没什么问题(被依赖);
如果该模块是完全自给自足,则会得不偿失:
- 失去重用其他模块的机会
- 什么都要自己做,重复造轮子,效率低
-
消息耦合:模块间的耦合关系表现在消息传递上。这里的“消息”会随着“模块”的不同而不同。
消息耦合是一种耦合度很低的耦合,调用方仅仅依赖于被调用方的“消息”,不需要传递参数,无需了解或控制被调用方的内部逻辑
例:两个系统交互的接口:HTTP接口、RPC接口;A类调用B的某个方法,该方法就是消息;
-
数据耦合:两个模块通过参数传递基本数据。
被调用方依赖于调用方的参数数据
- 数据通过参数传递,并非全局数据、配置文件、共享内存等方式
- 依赖的是基本数据类型
-
数据结构耦合:两个模块通过传递数据结构的方式传递数据,又称标签耦合,这里的数据结构是可理解为自定义对象参数,如:VmModel、Emp(区别于数据耦合)。
-
控制耦合:一个模块可以通过某种方式来控制另一个模块的行为。
-
外部耦合:两个模块依赖于外部相同数据格式、通信协议、设备接口
-
全局耦合:两个模块共享全局数据,也叫普通耦合
-
内容耦合:一个模块内容依赖另一个模块内容
高内聚低耦合
问1:为什么要高内聚低耦合?
答:降低软件复杂性。提升软件的可复用、移植、扩展、修改能力。
问2:高内聚低耦合是否意味着内聚越高越好,耦合越低越好?
答:并非如此,高内聚和低耦合像天平两端,不可能同时上升,需要从中做个平衡。(如:关联很小的类全放一起反而增加了复杂性)
低内聚模块特性
- 低内聚模块让人难以理解,增加了理解复杂度(关联紧密的元素都分开了,如:机器和货道独立到两个服务)
- 低内聚模块容易变化,增加了修改的复杂度(低内聚表明拆分细,变化点多,元素变化可能引起设计、测试、部署等的改变)
低耦合特性
- 模块本身显得庞大,功能集中在一起,这样反而会导致模块本身不稳定性增加(量变引起质变)
- 模块无法被重用
高内聚低耦合实际作用
高内聚将与模块相关的变化封装在模块内,产生的变化对其他模块的影响较小;低耦合使得模块之间的关联减小,一个模块受其他模块的影响可能性减小。
高内聚低耦合本质在于变化,核心是降低变化的影响。
类设计原则

SRP(单一职责原则)
Martin(人名)定义:一个类只有一个职责,职责是指引起该类变化的原因。
引起类变化的原因,如下变化都是类的职责吗:
- 给类新增一个方法
- 给类新增属性
- 给类方法新增一个参数
另一种定义:SRP就是每个模块只做一件事
例1:学生信息管理系统,以下是四件事还是一件事?
- 新增学生信息
- 查询学生信息
- 修改学生信息
- 删除学生信息
站在我的角度,学生管理系统的职责是管理学生信息,而这个职责包含了新增、查询、修改、删除学生四个功能。
例2: 我是快递员,我的工作是分包、收快递、送快递、通知收货人取快递等,在我们看来快递员的职责是快递管理。
职责的结论
- 职责是站在他人角度定义的
- 职责不是一件事,而是多件事
类的职责
- 类的职责是站在其他类的角度定义的
- 类的职责是一组多个相关功能的
SRP的使用范围:只适合基础类,不适用基于基础类的聚合类。
SRP总结:单一职责原则适用于模块(服务、类、模块、包等)职责定义,强调模块的的职责应该保证单一,且职责是一组相关功能的集合。
OCP(开闭原则)
维基百科:对扩展开放,对修改关闭(Open-Closed Principe)。 对使用者修改关闭,对提供者扩展开放。
个人理解:一个软件模块(类、模块、系统、函数等)应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。(如提供者有新的功能扩展,不能修改使用者模块)
基本介绍:
(1)一个软件实体如类,模块和函数应该对扩展开放(对于提供方来说),对修改关闭(对于使用方来说)。用抽象构建框架,用实现扩展细节。
(2)当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
(3)编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
例:有个IBook接口,有name()、price()、author()三个抽象方法,然后有一个小说类NovelBook实现了IBook。
- 需求1:需要给所有的书籍类新增出版时间publicationDate()功能。
- 需求2:对小说类的书籍进行打折销售。
- 需求1必须放在IBook接口实现
- 需求2的可能实现方式如下:
- 修改IBook接口
- 修改NovelBook类的价格方法
- 新增一个OffNovelBook小说打折类继承NovelBook,在OffNovelBook对价格进行处理。
问:需求1和需求2的实现方案满足OCP?
OCP的应用原则:
-
接口不变:接口里的函数名、参数、返回值等,可以应用OCP。
-
接口改变:已有的函数名称、参数、返回值或新增函数,OCP不再适用。
为什么使用开闭原则:
1、开闭原则是最基础的设计原则,其它五个设计原则都是开闭原则的具体形态,它们本身就遵循开闭原则。依照java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。
2、提高复用性
面向对象设计中,所有逻辑都是从原子逻辑组合而来,不是在一个类中独立实现一个业务逻辑。只有这样的代码才可以复用,粒度越小,被复用的可能性越大。那为什么要复用呢?减少代码的重复,避免相同的逻辑分散在多个角落。
3、提高维护性
软件上线后,需要对程序进行扩展,维护人员更愿意扩展一个类,而不是修改一个类。
让维护人员读懂原有代码,再修改,是一件痛苦的事情,不要让他在原有的代码海洋中游荡后再修改,那是对维护人员的折磨
4:面向对象开发的要求
万物皆对象,我们要把所有的事物抽象成对象,然后针对对象进行操作,但是万物皆发展变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。
如何运用开闭原则?
-
抽象约束
抽象是将一组关联共性事物独立出来,形成一组功能的定义,是面向对象编程的一个基础骨架抽象类。抽象没有具体的实现,因此变化的可能结果较多,变化由具体扩展类实现,而抽象可以约束变化。
-
封装变化
满足接口隔离原则
(1). 相同变化封装到一个接口或抽象类中
(2). 不同变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。 -
制定项目章程
约定优于配置,团队中,建立项目章程是非常重要的,因为章程是所有人员都必须遵守的约定,对项目来说,约定优于配置。这比通过接口或抽象类进行约束效率更高,而扩展性一点也没有减少。如:不同功能独立到不同函数;编程之前先设计抽象骨架;
OCP可广泛应用在函数、类、系统、子系统、模块等角色之间关系的设计。类之间应用OCP,使用interface进行交互,系统或模块之间使用规定好的协议,如:HTTP、GRPC等。
*提供者如果随意改动会引起使用者一起修改,这些扩展的灵活性就大大降低,使用者较多则修改影响较大。一般情况,提供者会通过接口定义一组功能定义。
错误例:
public class DriverCar {public static void main(String[] args){Player player = new Player();player.play(new Bmw());player.play(new Benz());player.play(new Dazh());}
}/*** 使用者*/
public class Player {/*** 使用者修改部分,提供方每次新增一个车型,使用者被迫都需要修改*/public void play(Car car) {String type = car.type();if(type.equals("bmw")) {System.out.println(car.derverBmw());} else if(type.equals("benz")) {System.out.println(car.derverBenz());} else if(type.equals("dazh")) {System.out.println(car.derverDazh());}}
}public class Car {String type;
}/*** bmw提供者*/
public class Bmw extends Car {public Bmw() {this.type = "bmw";}public String derverBmw {return "derver bmw.";}
}
/*** benz提供者*/
public class Benz extends Car {public Benz() {this.type = "benz";}public String derverBenz {return "derver benz.";}
}
/*** dazh提供者*/
public class Dazh extends Car {public Benz() {this.type = "dazh";}public String derverDazh {return "derver dazh.";}
}
OCP例:
public class DriverCar {public static void main(String[] args){Player player = new Player();player.play(new Bmw());player.play(new Benz());player.play(new Dazh());}
}/*** 使用者*/
public class Player {/*** 提供方每次新增一个车型,使用者无需修改*/public void play(Car car) {System.out.println(car.derver());}
}public class ICar {String derver();
}/*** bmw提供者*/
public class Bmw implements ICar {@Overringpublic String derver {return "derver bmw.";}
}
/*** benz提供者*/
public class Benz implements ICar {@Overringpublic String derver {return "derver benz.";}
}
/*** dazh提供者*/
public class Dazh implements ICar {@Overridepublic String derver {return "derver dazh.";}
}
LSP(里氏替换原则)
Liskov(发明者):
- 子类的对象提供了父类的所有行为,且加上子类额外的一些东西(可能是方法或属性)
- 当程序基于父类实现时, 如果将子类替换父类而程序不需要修改,则说明符合LSP
Martin:
- 函数使用指向父类的指针或者引用时,必须能否在不知道子类类型的情况下使用子类对象。
- 子类必须能替换成它们的父类
个人理解:对象引用尽量使用接口或抽象类,这样子类可以无感替换父类。
函数和父类交互

- 调用父类的方法(方法输入)
- 得到父类方法的输出(方法输出)
由上函数和父类交互体现可得出:子类应该和父类有同样的输入和输出
如何做到ISP:
- 子类必须实现或继承父类所有的公有方法
- 子类每个方法的输入参数必须和父类一样(也可以比父类更严格)
- 子类每个方法输出必须不比子类少(子类返回值应该比父类宽松),即父类的返回值应该是子类返回值的子集
例:
/*** 长方形*/
public class Rectangle {protected int _width;protected int _height;/*** 设置宽*/public void setWidth(int _width) {this._width = _width;}/*** 设置高*/ public void setHeight(int _height) {this._height = _height;}/*** 获取面积*/ public int getArea() {return this._height * this._width;}
}/*** 正方形*/
public class Square extends Rectangle {/*** 设置宽*/public void setWidth(int _width) {this._width = _width;this._height = _height;}/*** 设置高*/ public void setHeight(int _height) {this._width = _width;this._height = _height;}
}public class UnitTester {//main函数相当调用者public static void main(String[] args) {//父类的指针Rectangle rectangle = new Rectangle();rectangle.setWidth(4);rectangle.setHeight(5);System.out.println(rectangle.getArea() == 20);//子类替换了父类new Rectangle()rectangle = new Square();rectangle.setWidth(4);rectangle.setHeight(5);//可正常调用输出System.out.println(rectangle.getArea() == 20);}
}
//打印结果:
//true
//false
ISP(接口隔离原则)
Martin:
- 客户端不应该强迫去依赖它们并不需要的接口
- ISP承认对象需要需要非内聚接口,然而ISP建议客户端不需要知道整个类(包含所有功能),只需要知道具有内聚接口的抽象父类即可。
个人理解:不同功能的接口可以用不同的方式聚合到不同抽象类。(不同功能分开,需要哪些接口聚合到抽象类,做到要哪些功能聚合哪些)。
好处:
- 功能隔离互不影响
- 灵活扩展
- 依赖清晰
- 按需聚合
隔离的理解:隔离自己需要和不需要的部分
例:
/*** 复印机接口*/
public interface Icopier {/*** 复印*/void copy(Paper paper);
}/*** 传真机接口*/
public interface IFaxMachine {/*** 传真*/void fax(String msg);
}/*** 打印机接口*/
public interface IPrinter {/*** 打印*/void print(Document doc);
}/*** 扫描仪接口*/
public interface IScanner {/*** 扫描*/void scan(Paper paper);
}
【MultiFuncPrinter】
/*** 多功能打印机(一体机)* 实现了Icopier、IFaxMachine、IPrinter、IScanner四个接,而不是提供一个IMultiFuncPrinter包含了所有复印、打印、传真、扫描功* 能*/
public class MultiFuncPrinter implements Icopier, IFaxMachine, IPrinter, IScanner {/*** 复印*/@Overridepublic void copy(Paper paper) {//}/*** 扫描IFaxMachine*/@Overridepublic void scan(Paper paper){//}/*** 传真*/@Overridepublic void fax(String msg){//}/*** 打印*/@Overridepublic void print(Document doc){//}
}
DIP(依赖反转原则)
Martin:也称
依赖倒置原则
DIP含义:
- 高层模块不应该直接依赖底层模块,两者都应该依赖抽象层
- 抽象不能依赖细节,细节必须依赖抽象
DIP里描述的模块指:系统、子系统、模块、类等,因此模块是广义概念,不是狭义的软件系统里各个子模块。
由DIP含义映射到面向对象领域如下内容:
- 高层模块依赖于底层模块:高层模块(调用类)需要调用低层模块(被调用类)方法
- 高层模块依赖抽象层:高层模块基于抽象层编程
- 低层模块依赖抽象层:低层模块继承或实现抽象层
- 细节依赖抽象:细节指低层模块(子类),和上面的依赖一样
- 抽象不能依赖细节:低层模块(子类)的变化不会影响抽象层
例:Player代表玩家,Ford、Benz、Chery
【Player】
/*** 玩家,对应DIP中的高层模块*/
public class Player {/*** 开福特* 不好的依赖,Player直接依赖了Ford(低层模块而不是抽象)*/public void play(Ford car) {car.shift();car.brake();}/*** 开奔驰* 不好的依赖,Player直接依赖了Benz(低层模块而不是抽象)*/public void play(Benz car) {car.shift();car.brake();}/*** 开奇瑞* 不好的依赖,Player直接依赖了Chery(低层模块而不是抽象)*/public void play(Chery car) {car.shift();car.brake();}/*** 开车* 好的依赖,Player直接依赖低层模块的抽象层接口Icar,不需要知道具体车型,Ford、Benz、Chery修改不会影响Player,只有ICar修改* Player才需要改变*/public void play(ICar car) {car.shift();car.brake();}
}
【ICar】
/*** 汽车接口,对应DIP的抽象层*/
public interface ICar {car.shift();car.brake();
}
【Benz】
/*** 奔驰车,对应DIP的低层模块或细节* 底层模块依赖于抽象层*/
public class Benz implements ICar {/*** shift里面的改变不影响Player*/@Overridepublic void shift() {//}/*** brake里面的改变不影响Player*/@Overridepublic void brake() {//}
}
【Ford】
/*** 奔驰车,对应DIP的低层模块或细节* 底层模块依赖于抽象层*/
public class Benz implements ICar {/*** shift里面的改变不影响Player*/@Overridepublic void shift() {//}/*** brake里面的改变不影响Player*/@Overridepublic void brake() {//}
}
【Chery】
/*** 奔驰车,对应DIP的低层模块或细节* 底层模块依赖于抽象层*/
public class Chery implements ICar {/*** shift里面的改变不影响Player*/@Overridepublic void shift() {//}/*** brake里面的改变不影响Player*/@Overridepublic void brake() {//}
}
LOP(迪米特法则)
定义:只与你的直接朋友交谈,不跟“陌生人”说话
含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
LOP要求限制软件实体之间通信宽度和深度,正确使用LOP将有以下两个优点:
- 降低类之间的耦合度,提高了模块(类、模块、子系统、系统等)的相对独立性。
- 提高了模块可复用率和系统的扩展性。
缺点:过度使用LOP会使系统产生大量的中介类,增加系统了复杂性(如结构、代码),使模块之间的通信效率降低。
*釆用LOP时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
从迪米特法则的定义和特点可知,它强调以下两点:
- 从依赖者的角度来说,只依赖应该依赖的对象。(依赖中间对象)
- 从被依赖者的角度说,只暴露应该暴露的方法。(接口提供方只提供调用方需要的方法)
总结:LOP目的在于减少模块之间的依赖,中介者可隐藏后方的复杂性。因此调用方只依赖了中介者而无需依赖后方多个模块,且只提供需要的接口。
应用设计模式:
- 中介者模式
- 外观模式
例:
- 一个中介,客户只要找中介要满足的楼盘 ,而不必跟每个楼盘发生联系。
- 无服务中的网关,前端都请求到网关,而不是直接请求具体的微服务。
如何应用设计原则
SOLID是干什么用的(What),具体在什么时候用(When),什么场景用(Where)
SOLID应用场景
| 设计原则 | 应用场景 | 应用说明 | 描述 |
|---|---|---|---|
| SRP(单一职责) | 用于类的设计 | 对象的职责应该是单一的 | 当我们想出一个类或者设计出一个类的原型后,可通过SRP核对类的设计是否符合SRP原则 |
| OCP(开闭原则) | 总的指导思想 | 对扩展开放,对修改关闭 | 开闭原则是核心原则,是其他所有原则的基础,其他原则必须先遵守OCP |
| LSP(里氏替换) | 用于指导类继承的设计 | 程序中的对象是可以在不改变程序正确性的前提下被他的子类替换 | 当设计类之间的继承关系时,使用LSP来判断你的继承关系设计是否符合LSP要求 |
| ISP(接口隔离) | 用于指导接口设计 | 多个特定功能接口好过于一个功能宽泛的接口 | ISP可以看作是SRP的变种,思想是一致的,都强调职责的单一性,而ISP用于指导接口的设计,SRP用于指导类的设计 |
| DIP(依赖反转) | 用于指导如何抽象 | 依赖于抽象而不是实现(面向接口编程) | 当设计类之间的依赖(调用)关系时,可以使用DIP来判断这种依赖设计是否符合DIP。 DIP和LSP相辅相成: 1. DIP可用于指导抽象出接口或者抽象类 2. LSP用于指导从接口或者抽象类(也可以是普通类)派生出子类 |
| LOD(迪米特法则) | 用于指导类依赖关系 | 当设计类之间依赖关系,可通过LOP判断它们之间依赖是否存在多余 |
NOP
NOP, No Overdesign Priciple,不要过渡设计原则
过渡设计案例
架构师眼光长远,预测外来5年的业务变化,最后设计出的结果极为复杂。影响点:开发量大且复杂、测试和运维麻烦、出现问题可能不容易排查。
过渡设计危害:
- 预测越远,预测结果的准确性越低。
- 过渡设计会引入不必要的复杂性(运用设计原则可能会引入新的复杂性),如:代码量庞大、代码可阅读行降低、开发周期长、投入产出不成正比。
- 过渡设计有时候远比设计不足危害更大,设计不足我们还有
重构利器,不会出现浪费大量人力、物力的情况;而如果过渡设计原有的投入赞成浪费,其次即使是重构,也需要花费更多的人力物力。
设计模式
Gang of four(Gof):模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。书:《设计模式-可复用面向对象软件基础》
即:模式是不断重复发生问题的解决方案
一个模式包含如下几个部分:
【名称】:模式名称隐含了模式的问题、解决方案、效果等信息
【问题】:问题描述了模式的应用场景,准确的理解模式对应的问题是理解模式的关键,也是实践中应用模式的关键
【解决方案】:描述了问题是如何解决的。
*设计模式不会描述一个具体的设计方案或实现,而是提供设计问题的抽象描述和如何运用一般意义元素组合(类或者对象组合)来解决问题。即:解决方案更关注问题的抽象分析和解决方案,而不是具体的设计实现。
【效果】:包含好的效果的不好的效果,因此使用设计模式可能也会引入新的复杂度。
设计模式分类:创建型模式、结构型模式、行为型模式
从**《设计模式-可复用面向对象软件基础》**副标题得出:
- 设计模式解决的是“
可复用”的设计问题,如性能设计、可用性设计、安全设计、可靠性设计都不适用 - 设计模式应用的领域是“
面向对象”。
设计模式应用
设计模式应用的问题
非常熟悉设计模式,也能写出设计模式的样例代码,但实际项目设计和开发时,往往就陷入迷茫,不知哪个地方需要运用设计模式。
《设计模式》中23种设计模式只是掌握了设计模式的“术”。
如:木匠对工具锯、钻、锤、刨样样精通,但他先要知道在什么地方运用这些工具。
设计模式之道
设计模式之“
道”就是用于指导我们什么时候用设计模式,为什么要用设计模式,23个设计模式告诉了我们How,而设计模式之道可以告诉我们Why和Where。
Gof《设计模式》:Find what varies and encapsulate it. 翻译:找到变化,封装变化。
- “
找到变化”解决了“在哪里”使用设计模式的问题,即回答了“Where”的问题 - “
封装变化”解决了“为什么”使用设计模式的问题,即回答了“Why”的问题
面向对象的核心就是拥抱变化、提高扩展性。利用设计模式的目的就是封装变化。
变化带来的影响:
- 变化需要开发,设计不好会导致大量编码和自测工作
- 测试需要测试变化的部分,关联的不变的部分也需要测试
- 变化可能引起系统改动,上线后可能会出现问题,导致可用性降低
封装变化的好处:封装变化提升代码的可复用性、可扩展性、可测试性等。
变化可以存在于类、模块、系统内,封装变化的方式:
类和设计模式封装变化模块封装变化系统封装变化
Gof 在《设计模式》中提出中心思想是找到变化、封装变化,两个设计原则,形成一个中心两个基本点:
基于接口编程,而不是基于实现编程优先使用对象组合而不是类继承
学习和应用设计模式:
- 学习设计模式:学习设计模式的时候,必须深入理解是为了解决什么变化引起的问题,然后看设计模式如何应用两个基本点来封装变化。
- 应用设计模式:找到问题可能变化的地方,再去选择合适的设计模式。
设计模式应用之道法器
设计模式应用之道法器帮助我们如何活用设计模式
道:找变化、封装变化
法:面向接口编程而不是实现;优先使用对象组合而不是继承;
术:GOF设计模式、其他解决方法
器:Java、C++、UML
例:单体架构应对之道法术器
道:拆分
法:分布式、模块化
术:SOA、微服务
器:SpringBoot
原则 VS 模式
设计原则和设计模式是互补的,体现在:设计原则主要用于指导“类的定义”的设计;设计模式主要用于“类的行为(变化)”设计。
| 设计原则 | 设计模式 | |
|---|---|---|
| 设计中使用先后顺序 | 先设计原则 | 后设计模式 |
| 作用 | 设计程序基础框架 | 设计程序运行规则 |
| 设计包含 | 类的定义(类、属性、方法)、类关系(封装、继承、多态)、抽象层设计 | 对象交互(交互) |
| 设计类别 | 静态设计(此时程序还是死的,没有运行规则) | 动态设计(让程序动起来) |
| 4R架构包含关系 | 4R(Ralation、Role、Rank-类分层) | 4R(Rule) |
| 可扩展性 | 保证软件可扩展性 | 提高软件可扩展性 |
先设计原则和设计模式,即现设计好Ralation、Role、Rank等类的定义,再设计具体的交互规则,设计原则和设计模式都是为类做出更好的软件设计。
设计模式示例
观察者模式
【业务】用户发出一条微博后,可能需要完成如下相关的事情
- 统计微博的数量
- 将微博推送给粉丝
- 微博小秘书要审核微博
由于业务变化,以上粗粝可能还会不断增加
【发现变化】加入发微博事一个独立模块完成的,则这个模块本身是稳定的,不会经常变化,但发出微博之后的操作是随时可能变化的。
【传统方法】
传统方法是将所有操作都封装在一个模块内部,发完微博后就开始继续完成后续的处理工作
public class Weibo {public static boolean publish(int userId, String content) {int weiboId = save(content);//统计处理Statistics.save(userId, content);//发给粉丝Message.push(userId, content);//微博小秘书审核Audit.audit(userId, content);return true;}private static int save(String content){//TODO 省略return 10000;}
}public class Statistics {public static int add(int userId, int weiboId) {//TODO 统计相关数据,例如将微博总数+1return 10000;}
}public class Message {public static void push(int userId, int weiboId) {//TODO 获取粉丝列表,推送微博消息return 10000;}
}public class Audit {public static boolean audit(int userId, int weiboId) {//TODO 微博小秘书审核微博内容return false;}
}
传统方法存在如下问题:
- 新增变化业务时,Weibo的publish需要同步修改
- 当原油变化业务被重构,publish方法同样可能需要修改
【设计模式方法】
设计模式封装变化是Observer模式,中文“观察者模式”或者“发布订阅模式”。即:某个对象对某个“发布者”感兴趣,需要观察发布者状态变化。
/*** 发布者*/
public class Subject{protected ArrayList<Observer> observers = new ArrayList<();public void attah(Observer o) {//添加观察者// 这里用到了里氏替换原则和依赖反转;面向接口编程observers.add(o);}public void detach(Observer o) {//删除观察者// 这里用到了里氏替换原则和依赖反转;面向接口编程observers.remove(o);}public void notifyObservers() {//通知所有观察者// 这里用到了里氏替换原则和依赖反转;面向接口编程for(Observer o: observers){o.update();}}
}/*** 抽象观察者*/
public class Observer {public abstract void update();
}/*** 微博*/
public class Weibo extends Subject {public static boolean publish(int userId, String content) {int weiboId = save(content);//通知所有观察者,无需像传统方法那样调用各个观察者函数notifyObservers();return true;}private static int save(String content){//TODO 省略return 10000;}
}/*** 微博小秘书* Audit依赖于抽象Observer*/
public class Audit extends Observer {private Weibo weibo;/*** 观察者聚合了一个具体的发布者对象Weibo而不是Subject,在发布者调用通知方法执行update方法时,观察者处理实际发布者对象Weibo数据*/public Audit(Weibo weibo) {this.weibo = weibo;}@Overrideprivate void update(){//TODO 审核内容,处理实际发布者对象Weibo数据}
}/*** 消息推送* Message依赖于抽象Observer*/
public class Message extends Observer {private Weibo weibo;/*** 观察者聚合了一个具体的发布者对象Weibo而不是Subject,在发布者调用通知方法执行update方法时,观察者处理实际发布者对象Weibo数据*/public Message(Weibo weibo) {this.weibo = weibo;}@Overrideprivate void update(){//TODO 获取用户粉丝,推送微博信息,处理实际发布者对象Weibo数据}
}/*** 统计* Statistics依赖于抽象Observer*/
public class Statistics extends Observer {private Weibo weibo;/*** 观察者聚合了一个具体的发布者对象Weibo而不是Subject,在发布者调用通知方法执行update方法时,观察者处理实际发布者对象Weibo数据*/public Statistics(Weibo weibo) {this.weibo = weibo;}@Overrideprivate void update(){//TODO 统计相关的数据,如微博总数+1,处理实际发布者对象Weibo数据}
}/*** 统计*/
public class Test {public static void main(String[] args){Weibo weibo = new Weibo();Audit audit = new Audit(weibo);Message message = new Message(weibo);Statistics statistics = new Statistics(weibo);weibo.attach(audit);weibo.attach(message);weibo.attach(statistics);weibo.publish(10000, "第一条微博");weibo.publish(20000, "第二条微博");weibo.publish(30000, "第三条微博");//TODO 统计相关的数据,如微博总数+1,处理实际发布者对象Weibo数据}
}
观察者模式:
- 观察者和被观察者都依赖于抽象,用到了依赖反转原则
- 父类的引用可以被子类替换,用到了里氏替换
GOF设计模式
| 变化原因 | 变化描述 | 可用设计模式 |
|---|---|---|
| 通过显式地指定一个类来创建对象 | 在创建对象时指定类名将使你受特定实现的约束,而不是特定接口约束。这会使未来的变化更复杂,为避免这种情况,我们应间接的创建对象。 | Abstract Factory、Factory Method、Prototype |
| 对特殊操作的依赖 | 当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。为避免把请求代码写死,你将可以在编译时刻或运行时刻很方便地改变响应请求的方法。 | Chain of Resposibility,Command |
| 对硬件和软件平台的依赖 | 外部的操作系统接口和应用编程接口(API)在不同硬件平台上是不同的。依赖于特定平台的软件很难移植到其他平台,甚至都很难跟上本地平台的更新。所以系统设计使限制其平台相关性就很重要了。 | Abstract Factory、Bridge |
| 对对象表示或实现的依赖 | 依赖于对象的客户在对象发生变化时也需要变化,对客户隐藏这些变化信息能阻止连锁变化。 | Abstract Factory、Bridge、Memento、Proxy |
| 算法依赖 | 算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。 因此发生变化的地方应该被封装起来。 | Builder、Iterator、Strategy、Template Method、Visitor |
| 紧耦合 | 紧耦合很难被复用,依赖密切,修改模块时需要了解改变其他类。 松耦合提高了类被复用的可能性,易扩展、学习、移植、修改。设计模式采用抽象耦合和分层技术来提高系统的松散耦合。 | Abstract Factory、Command、Facade、Mediator、Observer、Chain of Reponsibility |
| 通过生成子类来扩充功能 | 优先使用对象组合而不是继承(扩展子类),应该优先利用现有对象的能力扩展新功能,过多的对象组合也会导致设计难以理解。 | Bride、Chain of Reponsibility、Composite、Decorator、Observer、Strategy |
| 不能方便的对子类进行修改 | 有时你不得不改变一个难以修改的类,或者可能对类的修改会要求修改其他已存在的类,应避免这种修改变化。 | Adapter、Decorator、Visitor |
总结

【参考文献】
李运华著《编程的逻辑-如何运用面向对象方法实现复杂业务需求》


















