《设计模式》学习笔记(维护中)

从上一个项目detectron2框架中,我意识到自己缺乏对大型开源项目的自顶向下的认识,这使得我在分析大型项目时,只见树木不见森林,对于逻辑关系穷根究底但是难以提炼整个系统如何运行。在大型项目中,首先需要理解整个系统流程是如何运行的,各个模块是如何通信与协作的,其次才是各个模块内部是如何编码的。

换句话说,没有一个对大型项目设计模式的理解。

为此,我学习了《设计模式》,并对其中的内容做简要的提炼和概括。后期,我可能从设计模式的角度去分析detectron2,并作为其它开源项目分析的出发点。

《设计模式》我学习的是b站李老师的这组课程,是从c艹的视角出发的,我多年没摸c艹,若有语法或思想上的错误,欢迎指出。另外,c艹和python的设计风格有不同,这门课可以学,但不能死学。23个设计模式_哔哩哔哩_bilibili

设计模式的关键点有二:可复用、面向对象。个人理解而言,是通过分析并分割系统中稳定的、变化的部分,复用稳定的、重构变化的,并灵活地根据需求来构建对象的方法。简单说,是干掉if else的方法。

从“分解”到“抽象”:大问题分解到小问题,分而治之,在大类中,对每个问题、每种情况写出对应的处理方法,是处理复杂问题的常见思路。这常常导致硬编码,需求变化则代码变化。但是忽略次要问题,先将大问题抽象为泛化的、理想化的模型,提高复用性,则是进阶的设计思路。

设计模式八大原则:设计模式-八大设计原则_笔记整理-CSDN博客_八大设计原则加点的是原文,建议对照阅读,其中我补充几点。

1. 依赖倒置原则:高层(稳定)模块不应当依赖于低层模块(变化),二者都应当依赖于抽象(稳定)。抽象不应当依赖于实现细节,实现细节依赖于抽象模块。

“依赖”指的是编译时“依赖”,换句话说,先编译A,才能再编译B,叫B依赖A。高层模块的实现依赖于抽象,实现细节依赖于抽象,将高层模块和实现细节中间以抽象隔离;代码层面上,高层模块的子功能以虚函数实现,实现细节继承自虚函数。在这个过程中,高层模块是稳定的,低层模块是变化的,但是低层模块的变化与高层模块间被抽象隔离开了。其核心是分割出一个系统中的稳定的部分组合成为高层模块,抽象出一个系统中变化的部分成为抽象模块,并在新的类中以具体的方法去重写(override)抽象的低层细节。表现出来的是,高层通过抽象去调用对应的低层。

2. 开放封闭原则:对扩展开放,对修改封闭。

避免修改源代码,需求变更时应当通过添加模块来实现。

3. 单一职责原则:一个类应当只有一个引起它变化的原因。变化的方向隐含着类的责任。

李老师说这里有坑,后续会讲。类的设计应当尽量简洁、单一,同时不失完备。

4. 里氏替换原则:子类必须能够替换它们的基类。继承表达类型抽象。

父类能执行的场合子类必须都可以直接执行。换句话说,区分“继承”和“组合”,应当是“父”包含“子”,而不是“父”相交“子”,那是组合而不是继承。

5. 接口隔离原则:不应当强迫客户程序依赖它们不用的方法。接口应当小而完备。

不要随便public,只把必要暴露给客户程序的部分public出来。这里李老师一笔带过,我猜是客户程序调用接口时,接口模块的不必要的public在编译时会引入额外开销,甚至可能因为public的bug导致编译失败。但是python主张能public的都public,所以这里我也不确定是否可以迁移过来。

6. 优先使用对象组合,而不是类继承。类继承通常为白箱复用,对象组合通常为黑箱复用。继承在某种程度上破坏了封装性,子类父类耦合度高。而对象组合则只要求被组合对象具有良好定义的接口,耦合度低。

提倡封装,提倡解耦。

7. 封装变化点。使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一层产生不良影响,从而实现层次间的松耦合。

参见1.依赖倒置原则。

8. 针对接口编程,而不是针对实现编程。不将变量类型声明为某个具体类,而是声明为某个接口。客户程序无需知道对象的具体类型,而只需要知道对象所具有的接口。减少系统中各部分的依赖关系,从而实现“高内聚,松耦合”的类型设计方案。

参见1.依赖倒置原则:从面向低层模块编程,转到面向抽象模块编程。这里接口约等于抽象模块。

李老师着重强调了标准化的面向接口编程。个人理解是,对于业务能提炼出其抽象的属性,更重要的是能制定标准去规范基于该抽象的实现细节。

最后,3个概念划分了模式的3个层次,在学习外网资料时可能会常常见到这3个词:

design idioms:设计习语,编程语言层面的特定模式和tricks。

design patterns:设计模式,类与相互通信的对象之间的组织关系,包括他们的角色、职责、协作方式等。要解决的是变化中的复用性问题。

architectural patterns:架构模式,系统与基本结构组织、子系统间的组织关系。

设计模式分类

值得注意的是,先有代码,后有设计模式,重构获得模式(refactoring to patterns)。从已有的业务逻辑中抽象出稳定的、变化的,再重构到合适的模式。

重构的技法:静态(绑定) 到 动态(绑定)。早绑定 到 晚绑定。继承 到 组合。编译时依赖 到 运行时依赖。紧耦合 到 松耦合。

从目的上看:创建型模式(creational)——对象创建,结构型模式(structural)——对象结构,行为型模式(behavioral)——多对象的交互。解决需求变化时对上述的冲击。

从范围上看:类模式处理类与子类的静态关系。对象模式处理对象间的动态关系。

in p3: 02:44,后续修改####

具体的设计模式

