内功修炼系列(壹):面向对象思想

面向对象是一种编程思想,而不是一种语言,更不是Java本身。Java也同样可以写出面向过程的程序,C语言也能使用面向对象的思想去编程。
本篇是《编程的逻辑:用面向对象方法实现复杂业务需求》一书的前两章的读书笔记,原作是李运华,内容掺杂原书作者的内容和自己的思考+总结。

OOP的发展历程

程序设计思想的演变是从无到面向过程,再从面向过程当中发现不足进而发展出来面向对象OOP。从无到面向过程,突破的点是——人们编写程序不需要关注具体的机器、指令集、存储介质等硬件因素。从这里开始有了所谓的高级语言。相比原来,程序更好编写和维护。

那么从上面时候开始发现面向过程思想的不足呢?上世纪中后期人们发现软件质量和按时交付率随着软件规模的不断变大、复杂度的不断变高而变得不尽人意,屡次出现了很多严重的事故。这被人们称为“软件危机”。

软件危机促进了软件工程的发展,针对软件危机,人们也作出了很多批判(比如“goto是有害的”/“我们需要模块化来控制程序复杂度”...)。大家解决这个危机的初步尝试的产物是结构化程序设计思想的出现,特征是"自顶向下、逐步细化和模块化",典型的程序语言有Pascal。

初步的尝试其实也很快面临问题,因为Pascal本质上还是面向过程,虽然结构化一定程度上控制了复杂性的问题,但是随着变化的增加,人们开始要求软件要具有可扩展、可维护性。人们第二次解决软件危机的行动产物,就是发明了诸如CPP、Java等著名的面向对象编程语言,这种支持OOP的编程语言结合OOP一直发展到现在。

面向过程和面向对象的比较

什么是面向过程,具有面向过程的编程思维是怎么样的?考虑一件事,使用面向过程的思维就是在思考“做完这件事需要什么步骤?”。类似一个流水线一样去划分、衔接工序,传递着处理产物。可以这样说,在面向过程的思想当中,程序=算法+数据结构。算法就是我的流水线,数据结构则是我上下游工序的输入、产出的物件的描述。

面向对象则不一样,如果使用OOP的思维方式,会考虑“做这件事情都有谁参与?他们长什么样?”。

就好像打篮球,想要组件王朝球队,我需要一个体重轻盈(最好不要超过80KG)的、跑动灵活的控卫,还需要一个稳如泰山(体重100KG以上),精通挡拆战术的中锋...。至于为了拿到季后赛的冠军这件事情或者是赢得下一场比赛如何打、使用什么战术、什么时候换人这些事情等到具体的变化、场景出现的时候再来考虑。

可以这样说,OOP的思想当中,程序=对象+交互。我定义他是什么样的,再编程的时候让他们之间交互。

面向对象的长短处

面向对象的好处就是拥抱需求的变更,打篮球和流水线生产东西不一样,流水线生产东西有明确的约定,如果上下游工序修改,则影响整体的运行。打篮球这件事本身就是拥抱变化的,如果说打篮球的战术等价于算法,那么我对象可以支持的算法就五花八门。这就叫扩展性。

可扩展性带来的好处主要迎合了不断变化的需求,经常变化的场景就适合使用面向对象思想。这是OOP思想的长处.

其实在一些基础、稳定不变的基础软件上,比如数据库、OS。OOP的长处“拥抱变化”其实就不那么明显。甚至有时候会因为远离底层导致性能方面略逊一筹。

衡量一个软件质量好有很多维度,比如:可扩展性、成本、性能、可靠、安全、可维护、可移植、可伸缩。OOP并不是所谓的“六角战士”,除了可扩展性,OOP这种编程思想基本上都拿不到很高的分数。

说在开篇的一些解释

如何使用C语言来运用OOP思想去写出OOP的代码呢?典型例子就是Redis,它的源码中的事件处理,在不同的OS有不同的实现,Linux使用epoll内核系统调用,Unix使用select内核系统调用... 。如果是面向过程,那么他可能会这样实现:

if(osname=='UNIX'){
  select();
}else if(osname=='LINUX'){
  epoll();
}

但是Redis并不使用面向过程的方式实现它,它定义了一些接口:

  1. 创建事件
  2. 添加事件
  3. 删除事件

