< 返回我的博客

北海 发表于 2023-12-15 16:40

Tags:面向对象

前言

我个人是修炼了多年的面向对象技术,这一点,相信很多同道中人跟我一样,经历了面向对象的洗礼,毕竟面向对象技术从上世纪七十年代就开始展露头角,是当时的先进编程思想,其理念符合人类对事物的认知方式;近二十多年,都是面向对象技术盛行。面向对象发展至今,有很多优美设计,诸如经典面向对象设计模式中的策略模式、装饰器模式等等,做UI系统时,组合模式+装饰器模式的绘制、ui树管理,别提多优美了,职责链处理UI交互事件,完美应对事件冒泡,简单又令人惊叹。继承、封装、多态,从它们被发明以来,一直有它伟大的意义。要说,Rust超越了面向对象,那内心还得问一句,装饰器在Rust中怎么实现,能不能实现的比面向对象更简单、易懂且优雅?当然,面向对象,也有其缺点,为人诟病的有对象间的相互调用关系混乱,将对象视为有限状态机封装导致的不利于并行等,这些表明面向对象不似它宣传的那么好,并不适合所有场景,这些场景呼唤更好的解决方案。

在Rust社区有不少对面向对象的讨论,这种讨论非常有意义,体现了某些编程设计上本质的东西,这也是我喜欢Rust这门语言的原因之一。作为Rust初学者,一开始我还不是很了解Rust所谓的“超越面向对象”体现在哪里,只能说一知半解、半推半就。一开始会觉得Rust对面向对象批判之后,然后就提了个Trait+Enum和类型的解决方案,就这?代替面向对象那么多设计模式的方案也没多少,就一个更严格的NewType状态机?面向对象可不止一个状态机模式!不免让人生疑:Enum在module之外,其子类扩展性何在?面向对象的诸多优秀设计,还能在Rust语言上良好表达吗?

有时会觉得,面向对象有时候比Rust的Enum、Trait表达能力差的并不多,甚至更好,却足以引发舌战;Rust有时比面向对象强的也不多,也足以引发对面向对象的批判。这种争论本身也没啥好不好,争论多了,自然就能发现什么了。但有时候,我们就是这么容易争论,而实际上,我们连对争论的本质问题是什么,都没搞清。

对Rust入门以后,再看Rust的设计,能意外地发现它想表达的东西了,很开心。现在,把这些对面向对象和Rust理解整理对比下,很多文章都介绍过,这一系列文章,将采用Rust尝试实现各种面向对象技巧的方式来叙述,目标是寻找Rust达成这些技巧的替代方案,同时搞清楚面向对象设计的问题在哪里,及Rust的改良又体现在哪里。 并不否定面向对象技术,也不会通过贬低它来宣示Rust的好,各有值得学习的东西,都有自己适用的领域。非要说的话,面向对象技术现在更普及易懂,是大部分编程人员的必修课,应该沉淀为基操;Rust则更艰涩高深,但其逻辑表达确实更基础,潜力更大。

面向对象之结构继承

一切先从最简单的开始。面向对象届有句老话:“多用组合,少用继承”,被奉为圭臬,很多对代码的批评就源自于没遵守它。让我们对比一下:

struct A {
    x: int,
}

struct B1 extends A {
    y: int,
}

struct B2 {
    a: A,
    y: int,
}

B1好,还是B2好?其实无法回答的。然而,看下B1、B2这两者的内存布局,会惊讶的发现,它们其实是一样的。

看到这里,我第一次怀疑,这么多年的“继承” vs “组合”的争论,是否有必要,我们怎么会这么傻,为了一个相同的东西,还能喋喋不休了这么多年🐶。

不过,有人可能会问了,这种只是内存布局一样,实际上能一样吗,B1、B2对x的访问是不一样的。

// B1是这样访问x的
int get_x = b1.x;

// 而B2是通过a访问的
int get_x = b2.a.x;

确实不一样,但实际上b2.x可以看成b2.a.x的语法糖。到最终编译到的汇编语言层面,真也的确如此。Rust语言可以轻松使用Deref特质实现该语法糖,连调用A的方法都能一并简化。

impl Deref for B2 {
    type Target = A;

    fn deref(&self) -> &Self::Target {
        &self.a
    }
}

所以,别在讨论,该用组合还是该用继承了!继承本身就是组合,还有啥好讨论的,就算是多继承的概念,也等同于组合多个不同结构。

了解到这一点,对底层而言,"is-a"就是"has-a",高级语言为其发明了“继承”,此时显的多此一举。底层结构上都一样,所以go语言的结构继承,看上去就是组合,rust亦如此。继承就是一种特殊的组合!

但组合可不仅仅是继承,组合变化更多。组合还可以包含一个“指针”型结构,"has-a-ref",这在链表、树这种自包含结构里尤其关键;组合还能包含一个集合——"has-many","has-a"都可以看成是"has many"的特例,比如树包含不止一个枝干。

// 表达能力上,B3更强,B1、B2都可以用B3来表示,B2不过是B3中包含长度为1的向量
struct B3 {
    a: Vec<A>,
    y: int,
}

面向对象的一个尴尬就是,本身继承的底层结构是组合,功能上也不如组合,却把“继承”提高到“三大概念”的核心层次,因小失大,以偏概全。从这点看,"has-a"拥有相同结构,加上"is-a"的语法糖,所以go、rust,概念更少,还能表达出“继承”,亦不失组合的含义,更受欢迎。

