软件设计原则是设计模式的基石。目的只有一个,降低对象之间的耦合,增加程序的可复用性、可扩展性、可维护性。
开闭原则 OCP定义:软件实体对扩展开放,对修改关闭。
对扩展开发,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
对修改关闭,意味着类一旦设计完成,就可以独立的工作,而不要对其进行任何的修改。
在面向对象设计中,我们通常通过继承和多态来实现OCP,即封装不变部分。
比如需求要实现2种状态的业务。
如果用if else来判断,那么后面加第三种状态,就还需要在此接口上增加else逻辑,不符合开闭原则。
用策略类实现,则定义策略接口,策略A和策略B为具体实现类,分别对应两种状态。假如下一次需求要实现第三种状态,那么直接定义一个StrategyC实现类就可满足。原有代码不变,符合开闭原则。
详情可点击:策略模式文章
里氏替换原则 LSP定义:程序中的父类型都可以正确的被子类型替换。
程序中的对象可以在不改变程序正确性的前提下被它的子类所替换,即子类可以替换任何基类能够出现的地方,并且经过替换后,代码还能正确工作。
根据LSP的定义,如果在程序中出现使用instanceof、强制类型转换或者函数覆盖,很可能意味着是对LSP的破坏。
假设定义一个抽象禽类,有一个飞翔方法fly(), 我们就可以自由的继承禽类衍生出各种鸟儿,并调用其飞翔方法。如果鸵鸟加入禽类行列,继承禽类,但不会飞,那么飞翔方法fly()就显得多余。而且在所有禽类出现的地方,无法用鸵鸟替换(此时不满足正确业务逻辑)。违反了里氏替换原则。
经过反思,是设计问题,禽类和飞翔无必然联系,所以禽类不应该定义飞翔方法fly(),把禽类飞翔方法fly()抽离出去单独定义飞翔接口Flyable。
对于有飞翔能力的鸟儿继承禽类并实现飞翔接口。鸵鸟继承禽类,但不实现飞翔接口,是否是鸟儿取决于是否继承自禽类,能不能飞取决于是否实现飞翔接口。所有禽类出现的地方都可以用子类进行替换,所有飞翔接口出现的地方都可以被其替换为实现。
依赖倒置原则 DIP定义:模块之间交互应该依赖抽象,而非实现。
DIP要求高层模块不应该依赖于底层模块,二者都应该依赖于抽象。抽象不应该依赖细节,细节应该依赖抽象。
比如某个人喂养小动物,如果依赖了具体的实现,则每新增一个动物,需要在Person内加一个对应的方法。违背了开闭原则,也不符合依赖倒置原则。
重新修改后,如下。新增一个Birds抽象类,具体的动物继承自父类Birds,Person中的方法参数依赖于抽象,而不是具体的实现。符合依赖倒置原则。
单一职责原则 SRP定义:对任何类的修改只能有一个原因。换句话说,一个类只应该负责一项职责。
SRP要求每个软件模块职责要单一,衡量标准是模块是否只有一个被修改的原因。职责越单一,被修改的原因就越少,模块的内聚性就越高,被复用的可能性就越大,也更容易被理解。
举例员工类 Employee,开发工作变了,需要修改Employee类,测试工作变了需要修改Employee类,不符合单一职责原则,类的复杂性也高。
职责多,引起此类变化的原因也多。后续变更的风险就大。
后续需求变更,会造成职责的混乱,类结构的不稳定。
改造后,类的职责单一。开发者的职责就是“写代码”,那么对其进行的修改只有与“写代码”相关的一个原因(画类图也是为了指导代码落地),这样才能确保类职责的单一性原则。
同时,类与类之间虽有着明确的职责划分,但又一起合作完成任务,它们保持着一种“对立且统一”的辩证关系。
以责任链模式为例,每个处理者类职责清晰,只处理与自己职责相关的业务。
以员工类为例,拆分后,各个员工完成相应的职责,共同保障项目上线。
这种清晰的职责范围划分就是单一职责原则的最佳实践。符合单一职责原则的设计能使类具备高内聚性,让单个模块变得简单易懂,如此才能增强代码的可读性和可复用性。并提高系统的易维护性和易测试性。
上面的例子是类职责单一,那么微服务划分也同理,采用单一职责原则,每个服务负责一块业务。同一类业务的变更落在单个服务内变更。
接口隔离原则 ISP定义:客户端对类的依赖基于最小接口,而不依赖不需要的接口。
接口隔离原则认为不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口要好。做接口拆分时,也要尽量满足单一职责原则。将外部依赖减到最少,降低模块间的耦合。
比如类A只需要使用方法1、方法3,类B只需要使用方法2、方法4,但在源代码层次上与所有方法形成依赖关系。这种依赖意味着我们对接口I的方法2修改,即使不会影响A所依赖的方法1、方法3的功能,也会导致它需要重新部署和编译。
改造后,类A不需要用到方法2、方法4,就可以选择不依赖它们。代码更加清晰,接口职责更加明确。
迪米特法则 LOD定义:一个类对于其它类知道的越少越好。
迪米特法则也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、简单化模块间的通信,并达到松耦合的目的。
反之,模块之间若存在过多的关联,那么一个很小的变动则可能会引发蝴蝶效应般的连锁反应,最终会波及大范围的系统变动。我们说,缺乏良好封装性的系统模块是违反迪米特法则的,牵一发动全身的设计使系统的扩展与维护变的举步维艰。
门面模式和中介者模式是迪米特法则极好的范例。 Tomcat中 RequestFacade类就使用了外观模式。RequestFacade是对Request类封装,屏蔽内部属性和方法,避免暴露。
合成复用原则 CRP定义:优先使用合成/聚合,而不是类继承。
比如对象的继承关系是在编译时就定义好了,所以无法在运行时改变从父类继承的实现。子类的实现与它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其它更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。
合成(组合)和聚合都是关联的特殊种类。
聚合表示一种弱的拥有关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分;
合成则是一种强大的“拥有”关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。
合成/聚合复用原则好处:优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模。
举例:手机软件划分可分为QQ、微信等,按品牌划分可分为华为、小米等。如果同时考虑这两种分类,其组合就很多。往下继续扩展软件、手机品牌,都会新增许多子类。违背了开闭原则,也限制了复用性。
用聚合关系实现的类图:后面新增软件,手机品牌类不用变更代码。继承的层次也少了。
参考资料刘韬:《秒懂设计模式》
张建飞:《代码精进之路:从码农到工匠》
Robert C. Martin:《架构整洁之道》
程杰:《大话设计模式》
关注公众号,后台回复【笔记】获取技术笔记PDF。