• 《架构整洁之道》学习笔记 Part 2 编程范式


    计算机编程发展至今,一共只有三个编程范式:

    • 结构化编程
    • 面向对象编程
    • 函数式编程

    编程范式和软件架构的关系

    • 结构化编程是各个模块的算法实现基础
    • 多态(面向对象编程)是跨越架构边界的手段
    • 函数式编程是规范和限制数据存放位置与访问权限的手段

    软件架构的三大关注重点功能性组建独立性以及数据管理,和编程范式不谋而合

    结构化编程

    限制控制权的直接转移,禁止 goto,用 if/else/while 替代

    • Dijkstra 发现:goto 语句的某些用法会导致模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法将大型问题进一步拆分成更小的、可证明的部分。
    • Bohm 和 Jocopini 证明了:可以用顺序结构、分支结构、循环结构构造出任何程序
    • 测试只能证明 Bug 的存在,并不能证明不存在 Bug
    • 结构化编程范式的价值:赋于我们构建可证伪程序单元的能力。如果测试无法证伪这些函数,就可以认为这些函数足够正确
    • 在架构设计领域,功能性降解拆分仍然是最佳实践之一

    面向对象编程

    限制控制权的间接转移,禁用函数指针,用多态替代

    什么是面向对象?
    • 数据与函数的组合?
      • o.f() 和 f(o) 没有区别
    • 对真实世界进行建模的方式?
      • 到底如何进行?为什么这么做?有什么好处?
      • 面向对象编程究竟是什么?
    • 封装、继承、多态?
      • 面向对象编程语言必须支持这三个特性
    封装

    把一组关联的数据和函数关联起来,外部只能看见部分函数,数据则完全不可见。

    封装并不是面向对象语言特有的,C 语言也支持:

    point.h

    struct Point;
    struct Point* makePoint(double x, double y);
    double distance(struct Point *p1, struct Point *p2)
    

    C 语言的封装是完美的封装:利用 forward declaration,Point 的数据结构、内部实现对 point.h 的使用者完全不可见。

    而后来的 C++ 虽然是面向对象的编程语言,但却破坏了封装性:

    point.h

    class Point {
    public:
        Point(double x, double y);
        double distance(const Point& p1, const Point& p2);
        
    private:
        double sqrt(double x);
    private:
        double x;
        double y;
    };
    

    C++ 编译器需要知道类的对象大小,因此必须在头文件中看到成员变量的定义。虽然 private 限制了使用者访问私有成员,但这样仍然暴露了类的内部实现。(C++ 的 PIMPL 惯用法可以在一定程度上缓解这个问题)

    Java 和 C# 抛弃了头文件、实现分离的编程方式,进一步削弱了封装性,因为无法区分类的声明和定义。

    继承

    C 语言也支持继承:

    namedPoint.h

    struct NamedPoint;
    struct NamedPoint* makeNamedPoint(double x, double y, char* name);
    void setName(struct NamePoint *np, char* name);
    char* getName(struct NamedPoint *np);
    

    namedPoint.c

    #include "namePoint.h"
    
    struct NamedPoint {
        double x;
        double y;
        char* name;
    };
    
    // 或者
    #include "point.h"
    
    struct NamePoint {
        Point parent_;
        char* name;
    };
    
    // 省略其他函数实现
    

    main.c

    #include "point.h"
    #include "namedPoint.h"
    
    int main() {
        struct NamePoint* p1 = makeNamedPoint(0.0, 0.0, "origin");
        struct NamePoint* p2 = mameNamePoint(1.0, 1.0, "upperRight");
        // C 语言中的继承需要强制转换 p1、p2 的类型
        // 真正的面向对象语言一般可以自动将子类转成父类指针/引用
        distance((struct Point*)p1, (struct Point*)p2);
    }
    

    在 main.c 中,NamePoint 被当作 Point 来使用。之所以可以,是因为 NamePoint 是 Point 的超集,且共同成员的顺序一致。C++ 中也是这样实现单继承的。

    多态

    在面向对象语言发明之前,C 语言也支持多态。

    UNIX 要求每个 IO 设备都提供 open、close、read、write、seek 这 5 个标准函数:

    struct FILE {
        void (*open)(char* name, int mode);
        void (*close)();
        int (*read)();
        void (*write)(char);
        void (*seek)(long index, int mode);
    };
    

    这里的 FILE 就相当于一个接口类,不同的 IO 设备有各自的实现函数,通过设置函数指针指向不同的实现来达到多态的目的。上层的功能逻辑只依赖 FILE 结构体中的 5 个标准函数,并不关心具体的 IO 设备什么。更换 IO 设备也无需修改功能逻辑的代码,IO 只是功能逻辑的一个插件

    C++ 中每个虚函数的地址都记录在一个叫 vtable 的数据结构中,带有虚函数的类会有一个隐式的、指向 vtable 的虚表指针。每次调用虚函数都会先查询 vtable,子类构造函数负责将子类虚函数地址加载到对象的 vtable 中。

    多态本质上就是函数指针的一种应用。用函数指针实现多态的问题在于函数指针的危险性。依赖人为遵守一系列的约定很容易产生难以跟踪和调试的 bug。面向对象编程使得多态再不需要依赖人工遵守约定,可以更简单、更安全地实现复杂功能。面向对象编程的出现使得“插件式架构”普及开来。

    此外,面向对象编程的带来的另一个重大好处是依赖反转:通过引入接口,源码的依赖关系不再受到控制流的限制,软件架构师可以轻易地更改源码的依赖关系。这也是面向对象编程范式的核心本质(关于依赖反转,后面会单独用一篇来介绍)。

    回到开始的问题,面向对象到底什么?有许多不同的说法和意见,但是对于软件架构师来说,面向对象编程就是以多态为手段,控制源码依赖的能力。这种能力可以让软件架构师构建某种插件式架构,让高层策略和底层实现组件相分离,底层组件作为插件可以独立开发和部署。

    函数式编程

    限制赋值操作

    • 函数式编程中的变量不可变

    • 不可变性是软件架构需要考虑的重点,因为所有的并发、死锁、竞争问题都是可变变量导致的,如果变量不可变,就不会有这些问题

    • 架构设计良好的程序应该拆分成可变不可变两种组件,其中可变状态组件中的逻辑越少越好

    • 事件溯源:只存储事务记录,不存储具体状态;需要状态时,从头计算所有事务。

      • 例如银行程序只保存每次的交易记录,不保存用户余额,每次查询余额时,将全部交易记录取出累计
      • 这种模式只需要 CR (Create & Retrieve),不需要 UD (Update & Delete),没有了更新和删除操作,自然也不存在并发问题
      • 缺点:对存储和处理能力要求较高(但随着技术的发展,这方面将越来越不成问题)
      • 应用:git

    总结

    从 1946 年图灵写下第一行代码至今,软件编程的核心没有变:计算机程序无一例外是由顺序结构、分支结构、循环结构和间接转移这几种行为组合而成的,无可增加, 也缺一不可。

    所有三个范式都是限制了编码方式,而不是增加新能力

    • 结构化编程:限制控制权的直接转移,禁止 goto,用 if/else/while 替代
    • 面向对象编程:限制控制权的间接转移,禁用函数指针,用多态替代
    • 函数式编程:限制赋值操作

    三个编程范式都是在 1958 - 1968 年间提出,此后再也没有新的范式提出,未来几乎不可能再有新的范式。因为除了 goto 语句、函数指针、赋值语句之外,也没有什么可以限制的了。

  • 相关阅读:
    在线招聘江湖:老、中、新三代平台对垒
    16.1 Socket 端口扫描技术
    Qt之自定义带游标的QSlider
    第十九课,编写并调用自定义函数
    【Spring】——4、使用@Scope注解设置组件的作用域
    MySQL之账号管理、建库以及四大引擎+案例
    Zustand 和 React 上下文状态管理
    电子电器架构 —— 车载网关边缘节点总线转换
    blender 快捷键 常见问题
    Flink--- 批处理 / 流处理
  • 原文地址:https://www.cnblogs.com/tengzijian/p/17557427.html