-->

后端面试38讲_13_11丨软件设计的开闭原则如何不修改代码却能实现需求变更

你好,我是李智慧。

我在上篇文章呢讲到软件设计应该为需求变更而设计,应该能够灵活快速的满足需求变更的要求。

优秀的程序员呢也应该欢迎需求变更。

因为持续的需求变更,意味着自己开发的软件保持活力,同时也意味着自己为软件需求变更而进行的设计有了用武之地。

这样的话技术和业务都进入了良性循环。

但是呢需求变更就意味着原来开发的功能需要改变,也意味着程序需要改变。

如果是通过修改代码实现需求变更,那么代码一定会在不断改变的过程中变得面目全非,也意味着代码的腐化。

那有没有办法不改变代码却能够实现需求变更呢?这个要求听起来有点玄幻,事实上却是软件设计需要遵循的最基本的原则,开闭原则。

开闭原则说,软件实体应该对扩展是开放的对,修改是关闭的对,扩展是开放的,意味着软件实体的行为是可扩展的当需求。

变更的时候,可以对模块进行扩展,使其满足需求变更的要求。

对修改是关闭的,意味着对软件实体进行扩展的时候啊,不需要改动。

当前的软件实体不需要修改代码。

对于已经完成的类文件呢,不需要重新编辑。

对于已经编辑打包好的模块呢,不需要再重新编译。

通俗的说呢就是软件功能可以扩展,但是呢软件实体不可以被修改,功能要扩展,软件又不能修改,似乎是自相矛盾的。

怎样才能做到不修改代码和模块却能实现需求变更呢?在开始讨论前,让我们先看一个反面的例子。

假设我们需要设计一个可以通过按钮拨号的电话,核心对象是按钮和拨号器那么简单的设计。

可能是这样的按钮类关联一个拨号器类。

当按钮按下的时候呢,调用拨号器相关的方法按钮在创建的时候啊可以创建数字按钮或者是发送按钮。

执行按钮的press方法的时候,会调用拨号器diya的相关方法,这个代码能够正常运行,完成需求设计似乎也没有什么问题,这样的代码实现我们司空见惯。

但是它的设计呢违反了KB原则。

当我们想要增加按钮类型的时候,比如当我们需要按钮支持信号和井号的时候,我们必须修改button的类代码。

当我们想要用这个按钮控制一个密码锁而不是拨号器的时候,因为按钮关联拨号器,所以依然需要修改特类的代码。

当我们想要按钮控制多个设备的时候,还是要修改bubutton类代码,似乎对button类做任何的功能扩展都要修改button类,这显然违反了开闭原则。

对功能扩展是开放的对代码修改是关闭的。

违反KB原则的后果是这个buta类非常僵硬。

当我们想进行任何需求变更的时候,都必须要修改代码。

同时我们需要注意到大段的switch case代码是非常脆弱的。

因当需要增加新按钮类型的时候,需要非常谨慎的,在这段代码中找到合适的位置。

因为稍不小心就可能会出现bug粗暴一点的说,当我们在代码中看到else或者是switch case关键字的时候,基本可以判断违反KB原则了。

而且这个button类也是难以复用的。

Button类强耦合的一个dialer类,在脆弱的switch case代码段耦合调用了dialoer的方法。

即使button类自身也将各种按钮类型耦合在一起。

当我想复用这个button类的时候,不管我需不需要一个stand按钮,button类都自带着这个功能。

所以这样的设计不要说不修改代码就能够实现功能扩展。

即使我们想修改代码进行功能扩展,里面也很脆弱,稍不留省损否掉到坑里了。

这个时候你再回头审视tton ton的设,是不是就能感觉到代码里面腐坏的味道。

如果让你接着维护这样的代码实现需求变更,是不是头疼难受?很多设计开始看啊,并没有什么问题。

如果开发出来的软件永远也不需要修改,也许怎么设计都可以。

但是当需求变更来的时候,就会发现各种僵硬、脆弱,所以设计的优劣需要放入需求变更的场景中进行考察。

当需求变更时,发现当前设计的腐坏就要及时进行重构,保持设计的强壮和代码的干净。

设计模式中的很多模式呢,其实都是用来解决软件的扩展性问题的,也是符合开闭原则的。

我们用策略模式对上面的例子进行重新设计。

我们在button和diller之间增加一个抽象接口,button server button依赖button server,而dialer的实现button server.当button按键的时候,就调用button server的button press的方法,事实上是调用dler实现的button press的方法,这样既完成了button按钮,按下时执行dler方法的需求,又不会使button依赖dler button可以扩展复用到其他需要使用button的场景。

任何实现button server的类,比如密码锁都可以使用button,而不需要对button代码进行任何修改。

而且button也不需要switch case代码去判断当前的按钮类型,只需要将按钮类型token传递给button server就可以了。

这样增加新的按钮类型的时候啊,就不需要修改button代码了。

策略模式是一种行为模式,多个策略实现同一个策略接口。

