设计模式
复用体现于:
- 抽象化和继承:使概念和定义可复用
- 多态:使实现和应用可复用
- 抽象化和封装:可保持和促进系统的可维护性
开-闭原则OCP
软件组成实体应该是对扩展开放的,但是对修改是关闭的。
也就是:软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及函数(Functions)等等,应该在不修改现有代码的基础上,引入新功能。 “开”,是允许对其进行功能扩展的; “闭”,是指对于原有代码的修改是封闭的,即不应该修改原有的代码。
解释:
- 在设计一个软件的时候,应当使这个软件可以在不被修改的前提下扩展
- 已有模块,尤其是最重要的抽象层模块不能动,需要保证稳定性和延续性
- 可以扩展新模块:增加新行为,保证灵活性
- 即:不允许更改的是系统的抽象层,允许扩展的是系统的实现层。
开-闭法则认为应该试图去设计出永远也不需要改变的模块。开闭原则的关键在于抽象化,用来给系统定义一个一劳永逸,不再更改的抽象设计,此设计允许有无穷无尽的行为在实现层被实现;同时,抽象层可以允许、支持所有扩展。
优点
可复用性好:能够提高适应性 、 灵活性
- 我们可以在软件完成以后,仍然可以对软件进行扩展,加入新的功能,非常灵活。因此,这个软件系统就可以通过不断地增加新的组件,来满足不断变化的需求。
可维护性好:
- 由于对于已有的软件系统的组件,特别是它的抽象底层不去修改,因此,我们不用担心软件系统中原有组件的稳定性,这就使变化中的软件系统有一定的稳定性和延续性。
一个软件系统的所有模块不可能都满足OCP,但是应该努力最小化这些不满足OCP的模块数量。
封装可变性原则
考虑设计中什么可能会发生变化,考虑你允许什么发生变化而不让这一变化导致重新设计
发现变化点,并封装之:
- 一种可变性不应散落在代码的很多角落
- 一种可变性不应当与另一种可变性混合在一起
里氏替换法则
使用指向基类(超类)的引用的函数,必须能够在不知道具体派生类(子类)对象类型的情况下使用它们。
解释:
一个软件如果使用的是一个父类的话,如果把该父类换成子类,它不能察觉出父类对象和子类对象的区别
也就是凡是父类适用的地方子类也适用
子类适用的地方不要求父类一定能适用
继承只有满足里氏代换原则才是合理的
小结
里式替换法则(LSP)清楚地表明了Is a(是什么)关系全部都是与行为有关的。
为了保持LSP,所有子类必须使用基类所期望的行为。
一个子类型不得具有比基类型更多的限制,可能这对于基类型来说是合法的,但是可能会因为违背子类型的其中一个额外限制,从而违背了LSP!
LSP保证一个子类总是能够被用在其基类可以出现的地方
依赖倒转原则
抽象不应当依赖于细节,细节应当依赖于抽象。
解释:
- 传统的设计是抽象层依赖具体层
- 传统的重用,侧重于具体层次的模块,比如算法、数据结构、函数库
- 因此软件的高层模块依赖低层模块
高层依赖底层会带来很多问题:
抽象层包含的是系统的业务逻辑和宏观的、战略性的决定,是必然性的体现
具体层则含有与实现相关的算法和逻辑,以及战术性的决定,带有相当大的偶然性选择。具体层经常有变动,难免出现错误
实现方式:
- 针对接口编程
- 而不针对实现编程
不将变量声明为某个特定0的具体类的实例对象,而让其遵从抽象类定义的接口。实现类仅实现接口,不添加方法。
- 任何变量都不应该持有一个指向具体类的指针或引用
- 任何类都不应该从具体类派生
- 任何方法都不应该覆写它的任何基类中已实现了的方法
优点:
- Client不必知道其使用对象的具体所属类。
- 一个对象可以很容易地被(实现了相同接口的)的另一个对象所替换。
- 对象间的连接不必硬绑定(hardwire)到一个具体类的对象上,因此增加了灵活性。
- 松散耦合(loosens coupling)。
- 增加了重用的可能性。
- 提高了(对象)组合的机率,因为被包含对象可以是任何实现了一个指定接口的类。
组合复用原则
优先使用(对象)组合,而非(类)继承
组合与继承可以一起工作,但优先使用组合可以获得重用性与简单性更佳的设计,随后可以通过继承,以扩充可用的组合类集。
组合
优点:
- 封装性:调用时仅能通过被包含对象的类接口对其进行访问,被包含对象的内部细节对外不可见,封装性好。
- 耦合性:在实现上对象的相互依赖性比较小。
- 单一性:每一个类只专注于一项任务。
- 动态性:通过获取指向其它的具有相同类型的对象引用,可以在运行期间动态地定义(对象的)组合。
缺点:
- 数量角度:系统中的对象过多。
- 容错角度:为了能将多个不同的对象作为组合块(composition block)来使用,必须仔细地对接口进行定义。
继承
- (类)继承是一种通过扩展一个已有对象的实现,从而获得新功能的复用方法。
- 泛化类(超类)可以显式地捕获那些公共的属性和方法。
- 特殊类(子类)则通过附加属性和方法来进行实现的扩展
优点:
- 易于实现:容易进行新的实现,因为其大多数可继承而来。
- 易于修改:易于修改或扩展那些被复用的实现。
缺点:
- 封装性: 破坏了封装性,因为这会将父类的实现细节暴露给子类,父类的内部细节对于子类可见。
- 当父类的实现更改时,子类也会随之更改。
- 从父类继承来的实现将不能在运行期间进行改变。
Coad规则
仅当下列的所有标准被满足时,方可使用继承:
- 子类表达了“是一个…的特殊类型”,而非“是一个由…所扮演的角色”。
- 子类的一个实例永远不需要转化(transmute)为其它类的一个对象。
- 子类是对其父类的职责(responsibility)进行扩展,而非重写或废除(nullify)。
- 子类没有对那些仅作为一个工具类(utility class)的功能进行扩展。
迪米特法则 (LoD)
又称最少知识原则:一个对象应该对其他对象尽可能少的了解。
狭义的迪米特法则
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
“朋友”条件:
- 当前对象本身(this)
- 以参量形式传入到当前对象方法中的对象
- 当前对象的实例变量直接引用的对象
- 当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友
- 当前对象所创建的对象
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。
缺点:
- 在系统里造出大量的小方法,这些方法仅仅是传递间接的调用,与系统的商务逻辑无关。
- 遵循类之间的迪米特法则会是一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。
门面模式和调停者模式实际上就是迪米特法则的应用。
广义的迪米特法则
- 优先考虑将一个类设置成不变类。
- 尽量降低一个类的访问权限。
- 尽量降低成员的访问权限。
- 谨慎使用序列化(
Serializable
)。
接口隔离原则 (ISP)
- 使用多个专门的接口比使用单一的总接口好。
- 一个类对另一个类的依赖性应建立在最小的接口上。
一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
单一职责原则
不要存在多于一个导致类变更的原因。
通俗的说,即一个类只负责一项职责。
解释:
如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。
而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。
此原则的核心就是解耦和增强内聚性。
问题由来:
类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
解决方案:
遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
职责扩散
因为某种原因,职责P被分化为粒度更细的职责P1和P2。
如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。
但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)
参考链接
https://www.cnblogs.com/geek6/p/3951677.html
https://www.cnblogs.com/tongkey/p/7170826.html