使用不同的实现方式去实现这个接口,再在事件处理的流程当中统一的去处理。

也许我们最早学Java写出来的代码就是面向过程的,在最早学习Java的时候,main方法并不是整个程序的一个入口,main方法是整个程序的全部。我们会定义这个类一些属性和方法,再以main方法为所有动作的施展之地,一行一行的调用我们的方法去实现我们的流程,这就是面向过程的思想的实践。可见思想的选择和施展并不囿于语言本身。

什么组成了我们的面向对象理论?

有人说类是一组对象的抽象,这个解释毛病在用不确定的概念解释不确定的概念,有人说类是属性和方法的集合,这个解释的毛病在于过度的浅显,将代码和我们的"类"这个概念划等号。

类的定义是:一组相似事物的统称。 站在你自己的角度,具有相似点的事物就可以称为一类。

类的组成有哪些:

类=属性+方法。

如何去抽象一个事物为一个类,首先要对事物要素识别,将其相似点抽取出来称为属性,属性的抽取的要求为“属性不可以再被分割”。这里很像我们设计数据库表字段的时候,你的老师会要求你设计的字段是不可以再分一样。

这叫做属性最小化原则。

事物有他自己的动作,描述事物的时候看到描述里面的动词,这个动词就可以被识别为一个方法。设计类的方法的时候,注意一个基本原则——方法单一化原则。也就是一个方法只干一件事情。

对象

对象是具体的类,是真实世界的模拟,是一个个体,存在于软件运行的过程中。

小总结一下:现实有相似点的事物归纳为现实意义上的类,现实意义上的一类事物抽象为软件中的类,软件中的类实例化称为对象,程序在运行的时候就使用这些对象。

软件中的类一般都能对应上现实意义上的一类事物,但是有的软件类是对应不上的,比如“策略”一词,我就可以把策略当做一个类。

接口

归咎于接口这个中文翻译的晦涩性。日常开发当中接口一次是最被滥用的一个词语,一个interface是一个接口,一个API也是一个接口。从英语词汇的角度来看,interface可以被拆开两半来理解。

  • inter是交互的意思,常见的词汇有International,Internet等,International是nation的交互,Internet是net网络的交互。
  • face比较简单,就是面部,面的的意思。

交互其实好理解,但是face的话,你可以认为一个face就是一个展现、一个传达。你可以认为这是一个功能。

我多个face进行交互,是我想要告诉外界,我又很多信息要传达。这个功能是很多样的。

总结一下接口的定义:接口是一组相关的交互功能点的定义的集合。结合实例,一个接口,包含一种类(比如UserDo)的CRUD,那么C或者是R或者是UD也好,其实都是一个个功能点,由于接口是不会实现的,那么其实就是功能点的定义,我有四个(even more)这种功能点的定义,那么其实它就是一个定义的集合。

话说回来,面对被滥用的接口一次,我们要学会分语境来判别它使用的是否正确,比如前后端联调,前端调用查询接口,那么这个“查询接口”只能是一个功能点,多个功能点才会组成一个真真的interface。