但话说回来,面向对象,其实有个关键的语法习惯改进,即以对象为主语的调用方式,类自然语言的“主-谓”或者“主-谓-宾”语句,终于不用主语倒置了,现在大部分语言都是默认如此了,Rust也是如此。这时候再考虑继承,若论直接使用父类的谓词行为,则是“is-a”的继承独有的,“has-a”/“has-a-ref”都要借助语法糖或者重新实现父类接口来表达这种"is-a"父类的行为。“has-many”如果也能表现出"is-a"的特性,那就是经典的组合模式了。不过大多时候是“has-many”表现不出“is-a”的特性,仅仅是一种集合管理。

impl DrawWidget for A {
    fn draw(&self, canvas: &mut Canvas) {
        ...
    }
}

// B1已天然实现了DrawWidget,仍可选覆盖实现
// impl DrawWidget for B1 { ... }

// B2则需要实现“is-a”。在Rust语言里,即便B2实现了Deref,也不代表着
impl DrawWidget for B2 {
    fn draw(&self, canvas: &mut Canvas) {
        self.a.draw(canvas);
        // draw y
    }
}

// B3是“has-many”,但本身也可以看成是一个Widget的话,那就是面向对象中经典的组合模式
impl DrawWidget for B3 {
    fn draw(&self, canvas: &mut Canvas) {
        for child in self.a {
            child.draw(canvas);
        }
        // draw y
    }
}

总结一下,"has"包括

  • "has-a"
    • 若意义等同于"is-a",则为继承;包括"has-(a,b)"等同于"is-(a,b)"型,多继承概念
    • 若不等同于"is-a",则为简单的包含关系,如一个数据包包含包头和包体,一个分数包含分子和分母等
    • 有时候,即可以用继承,也可以用组合,比如代理模式,就有继承代理和组合代理2种,其结构本相同,何须再分出你我,这就是面向对象不必要的高级概念复杂化
    • "has-a"还可以是"has-a-ref",C/C++中包含一个其他结构的指针成员,内存结构不同于继承,却也能形如继承,是组合的一种,链表这类自包含结构必备
  • "has-many"
    • 若还有跟"is-a"一样的特性,就是组合模式
    • 普通的集合管理

回到面向对象语义/语言,要问该选哪一种,就看那种能更精准地表达,猫是动物、鸭子是动物,这种就是继承,猫、鸭子继承了动物,肯定比猫、鸭子包含一个动物结构好。而树包括枝干,就是包含关系好。高级语言,对同一结构不同表达,怎么方便人理解怎么来,如此而已。

在Rust语言中,则没了继承的概念,都是组合。因为Rust的Deref,让Rust保留了继承的部分功能性,并没有关闭面向对象的大门。但需注意,B2并未因Deref自动继承实现A所有的特质。Rust舍弃了高级语言复杂的“继承”概念,把底层是什么样就是什么样的组合原汁原味地展现出来,同时保留下其他变化,既继承的弊端可以被摒弃替换成组合或者特质实现的变化,这种变化也许才是那更常见的大多数情况,废除“继承”可能会是未来语言的标准做法。

结束语

本篇到这里暂时就结束了,因为篇幅不宜太长,先行文至此,一篇围绕一个小主题。本文仅仅讨论了面向对象的结构继承。面向对象的结构继承,其实狭隘了,从内存结构布局上看,仅仅是组合的一种特例,还不如说成就是组合,组合意义更广泛。有时候,不妨把底层的概念直接暴露出来,也没增加复杂度,理解上会更直白。

Rust也为结构继承留下了Deref的方案,不过请留意,Deref并没让子类自动继承实现父类的特型,只是一个解引用的语法糖,而且一个结构只能实现一次Deref到一个Target。Deref并非仅为结构继承而生,Rust也没怎么提倡用Deref式的继承,官方文档从来没说它是用来对标“继承”的,倒是不少开源项目,拿它映射继承,如果符合“is-a”的意义,还挺适用的。

继承仍有些问题、冲突没展示出来,后面再继续探讨。包括Deref这种具备关联类型的特质,它到后面也不再仅是一个语法糖,在表达实现逻辑语义时,子类没必要实现父类的特型,在特型限定时它们将有更丰富的逻辑表达意义。

评论区

写评论
jiahao6635 2024-04-08 16:35

写的好,支持,催更!!

作者 北海 2023-12-24 14:35

[抱拳],哈哈,努力写好,献给所有喜欢Rust和面向对象的知音圈子

--
👇
wukong: 写的很好,从 deref 的角度看组合和继承的,很有启发,期待更新!

wukong 2023-12-22 14:01

写的很好,从 deref 的角度看组合和继承的,很有启发,期待更新!

作者 北海 2023-12-22 11:30

感谢Mike老师支持~

--
👇
Mike Tang: 支持支持!鼓励原创,公众号已经推了哈。

Mike Tang 2023-12-21 23:46

支持支持!鼓励原创,公众号已经推了哈。

作者 北海 2023-12-20 23:43

感谢支持,不好写啊,来回修改才能表达出来,我加油写好🙏

--
👇
ribs: 写的好,支持,催更!!

ribs 2023-12-16 17:00

写的好,支持,催更!!

1 共 7 条评论, 1 页