【Go实现】实践GoF的23种设计模式:SOLID原则( 五 )


最后,的实现如下,只依赖于、和三个抽象接口 。后续再有需求变更,只需扩展对应的接口即可,无须再变更:
// demo/monitor/pipeline/pipeline_plugin.go// ETL流程定义type pipelineTemplate struct {inputinput.Pluginfilterfilter.Pluginoutputoutput.Plugin...}func (p *pipelineTemplate) doRun() {...for atomic.LoadUint32(&p.isClose) != 1 {event, err := p.input.Input()event = p.filter.Filter(event)p.output.Output(event)}...}
OCP是软件设计的终极目标,我们都希望能设计出可以新增功能却不用动老代码的软件 。但是100%的对修改封闭肯定是做不到的,另外,遵循OCP的代价也是巨大的 。它需要软件设计人员能够根据具体的业务场景识别出那些最有可能变化的点,然后分离出去,抽象成稳定的接口 。这要求设计人员必须具备丰富的实战经验,以及非常熟悉该领域的业务场景 。否则,盲目地分离变化点、过度地抽象,都会导致软件系统变得更加复杂 。
LSP:里氏替换原则
上一节介绍中,OCP的一个关键点就是抽象,而如何判断一个抽象是否合理,这是里氏替换原则(The,LSP)需要回答的问题 。
LSP的最初定义如下:
If for eacho1 of type S there is ano2 of type T such that for allPin terms of T, theof P iswhen o1 isfor o2 then S is aof T.
简单地讲就是,子类型必须能够替换掉它们的基类型,也即基类中的所有性质,在子类中仍能成立 。一个简单的例子:假设有一个函数f,它的入参类型是基类B 。同时,基类B有一个派生类D,如果把D的实例传递给函数f,那么函数f的行为功能应该是不变的 。
由此可以看出,违反LSP的后果很严重,会导致程序出现不在预期之内的行为错误 。最典型的就是正方形继承自长方形的例子 。
长方形和正方形例子的详细介绍,请参考【Java实现】实践GoF的23种设计模式:SOLID原则 中的“LSP:里氏替换原则”一节
出现违反LSP的设计,主要原因还是我们孤立地进行模型设计,没有从客户端程序的角度来审视该设计是否正确 。我们孤立地认为在数学上成立的关系(正方形 IS-A 矩形),在程序中也一定成立,而忽略了客户端程序的使用方法 。
该例子告诉我们:一个模型的正确性或有效性,只能通过客户端程序来体现 。
下面,我们总结一下在继承体系(IS-A)下,要想设计出符合LSP的模型所需要遵循的一些约束:
基类应该设计为一个抽象类(不能直接实例化,只能被继承) 。子类应该实现基类的抽象接口,而不是重写基类已经实现的具体方法 。子类可以新增功能,但不能改变基类的功能 。子类不能新增约束,包括抛出基类没有声明的异常 。
前面的矩形和正方形的例子中,几乎把这些约束都打破了,从而导致了程序的异常行为:1)的基类不是一个抽象类,打破约束1;2)重写了基类的和方法,打破约束2;3)新增了没有的约束,长宽相等,打破约束4 。
因为Go天然就不支持继承,实现多态只能通过接口的方式,所以,对Go语言来说,上述的约束1~3其实已经满足了:1)接口本身不具备实例化能力,满足约束1;2)接口没有具体的实现方法,也就不会被重写,满足约束2;3)接口本身只定义了行为契约,并没有实际的功能,因此也不会被改变,满足约束3 。
虽然Go语言中使用接口替代继承来实现多态和抽象,能够减少很多不经意的错误 。但是面向接口设计仍然需要遵循约束4,下面我们以分布式应用系统demo为例,介绍一个比较隐晦地打破约束4,从而违反了LSP的实现 。
还是以监控系统为例,为例实现ETL流程的灵活配置,我们需要通过配置文件定义的流程功能(数据从哪获取、需要经过哪些加工、加工后存储到哪里) 。当前需要支持json和yaml两种配置文件格式,以yaml配置为例,配置内容是这样的: