以面向对象为核心的多范式编程

《以面向对象为核心的多范式编程》源站链接,阅读体验更佳~
《如何写出更优雅的代码——编程范式简述》一文中,我们介绍了结构化编程、面向过程编程、面向对象编程和函数式编程这几种最基本的编程范式。

其中的结构化编程是最为底层的编程范式,没有之一,它甚至不会影响开发者对程序的看法,它主要规范的是程序的控制流的编写方式;而面向过程编程、面向对象编程和函数式编程都会影响甚至决定开发者对程序的看法,本质上是会对程序的组织方式产生深刻影响的。

面向过程编程、面向对象编程和函数式编程不仅是三种最主流的编程范式,更是三种最为主流的程序设计方式,它们不仅仅是套招,更是思维方式。对于面向过程编程、面向对象编程和函数式编程的基本特征,在《如何写出更优雅的代码——编程范式简述》一文中我们已经做过介绍了,这片文章中我们将从程序组织方式的角度出发重新对这三种编程范式进行讨论。

面向过程编程和面向对象编程

在长期的编程实践中,我们逐渐达成了这样的共识——面向对象编程是比面向过程编程抽象层次更高的一种编程范式,因为面向过程的思维方式更加贴近计算机,而面向对象的思维方式则更加靠近人类的思考方式。因为我们在编写复杂业务逻辑的时候其实就是在对现实世界进行建模,而我们对现实世界的认知其实就是基于事物的,比如我们在对一个电商系统进行建模的时候,首先就会想到商品、订单等等事物,这在面向对象中自然而然就产生了‘对象’的概念。

然而,人类在处理问题时候的思维方式又和计算机是别无二致的,都是基于‘过程’的,比如我们在思考电商的基本业务流程的时候,会想到这样的过程——用户从商品列表中选择自己喜欢的商品,然后下单产生订单,然后支付等等。

也就是说,人类认识世界的方式是面向对象的,而处理问题的思维方式又是面向过程的,这就导致面向过程编程和面向对象编程是密不可分的,这也是导致我们虽然使用的是支持面向对象编程范式的编程语言,而写出的代码往往是面向过程风格的原因。关于我们使用面向对象编程语言仍然容易写出面向过程风格的代码这一点,在下一篇文章中会有更加深入的讨论。

下面,我们先来简单说一下,面向过程编程和面向对象编程在代码组织方式上的不同。

foo(obj) vs obj.foo()

《如何写出更优雅的代码——编程范式简述》一文中我们说明了,面向过程编程中的数据的定义和行为的定义是分开的,我们要定义数据,定义操作数据的函数,然后通过函数之间的相互调用来完成我们的业务逻辑;这落实到编码上大致的形式是这样的(使用C语言伪代码进行描述):

// 定义数据结构
typedef struct {
  // 结构体中属性的定义
  ...;
} DataStruct;
// 定义操作数据结构的函数
void foo(DataStruct * const dataStruct) {...}
// 主函数
int main() {
  // 初始化结构
  DataStruct dataStruct = {...};
  // 调用函数并传入数据结构进行操作
  foo(&dataStruct);
}

而面向对象编程中,数据和操作数据的函数式定义在一起的,落实到代码上大致如下(用Java语言伪代码进行描述):

class DataStruct {
  // 类属性的定义
  ...;
  
  // 方法
  public void foo() {...}
}

// 主类 & 入口函数
public class MainClass {
  public static void main(String[] args) {
    // 创建对象
    DataStruct dataStruct = new DataStruct(...);
    // 通过新创建的对象调用对象方法
    dataStruct.foo();
  }
}

通过对上面两段伪代码的观察,其实我们可以发现,不管是面向过程还是面向对象,编程的整体流程似乎都是一样的——我们首先要对外提供一个入口函数,然后在入口函数中嵌入我们的具体逻辑。这其实就是面向过程编程的基本编程模型。我们惊讶的发现,其实面向过程编程和面向对象编程的起手式竟然是一致的,那面向对象的意义是什么呢?其实我们不难发现,在主函数中调用下层函数的方式有所不同。

