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


上述这个例子就是一个违反LSP的典型场景,虽然在约定好的前提下,程序可以运行正确,但是如果有客户端不小心破坏了这个约定,就会带来程序行为异常(我们永远无法预知客户端的所有行为) 。
要纠正这个问题也很简单,就是去掉这一层抽象,让.等工厂方法的入参声明为具体的配置类,比如可以这么实现:
// pipeline插件工厂type PipelinePluginFactory struct {}func (p *PipelinePluginFactory) Create(config *config.Pipeline) plugin.Plugin {... // pipeline插件实例化过程}
从上述几个例子中,我们可以看出遵循LSP的重要性,而设计出符合LSP的软件的要点就是,根据该软件的使用者行为作出的合理假设,以此来审视它是否具备有效性和正确性 。
ISP:接口隔离原则
接口隔离原则(The,ISP)是关于接口设计的一项原则,这里的“接口”并不单指Java或Go上使用声明的狭义接口,而是包含了狭义接口、抽象类、具象类等在内的广义接口 。它的定义如下:
not betoonit does not use.
也即,一个模块不应该强迫客户程序依赖它们不想使用的接口,模块间的关系应该建立在最小的接口集上 。
下面,我们通过一个例子来详细介绍ISP 。
上图中,、、都依赖了,但实际上,只需使用.func1方法,只需使用.func2,只需使用.func3,那么这时候我们就可以说该设计违反了ISP 。
违反ISP主要会带来如下2个问题:
增加模块与客户端程序的依赖,比如在上述例子中,虽然和都没有调用func1,但是当修改func1还是必须通知~3,因为并不知道它们是否使用了func1 。产生接口污染,假设开发的程序员,在写代码时不小心把func1打成了func2,那么就会带来的行为异常 。也即被func2给污染了 。
为了解决上述2个问题,我们可以把func1、func2、func3通过接口隔离开:
接口隔离之后,只依赖了,而上只有func1一个方法,也即不会受到func2和func3的污染;另外,当修改func1之后,它只需通知依赖了的客户端即可,大大降低了模块间耦合 。
实现ISP的关键是将大接口拆分成小接口,而拆分的关键就是接口粒度的把握 。想要拆分得好,就要求接口设计人员对业务场景非常熟悉,对接口使用的场景了如指掌 。否则孤立地设计接口,很难满足ISP 。
下面,我们以分布式应用系统demo为例,来进一步介绍ISP的实现 。
一个消息队列模块通常包含生产()和消费()两种行为,因此我们设计了Mq消息队列抽象接口,包含和两个方法:
// Mq 消息队列接口type Mq interface {Consume(topic Topic) (*Message, error)Produce(message *Message) error}// 当前提供MemoryMq内存消息队列的实现type MemoryMq struct {...}func (m *memoryMq) Consume(topic Topic) (*Message, error) {...}func (m *memoryMq) Produce(message *Message) error {...}
当前demo中使用接口的模块有2个,分别是作为消费者的和作为生产者的:
type MemoryMqInput struct {topicmq.Topicconsumer mq.Mq // 同时依赖了Consume和Produce}type AccessLogSidecar struct {socketnetwork.Socketproducer mq.Mq // 同时依赖了Consume和Producetopicmq.Topic}
从领域模型上看,Mq接口的设计确实没有问题,它就应该包含和两个方法 。但是从客户端程序的角度上看,它却违反了ISP,对来说,它只需要方法;对来说,它只需要方法 。
一种设计方案是把Mq接口拆分成2个子接口和,让直接实现和:
// Consumable 消费接口,从消息队列中消费数据type Consumable interface {Consume(topic Topic) (*Message, error)}// Producible 生产接口,向消息队列生产消费数据type Producible interface {Produce(message *Message) error}