后端技术面试38讲
李智慧
同程艺龙交通首席架构师,前Intel&阿里架构师,《大型网站技术架构》作者
立即订阅
3682 人已学习
课程目录
已更新 16 讲 / 共 38 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 掌握软件开发技术的第一性原理
免费
软件的基础原理 (8讲)
01丨程序运行原理:程序是如何运行又是如何崩溃的?
02丨数据结构原理:Hash表的时间复杂度为什么是O(1)?
03丨Java虚拟机原理:JVM为什么被称为机器(machine)?
04丨网络编程原理:一个字符的互联网之旅
05丨文件系统原理:如何用1分钟遍历一个100TB的文件?
06丨数据库原理:为什么PrepareStatement性能更好更安全?
07丨编程语言原理:面向对象编程是编程的终极形态吗?
答疑丨Java Web程序的运行时环境到底是怎样的?
软件的设计原理 (6讲)
08丨软件设计的方法论:软件为什么要建模?
09丨软件设计实践:如何使用UML完成一个设计文档?
10 | 软件设计的目的:糟糕的程序员比优秀的程序员差在哪里?
11丨软件设计的开闭原则:如何不修改代码却能实现需求变更?
12 | 软件设计的依赖倒置原则:如何不依赖代码却可以复用它的功能?
13丨软件设计的里氏替换原则:正方形可以继承长方形吗?
不定期加餐 (1讲)
加餐 | 软件设计文档示例模板
后端技术面试38讲
登录|注册

11丨软件设计的开闭原则:如何不修改代码却能实现需求变更?

李智慧 2019-12-16
我在上篇文章讲到,软件设计应该为需求变更而设计,应该能够灵活、快速地满足需求变更的要求。优秀的程序员也应该欢迎需求变更,因为持续的需求变更意味着自己开发的软件保持活力,同时也意味着自己为需求变更而进行的设计有了用武之地,这样的话,技术和业务都进入了良性循环。
但是需求变更就意味着原来开发的功能需要改变,也意味着程序需要改变。如果是通过修改程序代码实现需求变更,那么代码一定会在不断修改的过程中变得面目全非,这也意味着代码的腐坏。
有没有办法不修改代码却能实现需求变更呢?
这个要求听起来有点玄幻,事实上却是软件设计需要遵循的最基本的原则:开闭原则。

开闭原则

