zoukankan      html  css  js  c++  java
  • 重新温习软件设计之路(3)

    本文是我学习课程《软件设计之美》的学习总结第三部分,分享面向对象的三个特点和五个设计原则的理解。

    上一篇:体会软件设计之美(2)

    1 面向对象的三个特点

    我们都知道面向对象有三个重要的特点:封装、继承 和 多态。

    封装

    封装是面向对象的根基,它指导我们将紧密相关的信息放在一起,形成一个单元,并在此基础之上我们可以一层一层地逐步向上不断构建更大的单元。

    封装的重点在于:对象提供了哪些行为,而不是有哪些数据。因此,我们在设计一个类的时候,先考虑其对象应该提供哪些行为,根据行为提供对应的方法,最后考虑实现这些方法需要有哪些字段。

    在封装过程中,我们可以遵循一个“最小接口暴露”原则,即尽量减少内部实现细节的暴露 和 减少对外暴露的接口。

    继承

    继承是面向对象的重要特性,目前有两种类型:实现继承 和 接口继承。

    实现继承,是站在子类的角度向上看,面对的是子类。

    Child obj = new Child();

    接口继承,是站在父类的角度向下看,面对的是父类。

    Parent obj = new Child();

    在使用继承上,我们常见的误解是:将实现继承当做了一种代码复用的方式。

    合理使用继承的方式是:一个通用原则(组合优于继承) 和 一个编程思想(面向组合编程),它们其实也是 分离关注点 的具体实践。

    多态

    只使用封装和继承的编程方式,只能称之为基于对象编程,只有把多态加入进来,才能称之为面向对象编程。换句话说,多态将基于对象与面向对象区分开来了。

    所谓多态,就是 一个接口,多种形态。

    实现多态,我们需要找出不同事物的共同点(前提是 分离关注点),从而建立起抽象。

    多态不一定要依赖于继承来实现,在面向对象编程中,更重要的是 封装和多态。比如,面向接口编程,只要遵循相同的接口,就可以表现出多态。

    2 SOLID设计原则

    在面向对象的设计原则中,比较成体系的当属SOLID原则,SOLID是五个设计原则首字母的缩写,它们分别是:

    • 单一职责原则(Single Responsibility Principle, SRP)

    • 开放封闭原则(Open-Closed Principle, OCP)

    • Liskov替换原则(Liskov Substitution Principle, LSP)

    • 接口隔离原则(Interface Segregation Principle, ISP)

    • 依赖倒置原则(Dependency Inversion Principle, DIP)

    SOLID原则出自 Robert Martin 的著作《敏捷软件开发:原则、模式与实践》和《架构整洁之道》,他在这两本书对SOLID原则进行了完整的阐述。

    单一职责原则

    一个模块最理想的状态是不改变(虽然几乎不可能实现),其次是少改变,这是一个模块设计好坏的衡量标准。Robert Martin在《敏捷软件开发:原则、模式与实践》中对单一职责的定义是:“一个模块应该有且只有一个变化的原因”,到了《架构整洁之道》中定义变为了:“一个模块应该对一类且仅对一类行为者(actor)负责”。郑晔老师说,这个定义的演化其实是从考虑变化的原因 到 考虑变化的来源,这些变化的来源就是不同的行为者(actor),我们需要将不同行为者负责的代码放到不同的地方去。因此,看起来它和分离关注点有着异曲同工之处,即都是通过拆分来达到更小的粒度以应对潜在变化带来的影响。理解好单一职责原则,我们需要:

    • 理解封装,知道要把什么样的内容放到一起;
    • 理解分离关注点,知道要把不同的内容拆分开来;
    • 理解变化的来源,知道把不同行为者负责的代码放到不同的地方;

    单一职责原则可以应用于不同的层次:小到一个函数,大到一个系统。

    开放封闭原则

    软件实体(类、模块、函数)应该对扩展开放,对修改封闭。

    对扩展开放,就是新需求应该用新代码实现。

    对修改封闭,就是不修改已有的代码。

    实现开放封闭原则的前提是:在软件内部留好扩展点。郑晔老师说道,扩展点就是我们需要去设计的地方,每一个扩展点都是一个需要设计的模型。而构建模型的难点,首先在于分离关注点,其次在于找到共性(即从变化中找到不变)

    一旦构建好扩展点,系统就可以逐渐地稳定下来。

    在具体实现中,要设计扩展点,一般都需要面向接口编程,即实现面向对象最重要的特征—多态。

    下面就是一个从变化中找到共性的重构代码小案例:

    重构之前:

    public class ReportService
    {
        public void Process()
        {
            // 01.获取当天的订单
            List<Order> orders = GetDailyOrders();
            // 02.生成统计信息
            OrderStatistics statistics = GenerateOrderStatistics(orders);
            // 03.生成统计报表
            GenerateStatisticsReport(statistics);
            // 04.发送统计邮件
            SendStatisticsMail(statistics);
        }
    }

    重构之后:

    (1)找到共性,构建模型

    public interface IOrderStatisticsConsumer
    {
        void Consume(OrderStatistics statistics);
    }
    
    public class StatisticsReporter : IOrderStatisticsConsumer
    {
        public void Consume(OrderStatistics statistics)
        {
            // To do : GenerateStatisticsReport(statistics);
        }
    }
    
    public class StatisticsMailer : IOrderStatisticsConsumer
    {
        public void Consume(OrderStatistics statistics)
        {
            // To do : SendStatisticsMail(statistics);
        }
    }

    (2)稳定入口,屏蔽变化

    public class ReportService
    {
        private List<IOrderStatisticsConsumer> _consumers;
    
        ......
        
        public void Process()
        {
            // 01.获取当天的订单
            List<Order> orders = GetDailyOrders();
            // 02.生成统计信息
            OrderStatistics statistics = GenerateOrderStatistics(orders);
    
            foreach (var consumer in _consumers)
            {
                consumer.Consume(statistics);
            }
        }
    }

    (3)新来需求,代码扩展

    public class StatisticsSender : IOrderStatisticsConsumer
    {
        public void Consume(OrderStatistics statistics)
        {
            // To do : SendStatistics2OtherSystem(statistics);
        }
    }

    综述,每做一次模型的构建,核心的类就会朝着稳定的方向前进一步。

    Liskov替换原则

    Barbara Liskov是一位图灵奖获得者(2008),以她名字命名的Liskov替换原则影响深远。所谓Liskov替换原则,就是子类型(subtype)必须能够替换其父类型(basetype)

    郑晔老师强调,理解该原则需要站在父类的角度 而不是 子类的角度,一旦站在子类的角度代码中就会出现如下所示的RTTI(运行时类型识别)相关的代码。

    public void Handle(Handler handler)
    {
        if (handler is ReportHandler)
        {
            // 生成报告
            (handler as ReportHandler).Report();
            return;
        }
    
        if (handler is NotificationHandler)
        {
            // 发送通知
            (handler as NotificationHandler).SendNotification();
            return;
        }
    }

    换句话说,关心子类是一种实现继承的表现,而接口继承才是我们努力的方向,接口继承也更符合Liskov替换原则。

    因此,我们要用父类的角度去思考,设计行为一致的子类。

    接口隔离原则

    所谓接口隔离原则,就是在接口中不要放置使用者用不到的方法。

    因为接口往往是由程序员来完成的,程序员很多时候没有区分开使用者和设计者,这也是没有做好分离关注点的一种体现。而接口设计不好,最常见的问题就是“胖”接口。

    解决方案就是,识别出接口不同角色的使用者,面向不同的使用者设计小接口

    从更广泛角度看,它也告诉我们不要依赖于任何不需要的东西,指导我们在高层次上进行设计。

    依赖倒置原则

    所谓依赖倒置原则,就是高层模块不应该依赖于低层模块,二者应该依赖于抽象

    理解依赖倒置的关键在于理解倒置,即让高层模块与低层模块实现解耦,高层模块尽量保持相对稳定,低层模块去依赖高层定义好的接口(即抽象),高层模块不会随着低层代码的变化而变化。

    计算机科学中的所有问题都可以通过引入一个中间层得到解决。

    —— David Wheeler

    郑晔老师将此原则简化为一点:依赖于抽象,并从中推导出几条可以覆盖大部分情况的指导编码的具体规则:

    • 任何变量都不应该指向一个具体的类;

    • 任何类都不应该继承自具体类;

    • 任何方法都应该改写父类中已经实现的方法;

    因此,在设计时我们需要依赖一个稳定的抽象,而具体的实现类往往大部分场景下都是由DI容器这类框架去负责调用和组装。

    3 小结

    本文我们学习了面向对象的三个特点和SOLID五个设计原则,它们可以指导我们如何设计可以应对长期变化的软件。

    • SRP,一个类的变化来源应该是单一的。

    • OCP,不要随便修改一个类。

    • LSP,应该设计好类的继承关系。

    • ISP,识别对象的不同角色来设计小接口。

    • DIP,依赖于构建出来的抽象而不是具体类。

    基于分离关注点的结构将不同的内容区分开来,再基于SOLID五大原则将它们组合起来。SOLID五大原则也是可以树立在我们心中的标尺,作为一个标准指导我们的设计。

    如果将这些设计原则比作“道”,那么设计模式就可以称得上是“术”了,每个设计模式都是一个特定问题场景的解决方案。

    最后,感谢郑晔老师的这门《软件设计之美》课程,让我受益匪浅!我也诚心把它推荐给关注Edison的各位童鞋!

    参考资料

    郑晔,《软件设计之美》(极客时间课程,推荐订阅学习

  • 相关阅读:
    HRBUST 1377 金明的预算【DP】
    POJ 1745 Divisibility【DP】
    HRBUST 1476 教主们毕业设计排版==算法导论上的思考题整齐打印
    HRBUST 1220 过河【DP+状态】
    HRBUST 1478 最长公共子序列的最小字典序
    HRBUST 1162 魔女【DP】
    HDU 1561The more, The Better【DP】
    HRBUST 1376 能量项链【DP】
    POJ 1934 Trip【最长公共子序列输出】
    上传图片代码总结
  • 原文地址:https://www.cnblogs.com/edisonchou/p/edc_relearn_software_design_part3.html
Copyright © 2011-2022 走看看