设计原则设计模式

article/2025/11/5 21:03:28

导论

  1. 什么是设计原则:判断程序设计质量好坏的准则。
  2. 什么是设计模式:软件设计过程中重复出现问题的解决方案
  3. 设计原则的作用:指导抽象、类、类关系设计,相当于指导设计程序基础框架(Rank-分层、Role-角色、Relation-类关系)
  4. 设计模式的作用:知道对象关系设计
  5. 如何运用设计原则和设计模式

设计原则

内聚

定义:模块内部元素彼此结合的紧密程度。(模块内部元素的关联性大小)

  • 模块:服务、模块、包、类/接口
  • 元素:业务模块(服务)、包/命名空间(模块)、类/接口/全局数据(包)、属性/方法(类/接口)

内聚性高低判断:模块内部的元素都忠于模块职责元素之间紧密联系(模块内部元素都为模块服务)。(若模块内元素都忠于模块职责,即使关联不紧密也不影响内聚性)

内聚分类

  1. 偶然内聚:模块之间的元素联系不大,因为“巧合”放在一起,实际没有什么内聚性,如:utils包。

  2. 逻辑内聚:元素逻辑上属于同一个比较宽泛的类,但元素的职责可能不同。如:鼠标和键盘为输入类,打印机和显示器为输出类。

  3. 时间内聚:元素在时间上很相近。

  4. 过程内聚:模块内元素必须按照固定的“过程顺序”进行处理。如:读文件、解析、存储、通知、响应结果等封装在一个函数模块,它们的顺序固定。

    *时间内聚和过程内聚区别是过程内聚的顺序是固定的,而时间内聚顺序可变。

  5. 信息内聚:模块内元素操作相同的数据,如:增删改查某个数据(某个Service内CRUD方法)。

  6. 顺序内聚:模块内某些元素的输出是另外元素的输入,如:规则引擎一个函数读取配置,将配置转换为指令,另一个函数执行指令。

  7. 功能内聚:元素都是为了完成同一个单一任务(内聚性最好的一种方式)。

耦合

定义:模块之间的依赖程度。(模块之间的关联性大小)

耦合分类

  1. 无耦合:模块之间没有任何关系或者交互

:无耦合是不是最好的?

答:不一定,如果一个模块是最底层模块,没什么问题(被依赖);

​ 如果该模块是完全自给自足,则会得不偿失:

  • 失去重用其他模块的机会
  • 什么都要自己做,重复造轮子,效率低
  1. 消息耦合:模块间的耦合关系表现在消息传递上。这里的“消息”会随着“模块”的不同而不同。

    消息耦合是一种耦合度很低的耦合,调用方仅仅依赖于被调用方的“消息”,不需要传递参数,无需了解或控制被调用方的内部逻辑

    例:两个系统交互的接口:HTTP接口、RPC接口;A类调用B的某个方法,该方法就是消息;

  2. 数据耦合:两个模块通过参数传递基本数据。

    被调用方依赖于调用方的参数数据

    • 数据通过参数传递,并非全局数据、配置文件、共享内存等方式
    • 依赖的是基本数据类型
  3. 数据结构耦合:两个模块通过传递数据结构的方式传递数据,又称标签耦合,这里的数据结构是可理解为自定义对象参数,如:VmModel、Emp(区别于数据耦合)。

  4. 控制耦合:一个模块可以通过某种方式来控制另一个模块的行为。

  5. 外部耦合:两个模块依赖于外部相同数据格式、通信协议、设备接口

  6. 全局耦合:两个模块共享全局数据,也叫普通耦合

  7. 内容耦合:一个模块内容依赖另一个模块内容

高内聚低耦合

问1:为什么要高内聚低耦合?

答:降低软件复杂性。提升软件的可复用、移植、扩展、修改能力。

问2:高内聚低耦合是否意味着内聚越高越好,耦合越低越好?

答:并非如此,高内聚和低耦合像天平两端,不可能同时上升,需要从中做个平衡。(如:关联很小的类全放一起反而增加了复杂性)

低内聚模块特性

  • 低内聚模块让人难以理解,增加了理解复杂度(关联紧密的元素都分开了,如:机器和货道独立到两个服务)
  • 低内聚模块容易变化,增加了修改的复杂度(低内聚表明拆分细,变化点多,元素变化可能引起设计、测试、部署等的改变)

低耦合特性

  • 模块本身显得庞大,功能集中在一起,这样反而会导致模块本身不稳定性增加(量变引起质变)
  • 模块无法被重用

高内聚低耦合实际作用

