1.开闭原则
概念
- 一个软件实体(类、模板和函数),应对扩展开放,对修改关系(通过配置文件修改)。
软件实体:项目中划分出的模块、类与接口、方法 - 所谓开闭,也正是对扩展和修改两个行为的一个原则。
- 核心:面向抽象原则:用抽象构建框架,用实现扩展细节,提高软件的可复用性和可维护性。
含义
当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
作用
开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体来说,其作用如下。
- 对软件测试的影响:
软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。 - 可以提高代码的可复用性:
粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。 - 可以提高软件的可维护性:
遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
实现方法
可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
eg:创建商品接口,并实现商品接口,添加折扣优惠方法
方案1:将修改置于接口,则每个实现类都要添加方法实现
方案2:再写一个处理优惠的逻辑类
- 优点:易提高软件的可复用性和维护性
- 缺点:易引起类爆炸
方案3:利用IOC框架(Spring)动态创建对象
public static void main(String []args){
//创建Spring容器,解析xml文件;
// ApplicationContext为一接口,new后面的是它的实现类,参数为xml配置文件
ApplicationContext context=new ClassPathXmlApplicationContext("bean.xml");
//根据Bean的id查找对象
HelloBean helloBean=(HelloBiz)context.getBean("helloBean");
System.out.println(helloBean. getPrice());
}
}
2.依赖倒置原则(Dependence Inversion Principle)
概念
- 底层模块:不可分割的逻辑
- 高层模块:逻辑的再组装
- 高层模块不应该依赖底层模块
- 两者都应依赖其抽象不应依赖其细节
- 细节应该依赖抽象
核心思想是:要面向接口编程,不要面向实现编程。
原则:通过面向接口的编程来降低类间的耦合性.主要规则有以下四点:
1.每个类尽量提供接口或抽象类,或者两者都具备。
2.变量的声明类型尽量是接口或者是抽象类。
3.任何类都不应该从具体类派生。
4.使用继承时尽量遵循里氏替换原则。
什么是抽象
抽象就是指接口或抽象类,两者都是不能直接被实例化的
什么是细节
细节就是实现类,实现接口或继承抽象类而产生的类就是细节
特点就是可以直接被实例化
依赖倒置原则
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
- 接口或抽象类不依赖于实现类;
- 实现类依赖接口或抽象类。
IOC (Inverse of Control) 控制反转
- 设计好的类由系统控制,而不是在类的内部控制,称为控制反转。
- 创建具体对象的控制权交给配置文件(Java或者C#通过反射机制动态创建对象)。
控制反转&依赖注入
控制反转IoC
- 对象的控制权进行转移
eg. 转移交给了IoC容器,它就是一个创建工厂。
需要的对象可以通过配置文件创建
依赖关系发生变化:原先的依赖关系消失,因为它们都依赖IoC容器。通过IoC容器建立它们之间的关系。
依赖注入
所谓依赖注入,就是由IoC容器在运行期间,动态地将某种依赖关系注入到对象之中。
注入方式:
- 构造函数注入
- setter注入
- 基于注解的注
总结
依赖倒置的基本要求:
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
- 接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。
- 依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合
- 变量类型尽量是接口或者是抽象类
- 任何类都不应该从具体类派生。(不一定)
- 尽量不要覆写基类的方法:
如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。
3.单一职责原则
3.1 定义
- 单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。
这里的职责是指类变化的原因,该原则提出对象不应承担太多职责,否则:
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
3.2 实现方法
单一职责原则需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。
发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
4.接口隔离原则
4.1 定义
- 接口隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
(一个类对另一个类的依赖应该建立在最小的接口上)。
即:要为各个类建立它们需要的专用接口,而非试图去建立一个很庞大的接口供所有依赖它的类去调用。
4.1.1 区别于单一职责原则
- 相同:接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想
- 不同:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
4.2优点
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
4.3 实现方法
4.3.1 使用规则
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
4.3.2 具体应用(学生成绩管理程序)
(插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能)
5.迪米特法则
只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)
5.1 定义
- 如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
5.2 优缺点
- 优点:(限制了软件实体之间通信的宽度和深度)
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
- 缺点:过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低
- 总结:在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰.
5.3 实现方法
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
5.3.1 注意事项
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。 -private
- 在类的设计上,优先考虑将一个类设置成不变类。 -final
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
5.3.2 实例(明星与经纪人关系实例)
(明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人)
6.里氏替换原则
- 继承必须确保超类所拥有的性质在子类中仍然成立
6.1 定义
-
如果对每一个类型为 T1 的 对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都替换 成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型
-
一个软件实体如果适用一 个父类的话,那一定是适用于其子类,所有引用父类的地方必须能透明地使用其子类的 对象,子类对象能够替换父类对象,而程序逻辑不变。
引申含义
- 子类可以扩展父类的功能,但不能改变父类原有的功能
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类可以增加自己特有的方法
- 子类重载父类方法时,方法的前置条件(方法的输入/入参)要比父类的更为宽松
- eg. 父类方法传参HashMap,子类方法传参可谓Map(宽松)
- 子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件要比父类相等或更严格
6.2 实例(几维鸟不是鸟)
点击查看鸟类与对应实例类
//鸟类
class Bird {
double flySpeed;
public void setSpeed(double speed) {
flySpeed = speed;
}
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
//燕子类
class Swallow extends Bird {
}
//几维鸟类
class BrownKiwi extends Bird {
public void setSpeed(double speed) {
flySpeed = 0;
}
}
此时调用几维鸟的飞行方法时会报错(x/0)
解决方法:可将几维鸟继承更宽泛的类(动物类)
7.合成复用原则
7.1 定义
- 合成复用原则(组合/聚合原则):软件复用时尽量先使用组合/聚合等关联关系,其次才考虑使用继承关系。
- 若要使用继承关系,则需严格遵循里氏替换原则
7.2 与继承复用的区别
- 继承复用(Is-A)缺点
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
- 合成复用(Has-A)优点
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
7.3 实现方法
- 合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
实例:银行卡功能实现(我们需要办理一张银行卡,如果银行卡默认都拥有了存款、取款和透支的功能)
合成复用:建立储蓄卡和信用卡两个接口,使用银行卡类实现两个接口实现取款、存款、透支功能。
在我的银行卡类中建立银行卡属性用于对我的银行卡进行管理(取款、存款、透支)。
8. 七种设计模式总结
设计模式的目的:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。
8.1 总结
- 开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭;
- 里氏替换原则告诉我们不要破坏继承体系;
- 依赖倒置原则告诉我们要面向接口编程;
- 单一职责原则告诉我们实现类要职责单一;
- 接口隔离原则告诉我们在设计接口的时候要精简单一;
- 迪米特法则告诉我们要降低耦合度;
- 合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。
8.2 记忆
-
访问加限制,函数要节俭,依赖不允许,动态加接口,父类要抽象,扩展不更改。
-
在程序设计时,我们应该将程序功能最小化,每个类只干一件事。若有类似功能基础之上添加新功能,则要合理使用继承。对于多方法的调用,要会运用接口,同时合理设置接口功能与数量。最后类与类之间做到低耦合高内聚。