编程的时候,clean的程序依赖策略接口,用行机根据不同的上下文ccleent程序传入不同的策略实现。

在我们这个场景中,clean程序就是button策略,就是需要用button控制的目标设备,拨号器、密码锁等等。

Button server就是策略接口。

通过使用策略模式,我们使button类实现了开闭原则。

Button符合开闭原则了,但是dialer又不符合开闭原则了,因为dialer要实现button server接口,根据参数token决定执行enter digit方法还是dialer方法,用需didialer或者switch case不符合开闭原则,那怎么办呢?这种情况可以使用适配器模式进行设计。

适配器模式是一种结构模式,用于将两个不匹配的接口适配起来,使其能够正常工作。

也就是说,不要有dialer类直接实现button server接口,而是增加两个适配器digit button didialer adapter stad button dialot adapter,由适配器实现button server接口。

在适配器的button press的方法中调用的dialoenter digit方法和dialer方法dialot保持原变didialer类实现adb原则。

在我们这个场景中呢,button需要调用的接口是button, pressed和dialog的方法不匹配。

如何在不修改data代码的前提下,使button能够调用dla代码,靠的就是适配器适配器degct button dialer adapter和sand button dialer adapter,实现了button server接口。

使button能够调用自己,并在自己的button press的方法中调用dialer的方法,适配了dialer.通过策略模式和适配器模式,我们是button和dialer都符合了开闭原则。

但是,如果要求能够用一个按钮控制多个设备,比如按钮按下进行拨号的同时还要扬声器,根据不同按钮发出不同的声音,将来还需要不同按钮点亮不同颜色的灯。

根据当前设计,可能需要在适配器中调用多个设备,增加设备,要修改适配器代码,又不符合开闭原则了,怎么办?这种情况可以用观察者模式进行设计。

Button server被改名为button lesteson来表表示这是个监听者器口,其表这个个改名不重,仅仅仅为了便识识别其因,如果要求bubutton price的不变,but listener和button server本质上是一样的,最后的是在button类里增加了成员变量list和成员方法。

Add listener通过add listener,我们可以添加多个需要观察按钮,按下世界的监听者实现。

当按钮需要控制新设备的时候,只需要将实现了button listener的设备实现添加到button的list列表就可以了。

观察者模式是一种行为模式,解决了一对多的对象依赖关系,将被观察者对象的行为通知到多个观察者,也就是监听者对象。

在我们这个场景中呢,button是被观察者目标设备、拨号器、密码锁等等,是观察者。

被观察者和观察者通过listener接口结耦合观察者的适配器,通过调用被观察者的add listener方法将自己添加到观察列表。

当观察行为发生时,被观察者会逐个遍历list的listener通知观察者。

如果业务要求按下按钮的时候,除了控制设备,按钮本身,还需要执行一些操作,完成一些成员变量的状态,改变不同按钮类型进行的操作和记录,状态各不相同。

按照当前设计,可能又要在button的press方法中增加switch case了。

怎么办?这种情况可以用模板方法模式进行设计。

在button类中定义抽象方法on press具体类型的按钮,比如send button实现这个方法。

Button类中增加抽象方法on press,并在press方法中调用on press方法。

根据模板方法模式,就是在负类中用press方法并在计算的骨价和过程,而抽象方法的实现则留在子类中。

在我们这个例子中,press方法就是模板。

Press方法除了调用抽象方法on press,还执行通知监听者列表的操作。

这些抽象方法和具体操作共同构成了模板。

而在子类stand button中实现了这个抽象方法,在这个方法中修改状态,完成自己类型特有的操作。

这就是模板方法模式。

通过模板方法模式,每个子类可以定义自己在press执行时的状态操作,无需修改button类,实现了开闭原则。

实现开辟原则的关键是抽象。

当一个模块依赖的是一个抽象接口的时候,就可以随意对这个抽象接口进行扩展。

这个时候啊不不需要对现有的代码进行任何修改,利用接口的多态性,通过增加一个新实现该接口的实现类能够完成需求变变更,不同景进行扩展的方法是不同的,这个时候就会产生不同的设计模式,大部分的设计模式都是用来解决扩展的灵活性问题的开辟原则可以说是软件设计原则的原则是软件设计的核心原则。

其他的设计原则更偏向技术性,具有技术性的指导意义,而开辟原则是方向性的。

在软件设计的过程中,应该时刻以开辟原则指导审视自己的设计。

而需求变更的时候,现在的设计是否能够不修改代码就可以实现功能的扩展。

如果不是,那么就应该进一步的使用其他的设计原则和设计模式,去重新设计更多的设计原则和设计模式。

我将在后续陆续讲解。

我在观察者模式小节展示的phone代码示例中并没有显示的定义degct button dialer adapter和stand button diala adapter这两个适配起来,但他们是存在的在在哪里?也欢迎在评论区写下你的思考,我会和你一起交流。

也欢迎把这篇文章分享给你的朋友,或者同事一起交流一下嗯。