我希望解决的是初学的抽象理解,而不是实现细节;我需要的实现细节是python语言下对cv框架的分析,现阶段我没有能力、也没有需求做实现。以下内容是纸上谈兵,仅作理解用,若有错误,欢迎指出。

template method:模板方法

系统流程稳定,但是需要支持稳定流程中局部方法的多态。系统的执行不依赖于开发者对局部方法的重写,但是系统对于特定场景下的多态行为依赖于开发者对局部方法的重写。例如各种二次开发框架,抠个虚函数(甚至是纯虚函数)让你重写(override),或是提供一个函数指针,注意重写和重载(overload)的区别:Java—重写与重载的区别_wintershii的博客-CSDN博客_重载和重写的区别,都是多态的体现,这篇文章讨论了多态和晚绑定:C++ 虚函数 重载 重写的区别(转)_Primeprime的专栏-CSDN博客_c++ 虚函数重载;至于重构,是屎山的体现。李老师的一句话很形象:“不要调用我,让我来调用你。”开发者向框架提供特定方法的重写,但是系统的执行由框架完成,框架的开发先于应用程序的开发。过程中,框架是复用的,客制化方法是扩展的,已编译文件是不受影响的。 

strategy:策略模式

系统流程稳定,但是需要支持稳定流程中,根据不同的上下文实现局部流程的多态,或者说在运行时实现特定业务的特定策略的多态,或者说干掉if else(包括switch case)。代码上,策略strategy需要以类的指针形式创建,极少时使用引用,而不能用实例,此外需要以上下文context指导系统找到对应的策略。过程中,系统流程和已有策略是复用的,新的策略是扩展的,已编译文件是不受影响的。虽然备用的策略是“多”,但是装载的是“一”。李老师强调了“复用”,是编译后复用,而不是编译前复用,编译前复用叫代码copy。

值得注意的是模板方法和策略模式的区分:模板方法模式与策略模式的区别_varyall的专栏-CSDN博客 个人理解还有一点很关键的区别:模板方法的多态体现在开发过程中,是框架稳定时多开发者根据多种业务对局部方法的重写;而策略模式的多态体现在应用过程中,先由开发者对多种业务场景开发特定的策略,并给出特定的引导接口context,再由客户程序选择的上下文context返回到系统中,实现灵活切换策略,从而实现开发和应用的松耦合。

observer / event:观察者模式

观察者模式分为3部分,subject主题(受观者),iprogress抽象观察者(观察者接口),observer具体观察者(其中受观者推荐有自己的基类,具体观察者继承抽象观察者),受观者对于观察者接口发布通知,观察者接口处理具体观察者的增删以及通知的下达,从而实现发布者和观察者的松耦合。是多对一的监听和更新。过程中,发布者和接口是复用的,具体观察者是热插拔的。

以上3中设计模式是解决组件协作问题的,模板方法实现了编译时方法的定制,策略模式实现了运行时方法的切换,而观察者模式实现了运行时多组件根据中央组件变化时随动的变化。

更本质来说,由实方法变成虚接口,继承是虚接口的继承;系统+虚接口+实方法的结构,替代了系统+实方法的结构。

李老师有句话非常精辟:“编译时复用,运行时多态。”

另一提倡是,“面向接口编程”。二者结合应当是这些设计模式的通用思想。

decorator:装饰模式

这很容易联想到python的装饰器,这是个重点技巧。我认为也是优于直接继承或是直接组合的方法。

装饰模式分为3部分,主体方法(还有主体方法所继承的基类),装饰器基类,和具体装饰器。将装饰功能的扩展,从装饰方法的静态的功能的扩展,转变到“1.对于对象通用装饰模式的继承;2.对于对象的动态的功能实现的组合”。主干方法对于对象的变换,是稳定的;装饰器对于对象的变换,是动态的、可叠加的。那么从方法的“继承+扩展”转变到方法的“继承+组合”,将复杂装饰方法对于对象的装饰,化归成“多种简单装饰方法继承了装饰器基类对于对象的装饰,再将多种简单装饰方法对于对象的装饰组合在一起”。表现上,是继承了装饰器基类(继承),输入了上一个装饰器(组合)。同时表现出“is-a"和“has-a”的特性。其关键点有二:1.抽象出通用装饰模式;2.将复杂装饰模式分解成功能单一的装饰模式。从编译时装配,转变到运行时装配。

bridge:桥模式

GoF根据业务习惯,将抽象的约等于业务的,实现的约等于平台的,例子中,无论什么平台的同质项目,总有“登录”等通用功能,但是在不同平台实现功能有不同的业务逻辑和实现方法。表现起来是public抽象的共同的属性,protected实现的具体的类的指针。

这块我觉得挺难懂的,特别是多维变化方向的桥模式是如何叠加的,查了一下网上的说法也不爱说人话。我认为从桥模式的核心特征出发:分离“抽象”和“实现”——实际上就是分了两片叶子出来,至于多维延伸的桥模式,无非是其“实现”的叶子节点又出现了更低层次的“抽象”和“实现”,或者是从不同级的父节点去分出新的“抽象”+“实现”。说人话就是,桥模式构建了一族二叉树,根节点是base,叶子节点1是“抽象”,叶子节点2是“实现”。当“实现”中有新的抽象元素时,叶子节点2“实现”成为下一级的父节点。代码层面上,public的是抽象,protected是实现(下一级类的指针)。实例化时是反向的,叶子在内,层层嵌套,替换父节点所protected的类的属性细节,最后到根节点。

以上装饰模式和桥模式都是单一职责原则的体现。

若我理解错误,贻笑大方,烦请不吝赐教。

未完待续####

你可能感兴趣的:(设计模式,设计模式,c++)