设计原则之开闭原则:类设计核心原则开闭原则原理解读
做面向对象编程的同学,应该都听过设计原则这回事。最常被提起,也被叫做核心原则的,就是开闭原则。
很多新手刚接触这个概念的时候,会觉得它说起来简单,真要用的时候又摸不着头绪。今天我们就把这个原则掰开揉碎,说说它到底是什么,为什么它能被叫做类设计的核心,日常开发里我们又该怎么用。
先讲最基础的定义,开闭原则的官方说法其实很简单:一个软件实体,比如我们写的类、模块、函数,应该对扩展开放,对修改关闭。
刚看到这句话你可能会懵,既要不让改原来的代码,又要加新功能?这不是矛盾吗?
其实你可以这么理解,我们做项目的时候,需求肯定会一直变。今天要加个新的支付方式,明天要改个报表的导出格式,后天要换个推送渠道。如果我们每次加新功能,都去改原来已经写好、测试好的核心代码,改着改着就会出问题。原来好好的功能,可能因为你改了一行代码出bug,还要重新走一遍全量测试,浪费时间又风险高。
开闭原则要解决的就是这个问题。它想让我们加新功能的时候,不用改原来能用的老代码,只需要加新的类或者新的模块就行了。
举个很生活化的例子,你家里装了一个插线板,原来上面插了电视机、冰箱、台灯,都用得好好的。现在你买了个新的投影仪,不需要拆了插线板重新接线,直接把投影仪插在空的插孔里就能用。这个插线板其实就符合开闭原则——加新设备不用改原来的结构,扩展新设备很方便,原来的接线也不用动。
放到代码里举个实际的例子你就更懂了。比如我们做一个电商系统,最开始只支持微信支付,你写了一个支付类,所有支付逻辑都写在里面。
后来业务要加支付宝支付,你直接去原来的支付类里加了一个if判断,如果是支付宝就走支付宝的逻辑。再过一阵要加银行卡支付,你又加个else if。慢慢这个类就会变得又大又乱,每次改都可能碰错原来微信支付的逻辑,测试起来也麻烦。
如果按照开闭原则来设计呢?我们会先抽出一个通用的支付抽象接口,里面只定义一个pay方法,不管什么支付方式都要实现这个方法。原来的微信支付,就写一个微信支付类实现这个接口,支付宝支付就是支付宝类,以后加银行卡支付,直接加个新的类实现接口就行,原来的接口代码和微信支付的代码一点都不用改。
这样一来,原来测试好的代码不会被触碰,出bug的概率直接降了很多,加新功能也只需要测试新写的类就行了,效率高很多。
那为什么说开闭原则是类设计的核心原则?其他设计原则,比如单一职责、里氏替换、依赖倒置这些,其实都是为了实现开闭原则服务的。
比如单一职责要求一个类只做一件事,就是为了方便你后面扩展。如果一个类塞了好几个功能,你要加新逻辑的时候,就只能改这个类,根本没法分开扩展。
再比如依赖倒置,告诉我们要依赖抽象不要依赖具体实现,其实本质也是为了让你扩展的时候不用改上层代码。就像刚才支付的例子,上层业务只依赖支付这个抽象接口,不管你加多少种新的支付方式,上层业务代码都不用改,这不就是对修改关闭吗?
很多人会说,那我完全不修改原来的代码,根本做不到吧?毕竟有些老需求变更,原来的代码确实有问题啊。
其实开闭原则不是说绝对一点都不能改原来的代码,它是一种设计思想,让我们尽量把会变化的部分抽出来,把稳定不变的部分固定住。后续变的都是抽出来的扩展部分,稳定的核心部分就不用动了。
如果真的是原来代码有bug,那肯定要改原代码,开闭原则不是不让改bug,是不让你在加新功能的时候随便改已经稳定的核心代码。
日常写代码的时候,想要用好开闭原则,其实也不难,记住几个简单的方向就行。
首先,提前预判变化。拿到一个需求的时候,先想想哪些地方以后大概率会变。比如支付方式肯定会加,报表导出格式以后说不定会变,推送渠道肯定会扩展。把这些会变的部分抽出来做成抽象,不要把所有逻辑都写死在一个类里。
当然也不用过度设计,没必要现在啥需求都没有,就提前抽象好七八种扩展,那反而会把代码搞复杂。一般来说,当你知道这块大概率会变,或者已经变过一次了,这个时候再抽离抽象也不晚,符合演进式设计的思路就好。
其次,依赖抽象不依赖具体。让核心的稳定代码依赖抽象接口,具体的实现都放到子类里去做,这样加新实现的时候,核心代码不用动。
还有,多用组合少用继承。继承其实是刚性比较强的结构,父类改了子类全受影响,想要加新的变体,只能不断加子类,有时候还会出现很多只用一次的子类,把继承树搞的乱七八糟。用组合的话,我们可以把不同的行为抽成接口,动态替换行为,扩展起来更灵活,也更容易符合开闭的要求。
我见过很多新手,一开始觉得开闭原则是老生常谈的东西,没必要太在意,等项目做了大半年,代码改到不敢下手,每次加个小功能都要翻三四个旧类改十几处代码,才会发现这个原则的好处。
其实它本质就是帮我们控制代码变化的影响范围,把变化隔离在外面,不要让新需求的变化蔓延到整个项目的核心代码里。时间久了,项目就能一直保持比较清晰的结构,不会改着改着就变成一团乱麻。
总的来说,开闭原则不是一个要你死记硬背的规则,它是一种让代码保持灵活可维护的设计思路。不用追求每次写代码都百分之百符合,只要你写类的时候多想想,这块以后会不会变,能不能把变化的地方分开,让加新功能的时候少改点老代码,慢慢就能用好这个核心原则了。
设计原则,开闭原则,类设计,面向对象编程,软件设计,开闭原则原理,核心设计原则,对扩展开放对修改关闭,代码设计,设计模式
[Q]:什么是开闭原则?
[A]:开闭原则是类设计的核心设计原则,官方定义是软件实体(类、模块、函数)应对扩展开放,对修改关闭,也就是新增功能时尽量添加新代码,而非修改已经测试稳定的原有代码。
[Q]:开闭原则为什么要要求对修改关闭?
[A]:修改已经稳定运行、测试完成的原有代码,很容易引入新bug,还需要重新进行全量测试,会增加开发成本和风险,对修改关闭可以降低这种风险。
[Q]:对扩展开放和对修改关闭矛盾吗?
[A]:并不矛盾,开闭原则是通过抽离抽象,让新增功能通过新增实现类/模块来完成,不需要修改原有核心代码,既实现了功能扩展,又不需要改动原有代码。
[Q]:开闭原则在代码里具体怎么体现?
[A]:比如开发支付功能,可以先抽离通用的支付抽象接口,每种支付方式做单独的实现类,新增支付方式只需要添加新的实现类,不需要修改原有的支付核心代码,这就是符合开闭原则的设计。
[Q]:开闭原则为什么是类设计的核心原则?
[A]:其他设计原则,比如单一职责、依赖倒置、里氏替换原则,本质都是为了帮我们实现开闭原则服务,最终目标都是让代码更容易扩展,减少不必要的原有代码修改。
[Q]:开闭原则要求绝对不能修改原有代码吗?
[A]:不是的,开闭原则是一种设计思想,不是绝对规则,修复原有代码的bug是允许修改的,它主要限制的是新增功能时随意修改稳定核心代码的行为。
[Q]:日常开发怎么用好开闭原则?
[A]:首先提前预判会变化的业务点,抽离变化部分做抽象;其次让代码依赖抽象而非具体实现;另外多用组合少用继承,同时避免过度设计,不需要在没有需求的时候提前做无用扩展。
[Q]:开闭原则能解决什么问题?
[A]:开闭原则可以控制需求变更的影响范围,把变化隔离在扩展模块,避免新需求的修改蔓延到项目核心代码,让项目长期保持可维护性,降低新增功能的开发和测试成本。
很多新手刚接触这个概念的时候,会觉得它说起来简单,真要用的时候又摸不着头绪。今天我们就把这个原则掰开揉碎,说说它到底是什么,为什么它能被叫做类设计的核心,日常开发里我们又该怎么用。
先讲最基础的定义,开闭原则的官方说法其实很简单:一个软件实体,比如我们写的类、模块、函数,应该对扩展开放,对修改关闭。
刚看到这句话你可能会懵,既要不让改原来的代码,又要加新功能?这不是矛盾吗?
其实你可以这么理解,我们做项目的时候,需求肯定会一直变。今天要加个新的支付方式,明天要改个报表的导出格式,后天要换个推送渠道。如果我们每次加新功能,都去改原来已经写好、测试好的核心代码,改着改着就会出问题。原来好好的功能,可能因为你改了一行代码出bug,还要重新走一遍全量测试,浪费时间又风险高。
开闭原则要解决的就是这个问题。它想让我们加新功能的时候,不用改原来能用的老代码,只需要加新的类或者新的模块就行了。
举个很生活化的例子,你家里装了一个插线板,原来上面插了电视机、冰箱、台灯,都用得好好的。现在你买了个新的投影仪,不需要拆了插线板重新接线,直接把投影仪插在空的插孔里就能用。这个插线板其实就符合开闭原则——加新设备不用改原来的结构,扩展新设备很方便,原来的接线也不用动。
放到代码里举个实际的例子你就更懂了。比如我们做一个电商系统,最开始只支持微信支付,你写了一个支付类,所有支付逻辑都写在里面。
后来业务要加支付宝支付,你直接去原来的支付类里加了一个if判断,如果是支付宝就走支付宝的逻辑。再过一阵要加银行卡支付,你又加个else if。慢慢这个类就会变得又大又乱,每次改都可能碰错原来微信支付的逻辑,测试起来也麻烦。
如果按照开闭原则来设计呢?我们会先抽出一个通用的支付抽象接口,里面只定义一个pay方法,不管什么支付方式都要实现这个方法。原来的微信支付,就写一个微信支付类实现这个接口,支付宝支付就是支付宝类,以后加银行卡支付,直接加个新的类实现接口就行,原来的接口代码和微信支付的代码一点都不用改。
这样一来,原来测试好的代码不会被触碰,出bug的概率直接降了很多,加新功能也只需要测试新写的类就行了,效率高很多。
那为什么说开闭原则是类设计的核心原则?其他设计原则,比如单一职责、里氏替换、依赖倒置这些,其实都是为了实现开闭原则服务的。
比如单一职责要求一个类只做一件事,就是为了方便你后面扩展。如果一个类塞了好几个功能,你要加新逻辑的时候,就只能改这个类,根本没法分开扩展。
再比如依赖倒置,告诉我们要依赖抽象不要依赖具体实现,其实本质也是为了让你扩展的时候不用改上层代码。就像刚才支付的例子,上层业务只依赖支付这个抽象接口,不管你加多少种新的支付方式,上层业务代码都不用改,这不就是对修改关闭吗?
很多人会说,那我完全不修改原来的代码,根本做不到吧?毕竟有些老需求变更,原来的代码确实有问题啊。
其实开闭原则不是说绝对一点都不能改原来的代码,它是一种设计思想,让我们尽量把会变化的部分抽出来,把稳定不变的部分固定住。后续变的都是抽出来的扩展部分,稳定的核心部分就不用动了。
如果真的是原来代码有bug,那肯定要改原代码,开闭原则不是不让改bug,是不让你在加新功能的时候随便改已经稳定的核心代码。
日常写代码的时候,想要用好开闭原则,其实也不难,记住几个简单的方向就行。
首先,提前预判变化。拿到一个需求的时候,先想想哪些地方以后大概率会变。比如支付方式肯定会加,报表导出格式以后说不定会变,推送渠道肯定会扩展。把这些会变的部分抽出来做成抽象,不要把所有逻辑都写死在一个类里。
当然也不用过度设计,没必要现在啥需求都没有,就提前抽象好七八种扩展,那反而会把代码搞复杂。一般来说,当你知道这块大概率会变,或者已经变过一次了,这个时候再抽离抽象也不晚,符合演进式设计的思路就好。
其次,依赖抽象不依赖具体。让核心的稳定代码依赖抽象接口,具体的实现都放到子类里去做,这样加新实现的时候,核心代码不用动。
还有,多用组合少用继承。继承其实是刚性比较强的结构,父类改了子类全受影响,想要加新的变体,只能不断加子类,有时候还会出现很多只用一次的子类,把继承树搞的乱七八糟。用组合的话,我们可以把不同的行为抽成接口,动态替换行为,扩展起来更灵活,也更容易符合开闭的要求。
我见过很多新手,一开始觉得开闭原则是老生常谈的东西,没必要太在意,等项目做了大半年,代码改到不敢下手,每次加个小功能都要翻三四个旧类改十几处代码,才会发现这个原则的好处。
其实它本质就是帮我们控制代码变化的影响范围,把变化隔离在外面,不要让新需求的变化蔓延到整个项目的核心代码里。时间久了,项目就能一直保持比较清晰的结构,不会改着改着就变成一团乱麻。
总的来说,开闭原则不是一个要你死记硬背的规则,它是一种让代码保持灵活可维护的设计思路。不用追求每次写代码都百分之百符合,只要你写类的时候多想想,这块以后会不会变,能不能把变化的地方分开,让加新功能的时候少改点老代码,慢慢就能用好这个核心原则了。
设计原则,开闭原则,类设计,面向对象编程,软件设计,开闭原则原理,核心设计原则,对扩展开放对修改关闭,代码设计,设计模式
[Q]:什么是开闭原则?
[A]:开闭原则是类设计的核心设计原则,官方定义是软件实体(类、模块、函数)应对扩展开放,对修改关闭,也就是新增功能时尽量添加新代码,而非修改已经测试稳定的原有代码。
[Q]:开闭原则为什么要要求对修改关闭?
[A]:修改已经稳定运行、测试完成的原有代码,很容易引入新bug,还需要重新进行全量测试,会增加开发成本和风险,对修改关闭可以降低这种风险。
[Q]:对扩展开放和对修改关闭矛盾吗?
[A]:并不矛盾,开闭原则是通过抽离抽象,让新增功能通过新增实现类/模块来完成,不需要修改原有核心代码,既实现了功能扩展,又不需要改动原有代码。
[Q]:开闭原则在代码里具体怎么体现?
[A]:比如开发支付功能,可以先抽离通用的支付抽象接口,每种支付方式做单独的实现类,新增支付方式只需要添加新的实现类,不需要修改原有的支付核心代码,这就是符合开闭原则的设计。
[Q]:开闭原则为什么是类设计的核心原则?
[A]:其他设计原则,比如单一职责、依赖倒置、里氏替换原则,本质都是为了帮我们实现开闭原则服务,最终目标都是让代码更容易扩展,减少不必要的原有代码修改。
[Q]:开闭原则要求绝对不能修改原有代码吗?
[A]:不是的,开闭原则是一种设计思想,不是绝对规则,修复原有代码的bug是允许修改的,它主要限制的是新增功能时随意修改稳定核心代码的行为。
[Q]:日常开发怎么用好开闭原则?
[A]:首先提前预判会变化的业务点,抽离变化部分做抽象;其次让代码依赖抽象而非具体实现;另外多用组合少用继承,同时避免过度设计,不需要在没有需求的时候提前做无用扩展。
[Q]:开闭原则能解决什么问题?
[A]:开闭原则可以控制需求变更的影响范围,把变化隔离在扩展模块,避免新需求的修改蔓延到项目核心代码,让项目长期保持可维护性,降低新增功能的开发和测试成本。
评论 (0)
