架构师训练营 - 第 2 课总结 -20200613- 软件设计
框架
如:MFC,AWT,SPRING,TOMCAT. 实现了多种设计模式,简化了一般开发工作,只需合法调用API即可使用.
工具与框架: log4j是工具,junit是框架. 工具用来被调用,框架用来调用程序.
框架的核心: 好莱坞原则- 框架调用程序,而不是反过来.
软件设计的臭味与目标
软件设计通用目标:强内聚,松耦合
软件设计臭味:一动就坏,改也难改. 有IF ELSE.
*面向接口设计.
*无多态无OOD.
1. 六大原则(SOLID)
以下引用至<设计模式之禅> - SOLID介绍
单一职责原则(SRP)
开闭原则(OCP)
里氏替换原则(LSP)
迪米特法则(LOD)
接口隔离原则(ISP)
依赖倒置原则(DIP)
1)开闭原则(OCP: Open-Closed Principle)
定义:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。遵循开闭原则的系统设计,可以让软件系统可复用,并且易于维护。这也是系统设计需要遵循开闭原则的原因:
1.稳定性。开闭原则要求扩展功能不修改原来的代码,这可以让软件系统在变化中保持稳定。
2.扩展性。开闭原则要求对扩展开放,通过扩展提供新的或改变原有的功能,让软件系统具有灵活的可扩展性。
抽象约束。首先通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型,引用对象尽量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定,一旦确定既不允许修改。
元数据(metadata)控制模块行为。尽量使用元数据(用来描述环境和数据的数据,通俗的说就是配置参数)来控制程序的行为,减少重复开发。
制定项目章程。对于项目来说,约定优于配置。
封装变化。第一,将相同的变化封装到一个接口或抽象类中;第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。
2)依赖倒置(DIP:Dependence Inversion Principle) ???
定义:抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
依赖倒置原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。在项目中使用,我们只要遵循以下几个规则就可以.
每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。
变量的表名类型尽量是接口或者抽象类。
任何类都不应该从具体类派生。
尽量不要覆写基类的方法。
结合里氏替换原则使用。
面向业务定义接口.
与策略模式互通
3)里氏替换原则(LSP:Liskov Substitution Principle)
定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
里氏替换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立. 在场景中查看,而不是仅在逻辑层面.
里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
子类必须完全实现父类的方法。
子类可以有自己的个性。
覆盖或实现父类的方法时输入参数可以被放大。
覆写或实现父类的方法时输出结果可以被缩小。
版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。
4)单一职责原则(SRP:Single responsibility principle)
定义:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
可以降低类的复杂度,实现什么职责都有清晰明确的定义;
提高类的可读性;
提高系统的可维护性;
变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,一个接口修改只对相应地实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
5)迪米特法则(LoD:Law of Demeter)
定义:一个软件实体应当尽可能少地与其他实体发生相互作用。
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
在类的设计上,只要有可能,一个类型应当设计成不变类;
在对其他类的引用上,一个对象对其他对象的引用应当降到最低。
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。我们在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。
6)接口隔离原则(ISP:Interface Segregation Principle)
定义:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
看到这里好像接口隔离原则与单一职责原则是相同的。其实接口隔离原则与单一职责原则的审视角度是不相同的,单一职责原则要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。
接口要尽量少。
接口要高内聚。
定制服务。
接口设计师有限度的。
一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。
接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装。但是,这个原子该怎么划分是设计模式中的一大难题,在实践中可以根据以下几个规则来衡量.
一个接口只服务于一个子模块或业务逻辑。
通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法。
已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
了解环境,拒绝盲从。 环境不同,接口拆分的标准就不同,深入了解业务逻辑,根据实际情况设计接口。
2. 几种设计模式
以下引用至菜鸟教程https://www.runoob.com/design-pattern/
1)策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。(多态)
2)观察者模式
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
如何解决:使用面向对象技术,可以将这种依赖关系弱化。
关键代码:在抽象类里有一个 ArrayList 存放观察者们。 (List of Listeners)
3)适配器模式
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
我们通过下面的实例来演示适配器模式的使用。其中,音频播放器设备只能播放 mp3 文件,通过使用一个更高级的音频播放器来播放 vlc 和 mp4 文件。
意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
何时使用: 1、系统需要使用现有的类,而此类的接口不符合系统的需要。 2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。 3、通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
如何解决:继承或依赖(推荐)。
关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
版权声明: 本文为 InfoQ 作者【👑👑merlan】的原创文章。
原文链接:【http://xie.infoq.cn/article/2ec273d1c99330d62e119acbf】。文章转载请联系作者。
评论