高内聚将与模块相关的变化封装在模块内,产生的变化对其他模块的影响较小;低耦合使得模块之间的关联减小,一个模块受其他模块的影响可能性减小。

高内聚低耦合本质在于变化,核心是降低变化的影响。

类设计原则

img

SRP(单一职责原则)

Martin(人名)定义:一个类只有一个职责,职责是指引起该类变化的原因。

引起类变化的原因,如下变化都是类的职责吗:

  • 给类新增一个方法
  • 给类新增属性
  • 给类方法新增一个参数

另一种定义:SRP就是每个模块只做一件事

例1:学生信息管理系统,以下是四件事还是一件事?

  • 新增学生信息
  • 查询学生信息
  • 修改学生信息
  • 删除学生信息

站在我的角度,学生管理系统的职责是管理学生信息,而这个职责包含了新增、查询、修改、删除学生四个功能。

例2: 我是快递员,我的工作是分包、收快递、送快递、通知收货人取快递等,在我们看来快递员的职责是快递管理。

职责的结论

  • 职责是站在他人角度定义的
  • 职责不是一件事,而是多件事

类的职责

  • 类的职责是站在其他类的角度定义的
  • 类的职责是一组多个相关功能的

SRP的使用范围:只适合基础类,不适用基于基础类的聚合类。

SRP总结:单一职责原则适用于模块(服务、类、模块、包等)职责定义,强调模块的的职责应该保证单一,且职责是一组相关功能的集合

OCP(开闭原则)

维基百科:对扩展开放,对修改关闭(Open-Closed Principe)。 对使用者修改关闭,对提供者扩展开放。

个人理解:一个软件模块(类、模块、系统、函数等)应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。(如提供者有新的功能扩展,不能修改使用者模块)

基本介绍:

​ (1)一个软件实体如类,模块和函数应该对扩展开放(对于提供方来说),对修改关闭(对于使用方来说)。用抽象构建框架,用实现扩展细节。

(2)当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

(3)编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。

例:有个IBook接口,有name()、price()、author()三个抽象方法,然后有一个小说类NovelBook实现了IBook。

  • 需求1:需要给所有的书籍类新增出版时间publicationDate()功能。
  • 需求2:对小说类的书籍进行打折销售。
  1. 需求1必须放在IBook接口实现
  2. 需求2的可能实现方式如下:
    • 修改IBook接口
    • 修改NovelBook类的价格方法
    • 新增一个OffNovelBook小说打折类继承NovelBook,在OffNovelBook对价格进行处理。

问:需求1和需求2的实现方案满足OCP?

OCP的应用原则:

  1. 接口不变:接口里的函数名、参数、返回值等,可以应用OCP。

  2. 接口改变:已有的函数名称、参数、返回值或新增函数,OCP不再适用。

为什么使用开闭原则:

1、开闭原则是最基础的设计原则,其它五个设计原则都是开闭原则的具体形态,它们本身就遵循开闭原则。依照java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。

2、提高复用性
面向对象设计中,所有逻辑都是从原子逻辑组合而来,不是在一个类中独立实现一个业务逻辑。只有这样的代码才可以复用,粒度越小,被复用的可能性越大。那为什么要复用呢?减少代码的重复,避免相同的逻辑分散在多个角落。

3、提高维护性
软件上线后,需要对程序进行扩展,维护人员更愿意扩展一个类,而不是修改一个类。

让维护人员读懂原有代码,再修改,是一件痛苦的事情,不要让他在原有的代码海洋中游荡后再修改,那是对维护人员的折磨

4:面向对象开发的要求
万物皆对象,我们要把所有的事物抽象成对象,然后针对对象进行操作,但是万物皆发展变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。

如何运用开闭原则?

  1. 抽象约束

    抽象是将一组关联共性事物独立出来,形成一组功能的定义,是面向对象编程的一个基础骨架抽象类。抽象没有具体的实现,因此变化的可能结果较多,变化由具体扩展类实现,而抽象可以约束变化。

  2. 封装变化

    满足接口隔离原则

    (1). 相同变化封装到一个接口或抽象类中
    (2). 不同变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。

  3. 制定项目章程
    约定优于配置,团队中,建立项目章程是非常重要的,因为章程是所有人员都必须遵守的约定,对项目来说,约定优于配置。这比通过接口或抽象类进行约束效率更高,而扩展性一点也没有减少。

    如:不同功能独立到不同函数;编程之前先设计抽象骨架;

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:

  1. 函数使用指向父类的指针或者引用时,必须能否在不知道子类类型的情况下使用子类对象。
  2. 子类必须能替换成它们的父类