面向过程中字程序调用的基本格式为:foo(obj),而面向对象编程中子程序的调用基本格式为obj.foo()。当然这个说法只是针对大多数情况下的。

那么obj.foo()这样的调用格式相比于foo(obj)这样的格式有哪些好处呢?从我个人的编程经验来看,体会最多的有如下两点:

  • 避免调用地狱

    大家一定都听说过嵌套地狱的问题,嵌套地狱指的是在多层的嵌套循环或者是嵌套分支结构中造成的代码结构杂乱和可读性严重降低的问题;如果你平时使用的编程语言是JavaScript,那么你也应该听说过回调地狱。

    foo(obj)这样的调用格式在书写单行代码的时候也很容易形成嵌套地狱,现在假设我们调用的所有的函数都是具有返回值的,而我们要对整个调用过程中的数据做链式的处理,那么我们就有可能写出如下的代码:

    e(d(c(b(a(data)))));
    

    上面的代码中只是在一行代码中嵌套调用了5个函数,就已经令人眩晕了。当然在面向过程中我们也很少这样去编码,为了可读性,通常我们都会将上述的一行代码拆分为5行:

    TypeA aRes = a(data);
    TypeB bRes = b(aRes);
    TypeC cRes = c(bRes);
    TypeD dRes = d(cRes);
    e(dRes);
    

    而如果是面向对象中的obj.foo()这样的调用形式,我们就可以将所有的函数调用串成一个链而不会产生调用地狱问题,并且代码的可读性和上面五行代码的写法几乎没有差别:

    data.a().b().c().d().e();
    

    通过上面的对比,我们可以发现obj.foo()这样的调用形式具有更强的表达能力和可读性。

  • 更好的IDE支持

    现在,我们大多数人的编程环境都是集成开发环境(IDE),而现在的IDE都具有非常强大的功能,其中一点就是代码提示的功能。

    如果我们使用的是面向对象中的obj.foo()这样的调用形式,那么IDE就可以非常方便的为我们提示出通过obj可调用的方法有哪些;而如果我们使用的是面向过程的foo(obj)这样的形式,则我们必须知道自己需要调用的函数的名称,至少需要知道函数名称的首字母是什么,否则IDE很难给出我们合理的提示。

面向对象的封装、抽象、继承和多态

上面我们提到的foo(obj)和obj.foo()是面向过程编程和面向对象编程最表面上的差异,同时我也认为这是二者之间最本质上的差异。

前面我们已经不止一次提到过,面向对象中会把数据和操作数据的算法放在一起声明,从而形成一个对象,这是面向对象最基本的特点。由面向对象的数据和算法在一起声明的基本特点所引发出来的其实就是我们耳熟能详的面向对象四大基本特点:封装、抽象、继承和多态。这片文章中,我会以DIY主机的场景来对面向对象的这四大基本特征做一个简单的说明,详细说明留在下一片文章中进行。

首先,我们做一个简单的思考,因为对象同时声明了数据和算法,如果对象可以选择性的对外暴露自己的部分数据和算法,甚至只对外暴露算法,所有对对象数据的操作都必须通过对象暴露在外的数据和算法进行,那么我们就实现了对对象所属数据的访问控制,这其实就是面向对象的封装特性;比如我们DIY主机,其实我们不用过多关心选择什么样的CPU、显卡、硬盘、电源、散热器等硬件,够用就好,最后我们使用我们的主机的时候其实只需要关心为我们的主机烧入什么样的操作系统,而直接影响我们使用的硬件设备其实只有显示器和键盘鼠标。在DIY主机这个场景中,我们的个人计算机其实只对我们暴露了它的操作系统、显示器和键鼠套,这就是一种封装。

