V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
x97bgt
V2EX  ›  程序员

开闭原则(open/closed principle)到底是啥意思?

  •  
  •   x97bgt · 68 天前 · 4817 次点击
    这是一个创建于 68 天前的主题,其中的信息可能已经有所发展或是发生改变。

    SOLID 的提出者 Martin 是这么描述开闭原则 OCP 的

    software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

    但我不大理解。代码一旦写完,就不能修改,只能往上添东西了?这不是堆 shi 山吗?

    60 条回复    2021-11-17 09:51:20 +08:00
    zongren
        1
    zongren  
       68 天前
    这。。。企业及理解了
    我希望我发布的类不允许被 client 魔改,而只能继承并使用已有的方法,或新增方法
    powerfulyang
        2
    powerfulyang  
       68 天前 via iPhone
    你用别人库的时候,希望不同版本自己都重新适配一遍?
    x97bgt
        3
    x97bgt  
    OP
       68 天前
    @zongren
    啥叫被 client 魔改?
    silk
        4
    silk  
       68 天前
    不能修改的是抽象基础吧,具体需求实现是基于这之上的
    x97bgt
        5
    x97bgt  
    OP
       68 天前
    @powerfulyang
    那只要接口不变,就不需要重新适配了啊。跟这段话有什么关系? 你修改或新增代码,都可以做到兼容老接口啊。
    bk201
        6
    bk201  
       68 天前
    保持兼容性的意思
    silk
        7
    silk  
       68 天前
    就好像 实现一个人 他要吃饭睡觉 这是基础的,至于说吃什么 睡在那里都属于可以修改的。而且吃饭睡觉也只是从人这个基础衍生的具体行为。套娃了
    x97bgt
        8
    x97bgt  
    OP
       68 天前
    @silk 你这个描述可以说得过去。但这句话完全没有提到抽象、接口什么的,感觉对不上。
    silk
        9
    silk  
       68 天前
    人是一个抽象的类 人的行为是你要实现的接口
    ENNRIaaa
        10
    ENNRIaaa  
       68 天前
    对扩展开放,对修改关闭
    BeautifulSoap
        11
    BeautifulSoap  
       68 天前   ❤️ 5
    楼上说复杂了,这里 clinet 指的是使用你定义好的类或函数的那一方。对于使用你的类或函数的那一方,你应该提供一定机制可以让他们方便扩展功能(比如提供改变执行策略的接口啊,继承啊之类的),而不应该是他们想要什么功能了,因为没法扩展实现功能,而直接跑过来直接去改你定义好的类或函数的代码
    Rocketer
        12
    Rocketer  
       68 天前 via iPhone   ❤️ 5
    因为修改一个地方(类、方法等)很可能会导致调用方跟着修改,这是麻烦且不可预期的,很容易导致故障,所以禁止修改。

    但你也不应禁止别人在你的基础上扩展,否则只能用你提供的功能就太局限了。

    这是个指导原则,脑子里时刻想着这些,你在设计代码结构的时候就会更优雅。各种设计模式几乎都是这个原则的优秀实践。
    otakustay
        13
    otakustay  
       68 天前   ❤️ 1
    一开始是个 A ,然后有一个需要 B ,完了之后 B 不需要了,又新增了需求 C

    在修改的方案上,A 加代码变成 AB ,AB 加代码变成 ABC
    此时应该有一个把 AB 删代码变回 A 的过程,但只要项目紧急些就不会干这事,最后就是 ABCDEFG 一堆了

    用扩展的方案,我们会设计一个有扩展性的 A ,然后有一个独立的 B ,使用 A+B 。之后再有一个独立的 C ,使用 A+C 的组合
    这样做的几个优势:
    1. B 是独立的,删除它是零成本。甚至不管它,它也不会影响其它的部件,并不导致系统复杂性的提升和可维护性的降低
    2. 配合良好的部署架构,A+C 和 B+C 可以做增量更新和热更新,但需要修改 A 代码变成 AB 的方案就几乎无法做增量更新
    3. 更极端的情况下,如果需要一部分用户是 A+B ,另一部分用户是 A+C ,扩展的方法可以保持结构更清晰。对于类似前端的情况,也可以做到向用户传输的内容更少

    扩展方案的代价就是,你需要 A 能支持往上面插 B 、C 、D ,就必须对 A 有一个足够抽象的设计
    silk
        14
    silk  
       68 天前
    #13 必须对 XXX 有一个足够抽象的设计 这是关键 不然还不如堆 shi 山
    x97bgt
        15
    x97bgt  
    OP
       68 天前
    @BeautifulSoap
    @Rocketer
    那就是定义好接口。有改动时,保证接口的兼容性(扩展),以做到不需要 client 修改调用方式。

    但是这里的适用对象是什么呢?是同一个包里的不同类,不同包,还是不同的 lib 之间?感觉尺度不一样,这个原则就比较微妙。啥时候应该应用?
    wfd0807
        16
    wfd0807  
       68 天前
    不要纠结于字面意思,你在自己的工作过程中记住一点:在完成当前需求的情况下,尽可能多的考虑未来可能发生的变化,并把这些变化作为当前需求的一部分;随着经验的累积,你会自然而然的熟练运用开闭原则
    ykrank
        17
    ykrank  
       68 天前
    这个是原则。而接口,抽象都是为了实现这个原则的产物。什么叫接口,接口就是提取开闭原则里面开的那部分;什么是实现,实现就是开闭原则里闭的那部分。
    如果说开闭原则是宪法,那接口这个定义只是根据宪法制定的法律而已。宪法肯定不会去提具体的法律名称,因为那些定义只是从自己引申出来的孙子而已。
    wfd0807
        18
    wfd0807  
       68 天前
    @wfd0807 “并把这些变化作为当前需求的一部分” => 并不是在当前需求中实现,而且在当前需求中留下扩展的余地
    eason1874
        19
    eason1874  
       68 天前
    不是不让你 debug ,而是不让修改功能(行为),保持一致性

    如果你在功能实现上就是屎一样,想改功能,那你就新增一个,而不是直接同名替换

    同名替换会导致牵一发而动全身,所有内部的外部的调用都需要重新适配
    ykrank
        20
    ykrank  
       68 天前
    你在谈接口这个定义的同时,其实就是在谈论开闭原则的具体实现。
    x97bgt
        21
    x97bgt  
    OP
       68 天前
    @BeautifulSoap
    @Rocketer
    @silk
    @wfd0807
    @ykrank
    @eason1874

    如果是这样,那 OCP 有点难做到
    - 如果兼容老接口是必须的,那肯定会一直堆东西,代码质量肯定会越来越糟糕。
    - OCP 的前提是要有良好的抽象 /接口。但这需要精确地理解业务,同时要有良好的设计能力。抽象做不好,还不能改,那就会越走越偏了。。。这有点难。。

    这些原则还是很博大精深的,什么时候要用,什么时候不用,纯粹靠经验了。。。
    taowen
        22
    taowen  
       68 天前
    两个人改同一个文件容易 conflict 。如果是通过新增文件的方式来实现新需求,就可以减少 merge conflict 。
    kop1989
        23
    kop1989  
       68 天前
    @x97bgt #21
    你论点的前提都有待商榷。

    1 、为何“堆内容”就会导致代码质量“肯定”越来越糟糕???
    2 、为何开闭原则要充分抽象?别忘了还有单一职责原则。

    软件工程就是为了追求软件实现的最优性价比而存在的。
    各种原则都有其利弊。你要抉择的是根据需求、成本等角度考虑保持原则的程度,而不是“用”与“不用”。
    BeautifulSoap
        24
    BeautifulSoap  
       68 天前
    @x97bgt 这东西就是个指导原则,你能做到就做,做不到也不会少块肉就是今后可能代码比较难维护。还有,开闭原则实现并不一定需要接口, 比如我们经常使用的各种 sort 函数就是很好遵循了开闭原则的函数(你要不同 sort 策略直接传个 callback 进去就好了)
    eason1874
        25
    eason1874  
       68 天前
    @x97bgt #21 开闭原则可以确保新旧版不会冲突,什么时候淘汰老版本是业务上的取舍,跟编程无关。

    兼容只应该用于过渡,要设置淘汰周期。要是抱着老版本一直不放,那下场就跟 IE 一样。你不主动淘汰落后的技术,那现实就把你淘汰掉
    x97bgt
        26
    x97bgt  
    OP
       68 天前
    @kop1989

    1. shi 山不都是堆出来的吗?多搞些 if 和多用点继承(我能想到的扩展方式),那就会慢慢变得无法维护了。
    2. 不抽象,那 client 处用什么方式来调用功能? 用用具体实现类?

    不过确实,每个原则都是权衡的过程。没有一定要遵守的原则。
    x97bgt
        28
    x97bgt  
    OP
       68 天前
    @eason1874
    所以遵循 OCP 的理由之一还要看 client 方是不是强势。如果是甲方,你不得不维护,那就就只能遵循了。
    x97bgt
        29
    x97bgt  
    OP
       68 天前
    @BeautifulSoap
    除了 sort 还有啥方式?我一开始能想到的就是实现接口和进行继承。
    lingo
        30
    lingo  
       68 天前
    然而我觉得堆屎山确实是最终归宿 = =
    BeautifulSoap
        31
    BeautifulSoap  
       68 天前 via Android
    @x97bgt 也可以类似策略模式那样规定流程,然后可自由替换不同步骤的执行逻辑。或者让 client 自由组合不同方法实现功能(比如各种不同的 builder ,用得最多的应该就是 sql builder 了)
    eason1874
        32
    eason1874  
       68 天前
    @x97bgt 内部项目这样搞也很麻烦,改一处影响几十处,增加不必要的兼容风险,连累自己,连累同事

    小打小闹的时候,工程规范像累赘,不灵活。随着工程复杂度增加,只有工程规范能把控风险,关键时候救你一命
    lancerss
        33
    lancerss  
       68 天前
    对扩展开放对修改关闭
    Caturra
        34
    Caturra  
       68 天前 via Android
    倒不如想想为什么要修改,是扩展做不到你想干的事情吗
    libook
        35
    libook  
       68 天前
    这个跟屎山没有关系,只是指导合理抽象的一种思路,避免不合理的抽象导致各种代码维护问题。

    我试着举个例子吧,如果不贴切就当抛砖引玉了。

    “modification”式的做法:写一个 class ,叫做“动物”:
    有一个属性叫做“物种”,枚举为“狗”、“猫”;
    有一个方法叫做“叫”。
    “叫”方法里写一个判断逻辑:当“物种”为“狗” 的时候,输出“汪汪”;当“物种”为“猫”的时候,输出“喵喵”。
    后面需求发生了变化,想要增加一个新物种“鸭子”,那么只能够去修改“动物”这个 class ,在“物种”这个枚举里添加“鸭子”,在“叫”这个方法里添加一个新的判断分支,当“物种”为“鸭子”的时候输出“嘎嘎”。

    “extension”式的做法:写一个 interface ,叫做“动物”:
    有一个属性叫做“物种”,
    有一个方法叫做“叫”。
    基于“动物”实现一个“狗”class ,“物种”属性为“狗”,“叫”方法为输出“汪汪”;
    基于“动物”实现一个“猫”class ,“物种”属性为“猫”,“叫”方法为输出“喵喵”;
    后面需求发生了变化,想要增加一个新物种“鸭子”,可以在不修改“动物”代码的基础上直接基于“动物”实现一个“鸭子”class ,“物种”属性为“鸭子”,“叫”方法为输出“嘎嘎”。
    kujio
        36
    kujio  
       68 天前
    差不多就是可以 copy 一个分支,让部分功能走新的分支,但不能修改原有的代码,
    以这种类似 java 重载的方式来实现修改,
    局限是有的,有时候确实麻烦,但可以保持代码兼容性.
    dddd1919
        37
    dddd1919  
       68 天前
    改了重写 ×
    继承扩展 √
    flniu
        38
    flniu  
       68 天前
    软件实体应该对扩展开放、对修改封闭。
    有一个直观的理解就是,该实体相关的单元测试,应该只添加(扩展新逻辑)、不修改(修改现有逻辑)。
    然后在不破坏单元测试(不破坏约定的外部行为)的前提下,可以经常重构,不断重构。
    再结合单一职责原则,软件实体可以被替换、退休,但确实不应该修改。
    unco020511
        39
    unco020511  
       68 天前   ❤️ 1
    意思是你写的工具,别人用的时候可以很方便的通过继承 /实现 /组合等等这样的方式来扩展功能,并不需要去修改你原来的代码
    otakustay
        40
    otakustay  
       68 天前
    @silk #14 了解不够那不叫设计,那叫瞎比划
    admol
        41
    admol  
       68 天前
    开闭原则讲的是代码的扩展性问题,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
    Buges
        42
    Buges  
       68 天前 via Android
    这个很好理解,你用一个第三方库,但和你的需求有所出入,你包装一套接口使用就叫 extension ,你直接去改库代码就叫 modification 。你说哪个更容易维护?
    Yuan2One
        43
    Yuan2One  
       68 天前
    其实就是增加内聚,降低耦合
    对修改关闭:不要变更各部件的中间耦合部分,不然会导致所有依赖部件都要修改
    对拓展开放:提供耦合实现,包别的部件可以轻易接入实现拓展
    开闭原则的典型实现就是接口
    我是这么理解的
    gaocc
        44
    gaocc  
       68 天前
    开放-封闭原则:对于持续迭代的项目,对于代码实体(类,模块,函数等)应该可以拓展,但不能修改;
    开闭原则的好处是,在实际开发中,需求会多次改变但程序还是会相对稳定,从而使第一个版本后可以不断推出新版本。
    2i2Re2PLMaDnghL
        45
    2i2Re2PLMaDnghL  
       68 天前
    一个好的设计思想不应该局限于面向对象。

    『这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。』

    即是指提供『改变行为』的『插槽』
    sort 的例子很明显,它通常提供一个 cmp 或者 key 函数的插槽。(当然,它通常提供了默认项,方法不一。)
    更一般的例子是允许虚函数的抽象类,这个「虚函数表」就是那个插槽的客观实体。在编写抽象类时,你根本无从得知 obj.meth 到底是哪段内存中的代码。
    georgetso
        46
    georgetso  
       68 天前
    我感觉生命周期函数就是典型的复合开闭原则的设计.
    生命周期函数本身就是提供给开发者扩展的 anchors, 同时不提供对象生命周期耦合关系的修改.
    Innovatino
        47
    Innovatino  
       68 天前
    @libook
    你的字多,你解释得也清楚。对于初学者来说够用了
    darknoll
        48
    darknoll  
       68 天前
    就是非侵入式,好处非常多
    11232as
        49
    11232as  
       68 天前
    我个人理解就是尽量写粒度小的接口,然后在其基础上扩展、搭积木,避免写出那种包含一堆业务功能的方法。有点类似 Unix 编程思想里那个小既是美的感觉...?
    dranfree
        50
    dranfree  
       68 天前
    提供合理的扩展接口,在不修改核心代码的情况下,为系统增加功能。
    FrankFang128
        51
    FrankFang128  
       68 天前
    你的理解跟我的一样,我也觉得应该拥抱修改。除了对外 API
    cryboy007
        52
    cryboy007  
       68 天前
    个人理解就是后续迭代不能影响现有功能
    powerman
        53
    powerman  
       68 天前   ❤️ 2
    理解软件工程以及代码架构,关键是你要理解一点,

    那就是不管 SOLID 原则以及设计模式的运用都是在做同一件事情,将易变的部分与不易变的部分隔离开来,更抽象的说法就是提供一套机制将策略与机制分离开来。

    一般来说机制是难以变化的,而策略经常变化,一个软件,框架可以看成一种机制,因为写业务大多只是使用框架提供的功能,而并非去对框架进行修改,也就是说框架是不易变的,而业务逻辑是易变的,在这里框架是一种机制,业务代码是一种策略,例如 mybaits 框架,你编写 SQL xml 这就是一种策略,而 mybatis 框架本身就是一套机制。

    做到这一点首先要对目标域的问题进行分解,发现难以变化的部分以及容易变化的部分,
    例如 Spring 框架,通过 XML 加载 bean 定义的 或者 通过类扫描 加载 Bean 定义 又或者你要自定义一种其它的方式来加载 bean 定义,总儿言之加载 Bean 定义的方式是多种变化的,但是对 bean 本身来说,bean 需要解决的领域问题是确定的,是不会变化的,所以抽象出来一个 BeanDefination 用于初始化 Bean ,这样 BeanDefination 作为一种初始化 bean 数据结构的机制是不易变的,而具体加载生成 BeanDefination 的策略是易变的。

    类似的设计思想还有很多,例如 BeanFactoryPostProcessor BeanPostProcessor 也是这样一种思想,框架本身提供了一种回调的机制,至于你如何客制化 BeanDefination 或者 客制化 Bean 的 instance 则是一种具体的策略。

    像 sort 方法 排序算法本身是一种机制,至于如何比对两个对象的大小则是一种具体的策略。

    总而言之,在代码层面上,你要时刻去思考,你的代码如果要复用,那么被复用的代码应该提供一种什么样的机制,让具体的策略去使用去组合起来方便,而不至于需要修改你原本的,至于具体的设计模式 反倒并没有那么重要,
    fkdog
        54
    fkdog  
       68 天前   ❤️ 1
    楼上这些解释都是复读机。

    你想想 spring 就明白了,我们在做 spring 相关插件扩展功能的时候,是不是都不需要修改 spring 核心类库的任何代码?
    因为 spring 提供了足够多的扩展点能让我们非常方便的实现需求。

    所以我们平时写代码时也应该像 spring 一样,通过类似模板等设计模式为变化较多的部分提供充分的可扩展空间。

    细化到具体的某个业务么,就是类似支付系统。一开始接的支付宝支付,然后加了微信支付,if-else 可以满足需求。但是如果后边需要对接银联等支付,if-else 就会堆成屎山。在不修改原来业务主流程的情况下,通过扩展多个支付方法子类显然是更优雅的选择。

    不过国内的互联网,如果不是搞基础框架、业务中台一类的,搞这些开闭原则啥的其实没多大意义,相反还会徒增烦恼。视项目规模和维护价值自己做取舍。
    jiayong2793
        55
    jiayong2793  
       68 天前
    保持类的最小颗粒度
    OnlyO
        56
    OnlyO  
       68 天前
    @fkdog #54 你这个回复解释的比较到位.
    xylophone21
        57
    xylophone21  
       68 天前
    一看你的客户是谁,二看你的业务这么开展.

    @fkdog 的例子就很好, 你做支付的,经常性的接一个新的支付(夸张了)就是你的业务, 对接的这个人就是你的客户. 让他们每次干活的时候不需要修改你的代码.

    但你的代码里, 假设原来用的是 mysql,你要切到 postgresql, 当然可以改你的代码.

    再深入一步, 你当然也可在设计之初,就想好, 如果后面要改数据库, 我是不是可以不修改, 留好扩展. 但这不是你的业务, 想多了就是过度设计了. 除非你做这个支付系统的目的, 就是为了测试各种不同的数据库系统. 或者说以及有现成的轮子了, 用起来几乎没有代价 (比如这个换数据库的例子,从某种意义上来说,就是有轮子的情况)

    再进一步, 换数据库虽然不是你的业务, 但他其实是框架的业务, 比如对 Spring Data JPA 来说, 他的一个目标就是不论他的用户用什么数据库, 核心流程都差不多. 那么换数据库就是他的业务.
    lux182
        58
    lux182  
       67 天前
    @powerman 非常好,学习了
    summerLast
        59
    summerLast  
       67 天前
    对扩展开放 对修改关闭 核心是怕修改引入新的问题 ;
    如果 没有引入新的问题而且行为表现一致的化 是可以修改的 ,假如代码已经腐烂了,是在腐烂外面一层层的包裹 最后腐烂的代码成为了软件珍珠 还是重构这是权衡之后做出的选择。
    很多时候你会发现有很多相对立的概念好像都对, 核心是缺失了上下文导致的,而这些对立的概念其实就是一种策略,策略也是需要有上下文来决定使用那种更好一些,不要去迷信各种模式,他们是抽象的概念 既然抽象必有信息的丢失,找到自己写代码的感觉
    powerman
        60
    powerman  
       67 天前
    @fkdog 复读机? 也不算吧,光是理解问题域,发现问题域中 难以变化的部分 将其剥离出来形成一种机制 与易变化的部分隔离 就要对领域知识有充足的了解,而并非对代码技术有多么深层次的理解。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2146 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 38ms · UTC 15:48 · PVG 23:48 · LAX 07:48 · JFK 10:48
    ♥ Do have faith in what you're doing.