三月份的另一个任务是李建忠老师讲解的“设计模式”这门课,整体上过了一遍,对23重设计模式也有了一个整体认识,大概了解了每个模式及其适用场景,这门课一个非常好的地方就是对每个模式讲解前先按照不考虑软件系统扩展性的思路写一份强耦合的代码,然后对其存在的问题进行分析,最后自然而然地引出相应的设计模式,这其实也是我们写代码时的一个过程,一般完美的软件架构都是不断迭代重构才实现的。这篇文章主要记录我对每个模式的一个学习笔记和思考。主要分为两大部分:八大设计原则和模式分类及简介。
1. 设计模式————八大设计原则这些设计原则的目的是为了适应软件的更新迭代,以尽可能便于维护、便于理解的方式设计软件架构。此外,在C++语言中,设计模式和OOP思想是相辅相成的,继承和多态是设计模式的基础。以下的八大设计原则要比设计模式更加重要。
- 依赖倒置原则(DIP)
①高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象;
②抽象不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定);
总结:这个原则是贯穿23种设计模式的,意思是说一个类B去依赖另一个类继承体系B的时候,我们应该去依赖B的基类(抽象类),而不应该去依赖B中的子类(派生类),因为子类是变化的,如果版本迭代增加了新的B的子类,那么A的依赖代码也需要修改。但如果我们依赖的是B的基类,那么通过多态调用也可以调到新增的B的子类而无需修改A类的代码。 - 开放封闭原则(OCP)
①对扩展开放,对更改封闭;
②类模块应该是可扩展的,但是不可修改的。
总结:在面对一些新需求时,我们应该考虑如何扩展(增加)原来的代码来达到要求,而不是对原来的体系进行重写,这样能保证之前已经编译测试通过的代码不会被破坏。所以我们设计的软件架构体系应该能适应这种扩展。在写类结构体系时,应该考虑到新增需求如何扩展的问题,这就需要设计有完备接口的抽象基类被其他类来依赖,从而保证版本迭代尽可能少地改动之前地代码。 - 单一职责原则(SRP)
①一个类应该仅有一个引起他变化的原因
②变化的方向隐含着类的职责
总结:设计一个类应该明确它地责任范围,不应该去做超出范围地工作。但只有一个引起它变化地原因可能不太现实,目前有些类处理的流程可能比较复杂,会涉及很多变化和状态,但责任范围一定是明确的,这样不会导致类功能混乱,不易于管理。至于变化的方向隐含着类的职责,其实就体现在类地成员函数上,这些成员函数就是类的职责功能,而类成员数据则表现着实例对象的状态。 - Liskov替换原则(LSP)
①子类必须能够替换它们的基类(IS-A)。
②继承表达类型抽象。
总结:简单来说就是任何可以传递基类指针的地方,都可以传一个子类指针。这也是多态调用能够正常工作的必要条件,子类和基类应该时is a
的关系,子类拥有所有基类拥有的特性(包括继承的和重写的),所以可以替换。如果违背了这个原则,那么这两个类本就不应该是继承的关系,可能是应该组合的。 - 接口隔离原则(ISP)
①不应该强迫客户程序依赖它们不用的方法
②接口应该小而完备
总结:在写一个类的时候,应该把主要的接口函数写为public,不必要函数尽量写到private中,防止客户程序依赖private中的方法,导致耦合严重,不利于维护。成员函数的可访问等级有三个,设置的基本原则是:允许客户代码来访问的接口public,允许子类使用的protected,之作自身类内部使用的private,成员数据一般也应该private。 - 优先使用对象组合,而不是类继承
①类继承通常为“白箱复用”,对象组合通常为“黑箱复用”
②继承在某种程度上继承了很多基类的函数,破坏了封装性,子类父类的耦合度过高。
③使用对象组合只要求被组合的对象具有良好的接口定义即可
总结:我的理解是不同体系的类之间一定不要使用继承,应该使用组合;使用继承的类,子类和父类中定义函数应该考虑其意义是否符合类定义的初衷,不应只考虑符合语法规则就盲目定义函数,防止破坏封装性。 - 封装变化点
①使用封装来创建对象之间的分界点,让设计者可以在分界点的一侧进行修改,而不会对另一侧产生不良影响,实现对象之间的松耦合。
总结:对象之间尽可能地解耦,不要允许彼此相互调用类或者数据进行增删改查。这样分别进行类设计不会对其他类产生影响,这个思想更多的体现在软件架构地设计中,每个模块各司其职,不要去做其他模块的工作或者修改其他模块的数据,模块与模块之间拥有确定的通讯函数接口,即分界点,能有效地避免代码混乱,便于维护。 - 针对接口编程,不要针对实现编程
①不将变量类型声明为某个特定的具体类,而声明为某个接口类;
②客户程序无需知晓对象的具体类型,只需要知道对象所具有的接口即可;
③减少系统中各部分的依赖关系,从而实现“高内聚、低耦合”的类设计方案。
总结:不仅依赖的类继承体系应该依赖抽象基类(只需要直到对象的接口即可),而且实现类的数据成员也可以是一些业务相关的接口类类型,在初始化的时候再指定为某个特定的实现类。减少系统中各部件的依赖关系,尽量不产生耦合,但肯定是需要有交互的地方,这些地方需要尽可能稳定。
接口标准化非常重要,我们写C++代码是面向对象编程,但在写对象之前,我们需要先确定好软件的整体架构,设计不同模块之间的调用逻辑,然后为每个模块设计完备的接口类定义(抽象基类),每个模块或许有一个或多个接口类定义,也会有一些全局性的接口类定义会用在很多模块中,此外不同类之间也会进行组合或继承来构建新的类(表现出一系列设计模式),这一套接口类包含很多类型,我们再去实现不同抽象类的子类,子类也有可能是抽象类,会继续派生。
各个抽象类之间依赖的时候我们只依赖抽象基类,这样可以保证这种依赖是稳定的,而实现类(可实例化的子类)的内部修改不会对上层接口调用产生影响,不需要修改上层代码,基于接口编程的优势就在于此。而且这样一来,不同模块的开发任务还可以由多人协作完成。
另外在切换模块算法的时候,我们可继承之前设计的接口派生新的子类来设计新的算法,只需要将旧算法实例化的地方改为新算法即可,有利于软件算法切换和版本迭代。
三层软件设计经验
① 设计习语:描述与特定编程语言相关的底层模式,技巧与惯用法,如《effective C++》中提出的那些建议。
② 设计模式:主要描述“类与相互通信的类对象之间的组织关系,包括他们的角色、职责和协作方式等方面。
③ 架构模式:描述系统中与基本结构组织关系密切的高层模式,包括子系统划分、职责分配,以及如何组织它们之间关系的规则。
以上这三方面是三个层次的设计思想,由低到高需要一一掌握,在这里设定一个目标吧,今年年底写一套自己的深度学习推理框架,在这个过程中,肯定是会涉及这三个层次的知识点,实践出真知!!!