再进一步,如果我们将对象必须对外暴露的数据和算法抽取出来做一个声明,那么我们就得到了一个基本的协议,这个协议就像是对象的使用说明书,我们使用对象的时候可以按照说明书的说明进行使用,同时,被使用的对象(实现方)则必须提供说明书上所说明的所有功能,这其实就是一种抽象。再回到DIY主机的场景,在实施DIY计划之前,我们就需要对我们主机做一个基本的描述:必须能够流畅运行3A游戏大作、能够快速编译和运行源代码、必须具有4k的显示器、要有机械键盘、鼠标要能多档调节DPI等等。有了这些基本的描述之后,我们就可以基于这个描述来搭建我们的主机了,基于上面的描述,我们的CPU、显卡、内存、硬盘、显示器、键鼠套等硬件都应该选择比较高档的,至于选择什么样的品牌,那就非常多了,最后可以组合出n多种的实施方案。上面我们对个人主机的描述其实就是一种抽象。

我们接着往下思考,假如我们已经有一个现成的对象了,现在需要提供一种新的对象,和原来的对象相比,我们的新对象只有几个算法有差别,这个时候我们有没有一种办法让新对象具有老对象的所有能力,同时对需要改进的能力进行覆盖和增强呢?继承就是为了这个场景而出现的。再次回到DIY主机的场景,如果我们的主机需要升级换代了,我们需要从头到尾把所有的硬件都升级一遍吗?其实大可不必,我们可以单独更换CPU、单独更换显卡,等等硬件,而没有替换过的硬件就被我们的“新”主机继承下来了。这里所体现出来的其实是一种重用,其实面向对象中继承的出现就是为了解决代码重用问题的。

最后,面向对象现在已经有了封装、抽象和继承,这可以解决一部分问题,但是也引入了新的问题,这些新的问题主要是由抽象和继承所引起的。对于同一个声明,它可能不止一个实现方,同时,一个对象有可能还会被其他的对象所继承,这个时候就出现了一个问题,对于同一个抽象,实际的类型可能有很多种,但是,本质上它们都具有相同的外观——它们的使用说明书是一模一样的;这就意味着,我们在使用这个抽象的时候是不需要关心具体的实现是什么样子的,这种情况下我们当然是希望使用抽象声明的时候,任何一个实际的实现都可以顶上去,而每一个被继承的对象也可以被继承它的对象所替换。这其实就是多态。多态体现的其实是一种is-a的类型兼容关系,在基于类的面向对象中表现为子类对象同时也是一个父类对象,在基于原型的面向对象中则表现为本对象同时也是其原型对象。

再回到DIY主机的场景中,现在市场上主流的个人计算机分为Windows和Mac两个平台,作为一个程序员,天天被大家安利Mac系统,最终自己经受不住诱惑购入了一台Mac Mini,现在我就拥有了两台主机,那么我们该怎么在两个主机之间进行灵活切换呢?我们可以借助一个KVM切换器来让我们的Windows主机和Mac主机共享一套屏幕和键鼠,这个时候我们的显示器上面所输出的画面可以是PC主机的Windows操作系统的画面,也有可能是Mac Mini所输出的MacOs的画面,对于显示器来说,无论是Mac Mini还是PC主机,都具有HDMI接口或者是DP接口这样的视频信号接口,而对于键盘鼠标来说二者也都有USB接口,也就是说,对于显示器和键鼠来说,主机只要是遵循了这些硬件接口,就可以被显示器和键鼠使用,这就是一种多态。

