定义:模块
内部元素
彼此结合的紧密程度。(模块内部元素的关联性大小)
内聚性高低判断:模块内部的元素都忠于模块职责
,元素之间紧密联系
(模块内部元素都为模块服务)。(若模块内元素都忠于模块职责,即使关联不紧密也不影响内聚性)
偶然内聚:模块之间的元素联系不大,因为“巧合”放在一起,实际没有什么内聚性,如:utils包。
逻辑内聚:元素逻辑上属于同一个比较宽泛的类
,但元素的职责可能不同。如:鼠标和键盘为输入类,打印机和显示器为输出类。
时间内聚:元素在时间上很相近。
过程内聚:模块内元素必须按照固定的“过程顺序”进行处理。如:读文件、解析、存储、通知、响应结果等封装在一个函数模块,它们的顺序固定。
*时间内聚和过程内聚区别是过程内聚的顺序是固定的,而时间内聚顺序可变。
信息内聚:模块内元素操作相同的数据,如:增删改查某个数据(某个Service内CRUD方法)。
顺序内聚:模块内某些元素的输出是另外元素的输入,如:规则引擎一个函数读取配置,将配置转换为指令,另一个函数执行指令。
功能内聚:元素都是为了完成同一个单一任务(内聚性最好的一种方式)。
定义:模块之间的依赖程度。(模块之间的关联性大小)
问:无耦合是不是最好的?
答:不一定,如果一个模块是最底层模块,没什么问题(被依赖);
如果该模块是完全自给自足,则会得不偿失:
消息耦合:模块间的耦合关系表现在消息传递上。这里的“消息”会随着“模块”的不同而不同。
消息耦合是一种耦合度很低的耦合,调用方仅仅依赖于被调用方的“消息”,不需要传递参数,无需了解或控制被调用方的内部逻辑
例:两个系统交互的接口:HTTP接口、RPC接口;A类调用B的某个方法,该方法就是消息;
数据耦合:两个模块通过参数传递基本数据。
被调用方依赖于调用方的参数数据
数据结构耦合:两个模块通过传递数据结构的方式传递数据,又称标签耦合,这里的数据结构是可理解为自定义对象参数,如:VmModel、Emp(区别于数据耦合)。
控制耦合:一个模块可以通过某种方式来控制另一个模块的行为。
外部耦合:两个模块依赖于外部相同数据格式、通信协议、设备接口
全局耦合:两个模块共享全局数据,也叫普通耦合
内容耦合:一个模块内容依赖另一个模块内容
问1:为什么要高内聚低耦合?
答:降低软件复杂性。提升软件的可复用、移植、扩展、修改能力。
问2:高内聚低耦合是否意味着内聚越高越好,耦合越低越好?
答:并非如此,高内聚和低耦合像天平两端,不可能同时上升,需要从中做个平衡。(如:关联很小的类全放一起反而增加了复杂性)
低内聚模块特性
低耦合特性
高内聚低耦合实际作用
高内聚将与模块相关的变化封装在模块内,产生的变化对其他模块的影响较小;低耦合使得模块之间的关联减小,一个模块受其他模块的影响可能性减小。
高内聚低耦合本质在于变化,核心是降低变化的影响。
Martin(人名)定义:一个类只有一个职责,职责是指引起该类变化的原因。
引起类变化的原因,如下变化都是类的职责吗:
另一种定义:SRP就是每个模块只做一件事
例1:学生信息管理系统,以下是四件事还是一件事?
站在我的角度
,学生管理系统的职责是管理学生信息,而这个职责包含了新增、查询、修改、删除学生四个功能。
例2: 我是快递员,我的工作是分包、收快递、送快递、通知收货人取快递等,在我们看来快递员的职责是快递管理。
职责的结论
类的职责
SRP的使用范围
:只适合基础类,不适用基于基础类的聚合类。
SRP总结:单一职责原则适用于模块(服务、类、模块、包等)职责定义,强调模块的的职责应该保证单一,且职责是一组相关功能的集合
。
维基百科:对扩展开放,对修改关闭(Open-Closed Principe)。 对使用者修改关闭,对提供者扩展开放。
个人理解:一个软件模块(类、模块、系统、函数等)应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。(如提供者有新的功能扩展,不能修改使用者模块)
基本介绍:
(1)一个软件实体如类,模块和函数应该对扩展开放(对于提供方来说),对修改关闭(对于使用方来说)。用抽象构建框架,用实现扩展细节。
(2)当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
(3)编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
例:有个IBook接口,有name()、price()、author()三个抽象方法,然后有一个小说类NovelBook实现了IBook。
- 需求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 {
@Overring
public String derver {
return "derver bmw.";
}
}
/**
* benz提供者
*/
public class Benz implements ICar {
@Overring
public String derver {
return "derver benz.";
}
}
/**
* dazh提供者
*/
public class Dazh implements ICar {
@Override
public String derver {
return "derver dazh.";
}
}
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
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 {
/**
* 复印
*/
@Override
public void copy(Paper paper) {
//
}
/**
* 扫描IFaxMachine
*/
@Override
public void scan(Paper paper){
//
}
/**
* 传真
*/
@Override
public void fax(String msg){
//
}
/**
* 打印
*/
@Override
public void print(Document doc){
//
}
}
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
*/
@Override
public void shift() {
//
}
/**
* brake里面的改变不影响Player
*/
@Override
public void brake() {
//
}
}
【Ford】
/**
* 奔驰车,对应DIP的低层模块或细节
* 底层模块依赖于抽象层
*/
public class Benz implements ICar {
/**
* shift里面的改变不影响Player
*/
@Override
public void shift() {
//
}
/**
* brake里面的改变不影响Player
*/
@Override
public void brake() {
//
}
}
【Chery】
/**
* 奔驰车,对应DIP的低层模块或细节
* 底层模块依赖于抽象层
*/
public class Chery implements ICar {
/**
* shift里面的改变不影响Player
*/
@Override
public void shift() {
//
}
/**
* brake里面的改变不影响Player
*/
@Override
public void brake() {
//
}
}
定义:只与你的直接朋友交谈,不跟“陌生人”说话
含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
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, 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
设计原则和设计模式是互补的,体现在:设计原则主要用于指导“类的定义
”的设计;设计模式主要用于“类的行为
(变化)”设计。
设计原则 | 设计模式 | |
---|---|---|
设计中使用先后顺序 | 先设计原则 | 后设计模式 |
作用 | 设计程序基础框架 | 设计程序运行规则 |
设计包含 | 类的定义(类、属性、方法)、类关系(封装、继承、多态)、抽象层设计 | 对象交互(交互) |
设计类别 | 静态设计(此时程序还是死的,没有运行规则) | 动态设计(让程序动起来) |
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 统计相关数据,例如将微博总数+1
return 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;
}
}
传统方法存在如下问题:
【设计模式方法】
设计模式封装变化是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;
}
@Override
private 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;
}
@Override
private 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;
}
@Override
private 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数据
}
}
观察者模式:
变化原因 | 变化描述 | 可用设计模式 |
---|---|---|
通过显式地指定一个类来创建对象 | 在创建对象时指定类名将使你受特定实现的约束,而不是特定接口约束。这会使未来的变化更复杂,为避免这种情况,我们应间接的创建对象。 | 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 |
【参考文献】
李运华
著《编程的逻辑-如何运用面向对象方法实现复杂业务需求》