开闭原则说:软件实体(模块、类、函数等等)应该对扩展是开放的,对修改是关闭的
对扩展是开放的,意味着软件实体的行为是可扩展的,当需求变更的时候,可以对模块进行扩展,使其满足需求变更的要求。
对修改是关闭的,意味着当对软件实体进行扩展的时候,不需要改动当前的软件实体;不需要修改代码;对于已经完成的类文件不需要重新编辑;对于已经编译打包好的模块,不需要再重新编译。
通俗的说就是,软件功能可以扩展,但是软件实体不可以被修改
功能要扩展,软件又不能修改,似乎是自相矛盾的,怎样才能做到不修改代码和模块,却能实现需求变更呢?
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《后端技术面试38讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(16)

  • 山猫
    我同意老师通过这个例子简单的描述开闭原则。但如果项目初始就对button按钮需要进行这么复杂的设计,那么这个项目后期的维护成本也是相当之高。

    作者回复: 是否要使用各种设计模式设计一个非常灵活的程序,主要是看你的需求场景,而不是看项目的阶段。

    如果你的场景就是需要这么灵活,就是要各种复用,应对各种变更,那么你一开始就应该这样设计。

    如果你的场景根本不需要一个可复用的button,那么就不需要这样设计。

    关键还是看场景。

    但是场景也会变化,一开始不需要复用,但是后来又需要复用了,那么在在需要复用的第一个场景,就重构代码,而不是等将来维护困难局面hold不住了再重构。

    ps 如果你习惯了这种灵活的设计,你会觉得这种设计并不复杂。对于软件开发而言,复杂的永远是业务逻辑,而不是设计模式。设计模式是可重复的,可重复的东西即使看起来复杂,熟悉了就会觉得很简单。

    pps 看起来复杂的设计模式就是用来解决维护困难问题的,正确使用设计模式,看起来复杂了,其实维护简单了,因为关系和边界更清晰了,你不需要在一堆强耦合的代码里搅来搅去。真正维护成本高的其实是你所谓的简单的设计,牵一发动全身,稍不注意就是各种bug。

    ppps 重要的话再说一次:
    关键还是看场景。

    没有银弹,没有一种必然就是好的设计方案,能理解场景的才是真·高手。

    2019-12-16
    14
  • Jesse
    思考题
    匿名内部类,已经数字按钮注册的listener其实就是DigitButtonDailerAdepter适配器的实现,sendButton中注册的listener其实就是SendButtonDailerAdepter适配器的实现。
    2019-12-16
    1
    3
  • 山猫
    没有想到我的评论老师会有那么多文字进行评论,也没有想到会有那么多赞。

    看了老师的评论,其实我也知道这个和所处的场景有很大关系。

    在项目开发过程初期,有很多东西是想不到的。譬如一个登陆页面,

    第一版可能就是简单的账号口令登陆,
    第二版可能就需要加上第三方登陆,
    再往后可能需要分类型登陆,
    在过一段时间可能有客户端的登陆。

    还有一种可能就是这个项目真的就这一个登陆,其他的登录方式又采取微服务的办法。

    有时候真的是看每个工程师的经验,还有对客户的熟悉程度。做开发好多年了,过度开发我也写过,预估不足我也写过。

    需求每天都在变,虽然设计模式能够照顾到大多数的需求变化,但总有坑死人的客户和打不死的需求。

    祝每个开发者好好学习技术,祝每个开发者都不要遇到坑死人的项目。

    疯了,累了,痛了,人间喜剧@_@
    2019-12-19
    1
    1
  • Roy Liang
    /*适配器模式*/
    public class Button {
        private ButtonServer server;
        private int token;

        public Button(int token, ButtonServer server) {
            this.token = token;
            this.server = server;
        }

        public void press() {
            server.buttonPressed(token);
        }
    }

    public interface ButtonServer {
        void buttonPressed(int token);
    }

    public class Dialer {
        public final static int SEND_BUTTON = -99;

        public void enterDigit(int digit) {
            System.out.println("enter digit: " + digit);
        }

        public void dial() {
            System.out.println("dialing...");
        }
    }

    public class DigitButtonDialerAdapter implements ButtonServer {
        private Dialer dailer = new Dialer();

        @Override
        public void buttonPressed(int token) {
            dailer.enterDigit(token);
        }
    }

    public class SendButtonDialerAdapter implements ButtonServer {
        private Dialer dialer = new Dialer();

        @Override
        public void buttonPressed(int token) {
            dialer.dial();
        }
    }
    2019-12-17
    1
  • Jonathan Chan
    求老师后续给出完整代码学习!
    2019-12-16
    1
    1
  • 诗泽
    通俗的说就是,软件功能可以扩展,但是软件实体不可以被修改

    如果这个phone 之前是没有摄像头,现在加了一个摄像头那phone 这个实体要不要修改呢?🤔
    2019-12-19
  • 唐二毛
    有一点想不通,在adapter里面还是需要判断呀?这并没有 达到老师说的 避免做switch/if判断的效果,而且判断的逻辑一点不少,还无端弄出这么多类,有必要非得这么做吗?

    作者回复: Adapter不需要判断,请看思考题

    2019-12-19
  • Roy Liang
    依葫芦画瓢,应用观察者模式写了一个密码箱类
    public class CodeCase {
        private Dialer dialer;
        private Button[] digitButtons;
        private Button lockButton;

        public CodeCase() {
            dialer = new Dialer();
            digitButtons = new Button[10];
            for (int i = 0; i < digitButtons.length; i++) {
                final int digit = i;
                digitButtons[i] = new Button();
                digitButtons[i].addListener(() -> dialer.enterDigit(digit));
            }
            lockButton = new Button();
            lockButton.addListener(() -> dialer.lock());
        }

        public static void main(String[] args) {
            CodeCase codeCase = new CodeCase();
            codeCase.digitButtons[1].press();
            codeCase.digitButtons[2].press();
            codeCase.digitButtons[3].press();
            codeCase.digitButtons[4].press();
            codeCase.lockButton.press();
        }
    }
    2019-12-18
  • 陈小龙 Cheney
    希望老师给出几个阶段的代码. 方便对着代码对比学习. 直接看文字感觉抽象模糊了.
    2019-12-17
  • 草原上的奔跑
    想问下老师,在写设计文档的时候,系统,子系统,组件,分别映射到代码的什么层面(类很好理解);还有老师的设计文档中没有业务架构图和技术架构图,老师对这两种架构图是怎么理解的,设计文档中需要加入吗?希望老师给予解答

    作者回复: 组件就是jar,dll这些。子系统和系统就是可运行的完整程序,一个或者多个war。

    不管是什么架构图,只要你觉得有助于你描述你的架构设计,都可以加入设计文档中。


    2019-12-17
  • 鹏酱
    Phone的构造函数里了
    2019-12-17
  • 一步
    结合王争老师的设计模式课,会理解的更透彻
    2019-12-16
  • Geek_d048e4
    老师,Dailer 实现 ButtonServer你画的UML图用的是实线三角形图,接口实现应该是虚线三角头

    作者回复: 谢谢指正,我用的UML工具没有虚线三角😢

    我在第9篇专栏提到,UML用来交流和思考,如果不影响交流,那么UML画法可以简化,也就是所谓的UML方言。

    2019-12-16
  • 龙龙first
    我觉得还是需要业务和技术经验才能知道怎样模块解耦。如果不清楚,就不知道是对button的实现做预留扩展还是对action做预留扩展,因为也有可能后续需求不是点击按钮操作,而是动作识别出发操作
    2019-12-16
  • Paul Shan
    开闭原则是移除底层的if else,取而代之的是上层的类结构。不过,我个人以为一开始的if else, 甚至switch 也没什么不妥的,毕竟代码简单直接。引入了很多类,读代码也是负担,而且也很难预料到哪些修改是必要的。当if else数量多于一定的数目,再开始重构。 不知道李老师如何看待这种观点。

    作者回复: 当你准备写第一个else的时候,就说明你的代码即将陷入僵化、牢固和脆弱,而且为将来的需求变更引入了一个糟糕的“设计模式”。

    如果其他人接手你的代码,他有两个选择,要么继续写更多的else以应对需求变更;要么心理暗骂一声然后重构你的代码。你希望他选择哪个?

    2019-12-16
    3
  • Paul Shan
    思考题
    Adapter 是在继承接口的时候调用了dialer不同的函数实现了,没有显式的Adapter。
    2019-12-16
收起评论
16
返回
顶部