简单分享一下我的“面向对象”个人桌面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-60yo0T9m-1679230995991)(http://qn-oss.laomst.site/20230302_010919873_1.png)]

面向对象编程和面向对象编程语言

上文中我们已经简单介绍了面向对象编程的四大基本特征,虽然以DIY主机为例子进行介绍并不是那么贴切,但是相信也可以让大家对面向对象的基本特性有一个感性的认知。

类和对象这两个概念最早出现在1960年,在Simula这种编程语言中第一次被使用,而面向对象编程这个概念第一次使用是在Smalltalk编程语言中,Smalltalk也被认为是第一个真正意义上的面向对象编程语言。1980年左右,C++语言的出现带动了面向对象编程的流行,直到今天,如果不按照严格的定义来讲,大部分语言都是面向对象编程语言,比如 Java、C++、Go、Python、C#、Ruby、JavaScript、Objective-C、Scala、PHP、Perl 等等。

上面我们简单介绍了面向对象编程的四大核心特性,同时说明如果不严格卡定义的话,现在大部分的编程语言都可以认为是面向对象编程语言,那么什么样的语言才算是严格意义上的面向对象编程语言呢?

面向对象编程时一种编程范式或者编程风格。它以类或对象为组织代码的单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石,那么,面向对象编程语言则是支持类或者对象的语法机制,并有现成的语言特性可以方便的实现面向对象编程的四大基本特征(封装、抽象、继承、多态)的编程语言。

其中最为核心的其实就是类或者对象的语法机制,只要拥有了类或者对象的语法机制,我们就可以实现将数据和操作数据的算法放到一起进行声明,这是面向对象编程最基本的对外表现形式。所以我们在刚接触一门编程语言的时候,只要他拥有某种语法机制可以让我们把数据和操作数据的算法放到同一个程序实体中进行声明的语法机制,我们都可以认为它是面向对象编程语言。

上文中也简单提到了,将数据和操作数据的算法放到同一个程序实体中声明的语法机制主要有两种,一种是类,一种是对象,这其实就对应了面向对象编程语言最基本的两种流派——基于类和基于原型。而基于类的面向对象编程语言则更加多一些。在基于类的面向对象编程语言中,类是事先定义的,对象是基于类实例化的。在基于原型的语言中,对象是主要实体,甚至不存在类。关于基于类的面向对象和基于原型的面向对象,我们在下一篇文章中会有更加详细的介绍。

事实上,在我们进行面向对象编程的时候,大多是使用面向对象编程语言进行的。但是,面向对象编程并不一定非要使用面向对象编程语言才可以实现,因为面向对象编程是一种编程风格或者是程序设计思想,是一种代码设计思维方式,即便是连类或者对象这样最基本的面向对象语法特性都不支持的语言,只要支持函数指针或者是函数式一等公民,我们同样可以把函数视为一种特殊的属性来自己实现面向对象编程,写出面向对象编程风格的代码,如果感兴趣的话可以阅读一下《扩展阅读——在C语言中实现面向对象编程》这篇文章。

从这个方面来讲,面向对象编程其实是一种受限的面向过程编程,它约束的是数据和操作数据的算法的组织方式,让数据和操作数据的相关算法具有更强的内聚性,而类或者对象的语法机制更是让数据和操作数据的相关算法聚合成为一个原子,成为一个最小的程序模块。

上面我们已经简单介绍了面向对象的核心特性,下面我们再来简单聊一聊函数式编程。

函数式编程和函数式编程语言

《如何写出更优雅的代码——编程范式简述》一文中我们也已经对函数式编程进行过一个简单的介绍,并说明了函数式编程中最核心的几点:函数是一等公民、lambda表达式和高阶函数。其中函数是一等公民是最为关键的,只有函数是一等公民的情况下,我们才能把函数视作一个特殊的数据类型,才能把函数当作函数的参数和返回值,而高阶函数则是函数是一等公民这个特性所自然产生的。而lambda表达式则为我们提供了在运行态计算一个函数的能力。借助这三个特性,我们可以实现函数的运行态组合使用。

《如何写出更优雅的代码——编程范式简述》一文中我们也简单介绍了函数式编程和声明式编程之间存在一定的关系,其实函数式编程中的概念和玩法还有很多,比如纯函数、函数的副作用、闭包、柯里化、偏应用等等。关于函数式编程的更多细节我们后面会有专门的专题进行介绍。这篇文章中只介绍函数式编程最粗浅的应用。

这里,对于什么样的编程语言可以被称为函数式编程语言我们同样放宽限制,只要支持函数是一等公民和lambda表达式这两个特性的编程语言,我们就认为它是一门函数式的编程语言。

函数的运行时组合

上文中我们提到过,函数式编程给我们提供了一种函数运行时组合的手段。

我们平常所说的函数的相互调用其实大都是一种静态期的绑定关系,如下代码:

public class Test {
  public void foo(int x) {
    ...;
  }
  public void goo() {
    int x = 2023;
    ...;
    foo(x);
    ...;
  }
  public static void main(String[] args) {
    Test obj = new Test();
    obj.goo();
  }
}

上面的代码中,goo方法中调用了foo方法,这种函数的调用关系其实是在代码的编译阶段就已经固定下来了的,我们无法在程序的运行过程中对goo方法所调用的foo方法进行替换(我们这里不考虑继承和多态的情况)。

如果我们采用函数式的写法,把goo方法变成一个高阶函数,这个时候我们就可以在程序的运行过程中有选择的为goo方法提供所需要的函数了:

public class Test {
  public void foo(int x) {
    ...;
  }
  public void goo(Consumer<Integer> foo) {
    int x = 2023;
    ...;
    foo(x);
    ...;
  }
  public static void main(String[] args) {
    Test obj = new Test();
    obj.goo(obj::foo);
  }
}

在上面的13行代码中,我们用方法引用的方式将foo方法传入goo方法中,如果有需要,我们也可以传递其他的函数进去,甚至我们可以通过lambda表达式计算一个函数进行传入,而不仅仅局限于foo方法。

从上面的例子中,我们可以感觉到,函数式编程为我们的程序编写提供了极大的灵活性,同时它提升了我们代码的抽象层次。

等到后面我们介绍面向对象编程中的一些设计模式的时候,我们会发现有一些设计模式其实可以用函数式编程的方式进行替代,不仅实现起来更加简单,而且程序的可读性会比使用设计模式更好。

总结:以面向对象为核心的多范式编程

上文我们介绍了面向对象和面向过程最大的不同点——把数据和操作数据的算法放到一起进行声明,形成一个最小的程序模块(类或者对象)。由于这种程序组织方式上的不同,让面向对象可以具有封装、抽象、继承和多态这样的玩法和特性。而在此基础之上,函数式编程可以作为我们算法实现方式的一种很好的扩展方式——算法可以在运行态进行组合使用。

至此,我个人的编码风格就已经形成了——整体上,代码的组织方式是面向对象的;实现对象内部的算法的时候,使用面向过程的编程方式,同样的,在实现某个具体的功能的时候,也是使用面向过程的方式进行思考,但是过程中的核心功能是由一个个对象负责的,而不是由函数承载;在实现算法的时候辅以函数式编程来提高算法的通用性和灵活性。

我个人称这种编程风格为以面向对象为核心的多范式编程,其实所围绕的就是面向过程编程、面向对象编程和函数式编程这三种最基本的编程范式之间的相互配合。

其实想要做到这三种基本编程范式之间的灵活运用和相互配合需要进行长时间的训练和思考,而我在日常的工作中发现,其实很多人都不能真正理解什么是面向对象编程,甚至有的人认为只要是使用面向对象编程语言编写出来的代码就是面向对象的,这其实是大错特错的。

我们上文中也简单说过,虽然我们认识世界的方式是面向对象的,但是我们处理问题的方式仍然是面向过程的,这就导致我们写代码的时候自觉不自觉的就会写出面向过程风格的代码。而要真正写出面向对象风格的代码,重中之重是理解面向对象的核心特性,以及理解什么是面向对象设计,所以,下一篇文章我们会继续深入聊一下面向对象。

以上就是我对我个人编程风格的介绍,本人深知自己技术水平和表达能力有限,文章中一定存在不足和错误,欢迎与我进行交流(laomst@163.com),跟我一起讨论,修改文中的不足和错误,感谢您的阅读。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

劳码识途

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值