个人理解:对象引用尽量使用接口或抽象类,这样子类可以无感替换父类。

函数和父类交互

  • 调用父类的方法(方法输入)
  • 得到父类方法的输出(方法输出)

由上函数和父类交互体现可得出:子类应该和父类有同样的输入和输出

如何做到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:

  1. 客户端不应该强迫去依赖它们并不需要的接口
  2. 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目的在于减少模块之间的依赖,中介者可隐藏后方的复杂性。因此调用方只依赖了中介者而无需依赖后方多个模块,且只提供需要的接口

应用设计模式:

  • 中介者模式
  • 外观模式

例:

  1. 一个中介,客户只要找中介要满足的楼盘 ,而不必跟每个楼盘发生联系。
  2. 无服务中的网关,前端都请求到网关,而不是直接请求具体的微服务。

如何应用设计原则

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,而设计模式之道可以告诉我们WhyWhere

Gof《设计模式》:Find what varies and encapsulate it. 翻译:找到变化,封装变化

  1. 找到变化”解决了“在哪里”使用设计模式的问题,即回答了“Where”的问题
  2. 封装变化”解决了“为什么”使用设计模式的问题,即回答了“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;}
}

传统方法存在如下问题:

  1. 新增变化业务时,Weibo的publish需要同步修改
  2. 当原油变化业务被重构,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

总结

在这里插入图片描述

【参考文献】

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

http://chatgpt.dhexx.cn/article/VS3msHvt.shtml

相关文章

设计原则详解

1.单一职责 一个类&#xff0c;只有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线&#…

五大设计原则——SOLID

目录 简介&#xff1a; 1、单一职责原则&#xff08;SRP&#xff09; 2、开闭原则&#xff08;OCP&#xff09; 3、里式替换原则&#xff08;LSP&#xff09; 4、依赖倒置原则 (DIP) 5、接口隔离原则 (ISP) 简介&#xff1a; 无论是软件系统设计&#xff0c;还是代码实现…

1. 设计原则

文章目录 设计原则思维导图核心理论SOLID单一职责开放封闭里式替换接口隔离依赖反转 KISSDRYLOD 设计原则思维导图 核心理论 基于接口编程 “基于接口而非实现编程” - “Program to an interface, not an implementation”。 “接口”就是一组“协议”或者“约定”&#xff…

七大设计原则

一、七大设计原则 &#xff08;1&#xff09;单一职责原则 &#xff08;2&#xff09;接口隔离原则 &#xff08;3&#xff09;依赖倒置原则 &#xff08;4&#xff09;里氏替换原则 &#xff08;5&#xff09;开闭原则 &#xff08;6&#xff09;迪米特法则 &#xff0…

chrome浏览器截长图

使用chrome浏览器 打开开发者模式(更多工具->开发者工具) mac 按commandshiftp windows 按ctrlshiftp 然后输入capture 选择capture full size screenshot就可以了 截了个长图的例子

手把手教你截长图

1.截长图的工具 相信很多小伙伴在平时工作做都会碰见截图的问题&#xff0c;那正常的图&#xff0c;我们有各种方式去截取&#xff0c;例如&#xff1a;QQ的CtrlAltA&#xff0c;微信的AltA等等 但是呢&#xff0c;如果要用到长图的时候&#xff0c;就束手无策了&#xff0c;这…

python如何截长图_利用 Python + Selenium 实现对页面的指定元素截图(可截长图元素)...

对WebElement截图 WebDriver.Chrome自带的方法只能对当前窗口截屏&#xff0c;且不能指定特定元素。若是需要截取特定元素或是窗口超过了一屏&#xff0c;就只能另辟蹊径了。 WebDriver.PhantomJS自带的方法支持对整个网页截屏。 下面提供几种思路。 方式一 针对WebDriver.Chro…

谷歌浏览器怎么截长图?

我们在使用电脑浏览网页的时候难免会需要进行一些长图的截取&#xff0c;而一般的截图只能实现一部分截取&#xff0c;那么我们要如何去实现这个操作呢&#xff1f;下面小编就给大家介绍一下怎么在谷歌浏览器上截长图的操作。 谷歌浏览器网页截长图怎么截&#xff1f; 1、进入C…

html2canvas截长图

github链接 一、下载运行后选择下图的html2canvas即可直接去到路由界面测试 二、下图是html2canvas路由页面&#xff0c;点击右上角的生成图片即可下载长图 三、源码路径&#xff08;html2canvas源码github&#xff09; 四、源码&#xff08;关键在generateImage 这个方法&…

