(一)优秀设计的特征
1.代码复用
减少成本和时间
2.扩展性
如何同时提高一个软件系统的可维护性和可复用性是面向对象设计需要解决的核心问题之 一。
(二)SOLID原则
1.单一职责原则
一个对象应该只包含单一的职责,并且该职责被完美的封装在一个类中。
2.开闭原则
软件实体应对扩展开放,而对修改关闭
软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。
开闭原则就是指软件实体尽量在不修改源码的情况下进行扩展。
开闭原则是评价基于某个设计模式设计的系统是否具备灵活性和可扩展性的重要依据。
3.里氏替换LSP原则
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
任何基类可以出现的地方,子类一定可以出现。(正方形继承长方形,<=实行增加宽度,正方形将一直增加下去直至溢出,长方形出现的地方正方形并不一定能出现)
子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
常见的 LSP 违规
- 子类中的退化方法:如果基类有一个方法,但基类的子类不需要该方法,那么如果子类的作者再次 退化该方法,这将是可替代的违规。例如abc三个方法,子类不需要bc方法就将其置空
- 从子类抛出异常: LSP 违规的另一种形式是向子类添加异常,而基类不希望这样。因为那时基类不 能被子类替代。 例如巧克力、视频、图片继承产品,巧克力实行下载方法抛出异常
4.依赖倒置原则
高层模块不应该从低层模块导入任何东西,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应当依赖于抽象。
针对接口编程,而不是针对实现编程
在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
案例一:
DataBiz执行业务的时候需要根据不同的类型频繁修改代码
- 基于依赖倒置原则,新增一个抽象的转换器 DataConvert , DataBiz 针对 DataConvert 进行编 程,
- 根据里氏替换原则,程序运行时对父类进行替换
- 根据开闭原则,将运行对象指定设置到 配置文件中。
开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段
案例二:
public class Computer {
private XiJieHardDisk hardDisk;
private IntelCpu cpu;
private KingstonMemory memory;
public IntelCpu getCpu() {
return cpu;
}
public void setCpu(IntelCpu cpu) {
this.cpu = cpu;
}
public KingstonMemory getMemory() {
return memory;
}
public void setMemory(KingstonMemory memory) {
this.memory = memory;
}
public XiJieHardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(XiJieHardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public void run() {
System.out.println("计算机工作");
cpu.run();
memory.save();
String data = hardDisk.get();
System.out.println("从硬盘中获取的数据为:" + data);
}
}
public class Computer {
private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;
public HardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public Cpu getCpu() {
return cpu;
}
public void setCpu(Cpu cpu) {
this.cpu = cpu;
}
public Memory getMemory() {
return memory;
}
public void setMemory(Memory memory) {
this.memory = memory;
}
public void run() {
System.out.println("计算机工作");
}
}
5.接口隔离原则
客户端不应该依赖哪些它不需要的接口。
每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。
这里有一个云服务提供商,和两个文件存储服务商,如果CloudProvider设计过大,那么有的小文件存储服务商将要实现它不需要的接口
将CloudProvider进行拆分,分成主机、缓存、存储三个接口
(三)组合复用原则
优先使用对象的组合,而不是使用继承来达到复用的目的
在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或 通过继承,但首先应该考虑使用组合/聚合
(四)迪米特法则
每一个软件单位对其他单位尽可能少的了解,而且局限于哪些与本单位密切相关的的软件。
每个单位应该只和它的朋友们通信,不与陌生人通信。
只和直接的朋友通信。
迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
1.狭义的迪米特法则
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某 一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
直接朋友:
1. 当前对象本身(this);
2. 以参数形式传入到当前对象方法中的对象;
3. 当前对象的成员对象;
4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
5. 当前对象所创建的对象
不是直接朋友的典型情况:只出现在方法体内部的类对象
案例:
老师的requireCount方法中创建了student列表,与非直接朋友通信了
修改为Monitor维护学生列表,老师调用monitor方法,没有与student发生关系
2.广义的迪米特法则
- 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
- 在类的设计上,只要有可能,一个类型应当设计成不变类;
- 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
案例:
这里human类的wash方法需要充分了解machine的方法
修改为全自动洗衣机,降低耦合。这里只需要调用machine的work方法,一切都交给machine去做
3.使用注意事项
- 在类的划分上,应当创建弱耦合的类,类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,每个类都应该降低成员的访问权限。
- 在类的设计上,只要有可能,一个类应当设计成不变的类。
- 在对其他类的引用上,一个对象对其他类的对象的引用应该降到最低。
- 尽量限制局部变量的有效范围,降低类的访问权限。
小结
1.面向对象设计原则的目标是什么?
提高软件的可维护性和可复用性,实现可维护性的复用。
2.什么是单一职责原则?
一个对象应该只包含单一的职责,并且该职责被完美的封装在一个类中。
3.什么是开闭原则?
软件实体应对扩展开放,而对修改关闭。
4.什么是里氏替换原则?
软件中所有引用基类(父类)的地方必须能透明地使用其子类的对象。
5.什么依赖倒置原则?
高层模块不应该从低层模块导入任何东西,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应当 依赖于抽象。即:针对接口编程,而不是针对实现编程。
6.什么是接口隔离原则?
客户端不应该依赖哪些它不需要的接口。
7.什么是组合复用原则?
优先使用对象的组合,而不是使用继承来达到复用的目的。
8.什么是迪米特法则?
每一个软件单位对其他单位尽可能少的了解,而且局限于哪些与本单位密切相关的的软件