Java中的interface是如何体现OOP当中接口的特征的呢?

  1. 接口可以定义多个功能,从1~N个都可以。
  2. 接口必须为public的修饰符,体现了inter前缀该体现的交互性。
  3. 接口没有实现(这里不讲default和static
  4. 接口里面的方法一般、标准的情况下适合一类相关的。

那么什么时候使用接口?当你不想知道对象所属的具体的类,但是想要知道这些类有什么功能的时候,就使用他!

抽象类

抽象类是一种特殊的类,特殊在:

  1. 不能实例化
  2. 只能继承 extends,不能implements

“存在即合理”,为什么要存在一种限制性很突兀的东西,一般事物的限制性很强烈、很突兀的时候,往往意味着它有另一方面强烈的好处,只是一般人很少意识到而已。

抽象类是更高层次的抽象,和普通类不同,比如苹果、草莓和菠萝都隶属于水果,那么水果就是更高层次的抽象。抽象类的特点是可以有自己的抽象方法,即只有声明和定义,并没有实现(这里非常类似接口,作用也异曲同工——“不想知道这个抽象运行的时候具体属于那个类,但是我想知道你能做什么”)。

既然抽象类和接口很类似,那为什么还区分这两个东西?——因为抽象类本质上还是类,强调的是一组事物共同的属性、定义的方法,部分方法其实实现出来是可以给子类进行共享的。而接口只强调方法声明上的相似性(一个接口包含的多个功能点只属于一类对象的),具体怎么实现需要实现类一个不落的去实现。

什么是抽象?

抽象的定义

抽象,抽象,白话的解释就是抽取比较相似的东西出来,结合之前说过的类和对象的两个定义:

  1. 类:类是一组相似事务的统称。
  2. 对象:对象是一个具体的类,是一个真实存在的类。

那么就可以再次详细的阐明抽象的详细定义:抽取多个对象或者类中比较像的部分。

抽象的层次可以很多种,比如把属性类似的东西抽取为父类,或者将行为类似的抽取为父类。具体应用设计的时候,可以从不同的观察角度来。

抽象有什么作用

抽象的作用是划分类似,其他学科里面也有类似的说法。

OOP的三大核心特征

相辅相成的三核心特征

  1. 封装
  2. 继承
  3. 多态

封装

封装从词汇上来看,其实就是不想让人看见。歹徒给你戴个黑色头套,其实也就是在封装自己,不想让自己的认识的人识破自己。在OOP里面,封装属于类的一个功能,类选择封装是为了自己的隐私,不想让自己的数据结构、属性方法暴露给别人来肆意修改,避免因为滥用导致出现问题的影响范围扩大。

这种避免糟糕影响范围扩大的目的被称为“隔离复杂度”。A类想要获取B类的信息,不需要知道A类的具体内部实现、结构(这叫复杂度),只需要调用A类提供的方法(这叫隔离)。

在OOP里面,封装的施加对象可以是属性和方法。比如我将一个类的某属性设置为private,这其实也就是封装,我封装了我这个类的属性为私有访问,想要访问我的属性必须通过我开放的getXXX方法( 在这个方法里面我可以校验你的权限,可以对你说“No means No” XD )。

其实,再被别人问到封装的好处的时候,应该从如下两个方面开始作答:

  1. 将类封闭,控制访问
  2. 隔离复杂度

继承

继承是OOP最基本的特征,有了继承这个特征,随之而来的就是子类、父类的定义。子类遗传了父类的部分方法和属性。而什么属性和方法可以遗传给子类,也取决于父类属性和方法的开放程度是在protected之上还是之下。

抽象和继承两者是什么关系?

再次回顾一下抽象的定义:抽取多个对象或者类中比较像的部分,是一个发生在设计阶段的动作。而继承是实现阶段的动作,根据抽象的思维产物得到类,在使用继承来表达这个思维产物。

可以说继承是OOP对抽象的表达。

多态

多态的英文是polymorphoismmorphoism是形态学的意思,poly前缀表示的是多的意思。在OOP里面,多态的定义是:使用父类的应用,能够调用子类的对象。

这种特性起到一个非常重要的作用,就是能使用多态来屏蔽子类对象的差异,通用性的代码很容易产出。如果新的子类添加和产生,对于原有的调用者的代码不需要任何变动。

总结

本章文章从几个方面阐述和介绍了OOP,首先,OOP是一种程序设计思想,起源是为了解决第二次软件危机——人们对“扩展性”的追求。和造成第二次软件危机的面向过程设计思想来说,OOP侧重于对目标事物的定义,大于完成目标的过程的定义。这种思维的好处是拥抱变化,能够很快的响应需求,暂时了第二次危机继续解决的扩展性要求。

OOP的理论支撑需要四类事物,类、对象、接口和抽象类。

  1. 类是相似事物的统称
  2. 对象是一个具体的个体的事物,在程序运行的时候承载信息。
  3. 接口是一系列功能点的定义,当我们不想知道某类对象在运行时候是谁,只想知道某类对象在运行时候能提供什么功能点的时候,就使用接口。
  4. 抽象类本质上还是类,包含一些需要子类定义的方法,也共享子类共有的一些方法。

OOP的三大核心特征也必须要牢记:

  1. 封装是为了控制对对象信息的访问,隔离了复杂性。
  2. 继承是抽象的表达方式,是OOP最基本的特征
  3. 多态的含义就是父类应用可以调用子类对象的实现,利用这个特性可以写出扩展性强、可修改性好的代码。
posted @ 2022-03-15 22:08  來福l4ifu  阅读(59)  评论(0编辑  收藏  举报