selenium+phantomjs截长图踩坑

目录 需求背景&#xff1a; 调研 phantomjs selenium 服务器部署 需求背景 BI上的报表需要设置定时任务截图发邮件到订阅人的邮箱中。刚开始以为截图的活是前端的&#xff0c;后来发现使自己的锅。 调研 截图的研究了一下&#xff0c;主流应该是 selenium 和 phantomjs。…

microsoft edge怎么截长图_实用技能 | Fireshot 网页截长图工具

FireShot 网页截长屏插件 网页截图有没有什么好方法? 在我们平常的工作、生活和学习中,截图是我们最常用到的功能之一。小编平常用到的是QQ、微信、电脑自带的快捷截图功能以及红蜻蜓截图软件等。 但是在浏览网页时,这些工具功能出现了一个致命的缺点,看到一个长长的文章,…

snipaste怎么滚动截长图_如何截长图,这3种方法你用过吗?

在工作中,经常需要截长图,那在电脑上你一般是如何操作呢?本期Word妹与大家分享2种快速截图技巧。 1、借用QQ工具 在最新的QQ版本中有一个长截图按钮,点击之后直接拉动需要长截图的内容,最后点击完成即可。 2、借用FastStone Capture工具 FSCapture是绿色版本不需要安装,可…

计算机win7截长屏,电脑截长图【应对法子】

喜欢使用电脑的小伙伴们一般都会遇到win7系统电脑截长图的问题&#xff0c;突然遇到win7系统电脑截长图的问题就不知道该怎么办了&#xff0c;其实win7系统电脑截长图的解决方法非常简单&#xff0c;按照 1&#xff1a;打开要截图的WORD文档&#xff0c;点击左上角的另存为或者…

网页截长图

很多时候我们需要在网页上截长图&#xff0c;下面记录下谷歌浏览器截长图的步骤&#xff1a; 1.在浏览器的右上角处找到三个竖着的小黑点按钮&#xff0c;点击选择该按钮&#xff0c;选择更多工具–>开发者工具 2.点击开发者工具后会显示这样的一个小窗口 3.按下CTRLShif…

分享电脑中截图的五种方法(包括截长图)

&#x1f482; 个人网站:【 海拥】【弗莱迪的物理实验】【开发文档导航】&#x1f91f; 风趣幽默的前端学习课程&#xff1a;&#x1f449;28个案例趣学前端&#x1f485; 想寻找共同学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼小组】&#x1f4ac; 免费且实用的计算机…

截长图方法

有的时候&#xff0c;我们可能需要截一个网站或者一些内容的图片&#xff0c;但屏幕不够大&#xff0c;出现滚动条&#xff0c;这时候我们就可以使用截长图的方法来达到截取整个网页的内容。 截长图方法有很多&#xff0c;以下介绍两种我所知道的截长图方法。 第一种方法&…

html5中框架怎么写,HTML5常用框架

HTML5常用框架 前言 随着HTML5规范的不断完善&#xff0c;围绕着这一生态有很多实用的框架&#xff0c;极大的提高了我们的开发效率&#xff0c;常见的框架代表有&#xff1a;UI层面的有Bootstrap、Yui、JqueryUI、CSS3Lib等&#xff0c;JS层面的有JQuery、Zeptojs、Angularjs、…

HTML框架构建

HTML框架构建 1、划分框架 A、使用标签决定如何划分框架.必须要有标签设定每个小窗口的网页&#xff0c;该标签中有src属性为每个URL值指定一个HTML文件&#xff08;这个文件必须事先做好&#xff09; B、标签常用的属性 属性描述cols用“像素数”或“%”分个左右窗口&#xf…

7个HTML5移动开发框架,初学HTML5必看

1、IONIC IONIC是目前最有潜力的一款HTML5手机应用开发框架。通过SASS构建应用程序&#xff0c;它提供了很多UI组件来帮助开发者开发强大的应用。它使用JavaScript MVVM框架和 AngularJS来增强应用。提供数据的双向绑定&#xff0c;使用它成为Web和移动开发者的共同选择。即将…

HTML5学习之常见的HTML5框架有哪些

HTML5简单易学门槛低&#xff0c;是Web时代前端开发最好用的工具。而HTML5开发人员的就业薪资也远远高于其他行业&#xff0c;资料显示&#xff0c;初级HTML5开发人员的平均薪资在8K-10K左右&#xff0c;拥有一定工作经验的人薪资普遍达到15K-20K。如此广阔的前景当然吸引了无数…