架构师考点大纲 面向对象方法
本文整理软件设计——特别是面向对象设计的几大原则。从网上收集,不同文章或视频,对其说法不同,有五大原则,七大原则,八大原则,本文加了部分个人的理解,但总体是汇集各家说法而得。
SOLID原则
S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写。
SRP - Single Responsibility Principle 单一职责原则
there should never be more than one reason for a class to change
一个类应该只有一个发生变化的原因
一个类只做一件事,一个类应该只有一个引起它修改的原因。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。
一个类应该仅有一个引起它变化的原因。
对象不应承担太多功能,正如一心不能而用,比如太多的工作(种类)会使人崩溃。唯有专注才能保证对象的高内聚;唯有唯一,才能保证对象的细粒度。
解决问题:
假如有A和B两个类,当A需求发生改变需要修改时,不能导致B类出问题。
现状:
在实际情况很难去做到单一职责原则,因为随着业务的不断变更,类的职责也在发生着变化,即职责扩散。如类A完成职责P的功能,但是随着后期业务细化,职责P分解成更小粒度的P1与P2,这时根据单一职责原则则需要拆分类A以分别满足细分后的职责P1和P2。但是实际开发环节,若类的逻辑足够简单,可以在代码上级别上违背单一职责原则;若类的方法足够少,可以在方法级别上违背单一职责原则。
OCR - Open Close Principle 开闭原则
Software entities like classes, modules and functions should be open for extension but closed for modification
一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
对扩展开放,对修改/更改封闭。
这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
软件对象(类、模块、方法等)应该对于扩展是开放的,对修改是关闭的。比如:一个网络模块,原来只有服务端功能,而现在要加入客户端功能,那么应当在不用修改服务端功能代码的前提下,就能够增加客户端功能的实现代码,这要求在设计之初,就应当将客户端和服务端分开。公共部分抽象出来。
问题由来:
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
解决办法:
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
LSP - Liskov Substitution Principle 里氏替换原则
Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方必须能透明地使用其子类的对象
子类应该可以完全替换父类。也就是说在使用继承时,只扩展新功能,而不要破坏父类原有的功能。
子类必须能够替换它们的基类(IS-A)。
继承表达类型抽象。
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系 子类应当可以替换父类并出现在父类能够出现的地方。比如:公司搞年度派对,都有员工都可以抽奖,那么不管是新员工还是老员工,也不管是总部员工还是外派员工,都应当可以参加抽奖。
里氏替换至少包含一下两个含义:
1、里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
2、如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
子类必须能够替换它们的基类(IS-A)。
继承表达类型抽象。
LOD - Law of Demete 迪米特法则
Talk only to your immediate friends and not to strangers
只与你的直接朋友交谈,不跟“陌生人”说话
也叫最少知识原则(Least Knowledge Principle,LKP)。一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
迪米特法则的定义是只与你的直接朋友交谈,不与”陌生人”说话。如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该应用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的朋友是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
优点:
1、降低类之间的耦合度,提高模块的相对独立性。
2、由于亲和度降低,从而提高了类的可复用率和系统的扩展性。
缺点:
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
使用迪米特法则需要注意:
1、在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
2、在类的结构设计上,尽量降低类成员的访问权限。
3、在类的设计上,优先考虑将一个类设置成不变类。
4、在对其他类的引用上,将引用其他对象的次数降到最低。
5、不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
6、谨慎使用序列化(Serializable)功能。
ISP - Interface Segregation Principle 接口分离原则 接口隔离原则
Clients should not be forced to depend upon interfaces that they don`t use.
The dependency of one class to another one should depend on the smallest possible.
1、客户端不应该依赖它不需要的接口。
2、类间的依赖关系应该建立在最小的接口上。
1、不应该强迫客户程序依赖他们不用的方法。
2、接口应该小而完备。(类间的依赖关系应该建立在最小的接口上)
换句话说,使用多个专门的接口比使用单一的总接口总要好。其实通俗来理解就是,不要在一个接口里面放很多的方法,这样会显得这个类很臃肿。接口应该尽量细化,一个接口对应一个功能模块,同时接口里面的方法应该尽可能的少,使接口更加灵活轻便。或许有的人认为接口隔离原则和单一职责原则很像,但两个原则还是存在着明显的区别。单一职责原则是在业务逻辑上的划分,注重的是职责。接口隔离原则是基于接口设计考虑。例如一个接口的职责包含10个方法,这10个方法都放在同一接口中,并且提供给多个模块调用,但不同模块需要依赖的方法是不一样的,这时模块为了实现自己的功能就不得不实现一些对其没有意义的方法,这样的设计是不符合接口隔离原则的。接口隔离原则要求”尽量使用多个专门的接口”专门提供给不同的模块。
DIP - Dependence Inversion Principle 依赖倒置原则
High level modules should not depend upon low level modules. Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
1、上层模块不应该依赖底层模块,它们都应该依赖于抽象。
2、抽象不应该依赖于细节,细节应该依赖于抽象。
1、高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。自注:高层模块较稳定,低层经常变化(如平台库等要做移植、适配),所以高层不能依赖底层。不管高层、底层模块,都要依赖抽象。
2、抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。
自注:越高层次,越要抽象(参考基类、纯虚函数),抽象的东西是稳定的(或很少变),细节会经常变,因此不能依赖细节的实现(这样太局限,不通用)。
3、依赖倒置的中心思想是面向接口编程。
4、依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多。
5、使用接口或抽象类的目的是指定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类来完成。
其它汇总
合成复用 Composite Reuse Principle
优先使用对象组合,而不是类继承
类继承通常为“白箱复用”,对象组合通常为“黑箱复用” 。
继承在某种程度上破坏了封装性,子类父类耦合度高。
而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
自注:如果只用继承,子类会越来越大,(不需要的)功能越来越多。
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
DRY - Don’t Repeat Yourself 千万不要重复你自身
尽量在项目中减少重复的代码行,重复的方法,重复的模块。许多设计原则和模式最本质的思想都是在消除重复。DRY意味着系统内的每一个部件都应该是唯一的并且是不模糊的。我们可以通过应用单一职责接口隔离等原则尽量拆分系统,模块,类,方法·。使其每一个部件都是职责明确的并且可重用的。DRY利用的方法就是抽象:把共同的事物抽象出来,把代码抽取到一个地方去。这样就可以避免写重复的代码。
KISS - Keep It Simple & Stupid 保持简单易懂
从小到几行代码的写法大到整个系统的架构我们都应该保持简单易懂。高手高就高在可以将复杂的东西“简单”的实现出来。刚入行的时候,我总喜欢用三目运算符将复杂的逻辑用一句冗长的代码行写出来。后面才发现这是非常愚蠢的。到了重构或者需求变更的时候,连我自己写的代码我都看着非常费劲难以下手。所以我们应该致力于代码的可理解性。降低复杂度也意味着维护变得简单。Martin Flower在《重构》中有一句经典的话:”任何一个傻瓜都能写出计算机可以理解的程序,只有写出人类容易理解的程序才是优秀的程序员。其实不光是程序,这个原则也可以延伸到产品的设计,业务的设计,项目结构的设计上。
YAGNI - You Ain’t Gonna Need It 适可而止
千万不要进行过度设计。我们经常会在开发当中尽可能的迎合未来可能的需求。而为了迎合某些产生概率极低的需求而设计的成本是非常高的,这种过度设计的收益非常低。可能你深思熟虑的设计花了不少时间成本,却在未来的两三年内这个设计却完全没有派上用场。一些设计是否必要,更多的应该基于当前的情况。而不是为了应对未来的各种变化,画蛇添足的设计。如果淘宝一开始就往日均交易上亿的情况进行设计的话,那么可能就不会有今天的淘宝了。因为创业公司的时间是非常宝贵的,比其他公司早一步退出新的服务就能抢占先机。并不是说淘宝不需要考虑以后交易量暴增的情况,而是不应该以当前日均交易才几万的情况下去设计编码日均交易上亿的项目。过度设计往往会延缓开发迭代的速度。
YAGNI原则指的是只需要将应用程序必需的功能包含进来,而不要试图添加任何其他你认为可能需要的功能。
Tell, Don’t Ask
Soc - Sepration of Concerns
其它
封装变化点
使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
针对接口编程,而不是针对实现编程
不将变量类型声明为某个特定的具体类,而是声明为某个接口。
客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。
自注:网络通信中,以 http、ws、tcp 接口分类实现,不是按某些类型的设备。