零、简单说说
做了七年开发,使用面向对象多年,但我发现身边的人并不一定知道什么是抽象,有屎山的原因也好,也有面向结构编程思想的也好,我们总会把一堆逻辑堆在长篇代码中,每次修改一处即使经历过不同层次的测试,上线以后还是会出现问题。
但是其实我们使用的工具语言,是有能力解决这个问题的,主要原因是我们很多时候因为时间紧急或其他墨守成规的问题,无法发挥出他的巨大作用。
现在的程序越来越复杂,各路大佬也开始推广领域驱动开发模式(DDD)
,不过这只是一个想法,各路大神所出的规范也各有千秋,当然适合自家工作生活的才是最好的。
说简单点,DDD其实就是使用我们工具的抽象能力
,去解决业务上的问题,不是死板的MVC三层架构模式然后把所有业务逻辑堆积在Service
中。不过在了解这个之前,有必要先了解面向对象设计和分析。
计划【MyOO】后续将继续整理面向对象相关的文章。
一、开发中设计方法的历史
面向数学(第一代语言1954---1958) → 算法抽象(第二代语言1959---1961) → 描述相关数据的意义(第三代语言1962-1980) → 面向对象(1980至今)
在第一代语言
中,主要以数学语言为主,解决科学家们遇到的相关需要计算的问题。其实这一块也是Lamda的起始,属于一组数字,输出结果的形式。
在第二代语言
中,数据可以组织在一起,形成块或数据结构,程序员们可以按照一定的契约来传输数据。
第三代语言
中,程序员对编程语言的要求越来越高,这个时期出现了大量的语言,但是幸存下来的没有多少,不过思想都被留下来,传递给后续出现的编程语言中。
第三代后期
的结构,大多使用的是面向过程编程的模式,并且应用不断的增长,需要大型开发团队,于是模块化的需求变得十分强烈。
结构化编程架构
第四代语言,即我们现在使用的面向对象类型的语言,他着重将一大块问题分解成多个小问题,进行处理,然后组合在一起,发挥最大的作用。也即使接下来想要继续讨论的问题。
中型系统 | 大型系统 |
---|---|
![]() |
![]() |
二、一切皆对象
我们在学习 Java 开发的时候,总是会很熟悉这句话。
📌 万物皆对象
其实对象是哲学中的一个名词
,代表着我们观察到的事物的现象。
开发借鉴了想法和名词,其完整的释义与哲学上的释义会有偏差,方向是一致的。利用这两者的关系,我们可以重新梳理编程中观察到的对象是什么。
💡 对象是在
空间和时间
上存在的,他包含了我们需要解决需求中相关问题的数据和动作
,我们让他执行每一步之后,数据发生的变更,我们可以通过查询数据来获取对象在某一个时间的信息快照。
接下来,让我们带着这个想法继续深入的讨论面向对象中的相关概念
三、面向对象分析
在讨论面向对象设计的表示法和过程之前,我们必须研究面向对象开发所基于的原则,也就是
抽象
、封装
、模块化
、层次结构
、类型
、并发
和持久
。
拥有丰富 Java 语言开发经验的同学,会认为类跟对象的关系就是一比一的,这是因为该语言在工作中,模糊了这两者的概念。但是当前我们在认识对象时需要将它与类这个概念拆分开来。
对象是存在于时间和空间中的具体实体,而类仅代表一种抽象,即一个对象的"本质"
。相当于说,我们日常编程无法控制运行中的对象(也许使用某些工具是可以的),但是我们可以控制对象的本质,即我们日常使用的类。
面向对象分析,则要求我们识别需求中的问题域,列出相关需求中使用的对象(如订单对象、产品对象),确立在系统运行时需要存在的角色,然后将这些元素抽象形成一个一个的对象,在系统中协作运行。
3.1 对象模型
缘于我们观察世界的方式,科学家们定义了对象模型。
首先,初始的对象模型是按模块分解的系统的组件,或者是知识表达的模块化单元,不同的单元之间协作共同完成问题的计算。
然后随着计算机技术的高速发展,诸如不同的方法学的提出,架构的演进,甚至是数据库的技术,定义了世上万物之间的关系重要元素:
- 数据
- 执行的动作
- 对象之间的关系
- 同类项抽象
三个面向对象的概念
💡
面向对象编程(OOP)
是一种实现的方法,在这种方法中,程序被组织成许多组相互协作的对象,每个对象代表某个类的一个实例,而类则属于一个通过继承关系形成的层次结构。
💡面向对象设计(OOD)
是一种设计方法,包括面向对象分解的过程和一种表示法,这种表示法用于展现被设计系统的逻辑模型和物理模型、静态模型和动态模型。
💡面向对象分析(OOA)
是一种分析方法,这种方法利用从问题域的词汇表中找到的类和对象来分析需求。
三者的关系,基本上OOA
的结果可以作为OOD
的模型,而OOD的结果则可以使用OOP
来实现相关的功能。
为什么需要抽象(这里指的是不停的抽出同类项,且有不同实现的子类)
因为对于世界万物的分类就是不停的抽象不同的概念来协作运行的,比如首先你是人,然后你是个开发者,人与开发者这两个概念涵盖的范围有重叠,也有差异,所以开发者
这个对象继承了人
这个对象,形成了"是一个(is a)"
的关系。那么人的所有能力,开发者都会拥有,但并不是所有人都有开发者的能力。
面向对象的主要要素:
-
抽象
抽象指的是从一个视角出发,提供了一个对象的基本描述,这个描述与其他的类型分离开来,可以单独处理某个问题域的能力。
举个例子,一个订单信息描述的对象,在用户下单时和仓库发货时的两个视角来看,他提供的功能是不同的。
抽象的程度不多不少,刚好满足系统的功能即可。
抽象分类
实体抽象
:问题域或解决方案中一个有用的模型;动作抽象
:封装了同一个问题域的不同处理方式,如订单的优惠计价模型,有不同的优惠策略;虚拟机抽象
:提供高层的通用操作,屏蔽底层的适配,如JVM虚拟机;偶然抽象
:毫无根据的,只是需要放在一起的实体的抽象
-
封装
将
承诺
的调用和结果暴露给客户类,内部实现对客户类隐藏。信息隐藏是将那些不涉及对象本质特征的秘密都隐藏起来的过程。通常,对象的结构
是隐藏的,其方法的实现
也是隐藏的。高层次的抽象不应该知晓低层次的实现。💡 日常编码中,那该死的开发工具总是贴心的帮我们把所有创建的 Class 设定为 Public 权限,我们日常开发时应该思考,这个类真的有必要 Public 吗?
举个例子:电饭煲,我们只需要知道他能够把饭煮熟,而不需要知晓他的电路板和工作过程。
-
模块化
模块化是抽象对象的更高一层封装,
指定需要暴露的对象
,隐藏部分内部使用的对象,是物理容器。一般会根据功能将一个大的系统分割成不同的小模块,通过多个模块协作完成大系统的功能。
其目标是允许不同模块独立的设计和修改,从而减少软件的开发成本。
如一个商城系统,可能会分割为产品模块、交易模块、支付模块等。
-
层次结构
层次结构一般分为
继承
和聚合
。继承提供一种 "is a" 的对象关系,比如 Spring 中的抽象:

从图中就可以看到,一个 ApplicationContext
是一个什么东西,他具备了哪一些能力。
而另外一个层次关系是聚合
,聚合允许实体控制子对象的生命周期,需要识别生命周期
是否和主体一致,从而做好充分的准备和销毁动作。
如一个学校,学生与学校的关系生命周期不一致,可以独立学校存在,而学校与学期的关系就是强关系,当学校没了以后就没有学期的说法。
次要要素:
-
类型
类型和类在运行时并不总是
等同
的,类型是针对一个对象的类的强约束,不同类型的值不可以强制替换使用,他保证我们编码过程中的约束。强类型的语言有一个特征,即修改抽象类时,我们需要重新编译所有的子类。针对这个问题我们可以采用
IOC容器
的思路,让子类在运行时再被注入,即利用多态
的特性。静态类型和动态类型
静态类型
(也称为静态绑定或早期绑定)意味着所有变量和表达式的类型在编译时就固定了,动态类型
(也称为延迟绑定)意味着所有变量和表达式的类型直到运行时刻才知道。 -
并发
虽然面向对象编程关注数据抽象,但是
封装
、继承
和并发
关注了过程抽象和同步。每个对象(来自于真实世界的一个抽象)都可以代表一个独立的控制线程(一种过程抽象)。这样的对象被称为"主动的"
。在基于面向对象设计的系统中,我们可以将世界概念化为一组协作的对象,其中某些是主动的,因此作为独立活动的中心。即并发协助我们识别系统中的
主动对象
和被动对象
。我们在开发存在并发的系统时,通常需要考虑一个操作是否可能出现并发的问题。必须考虑
死锁
、活锁
、饥饿
、互斥
和竞争条件
等问题。 -
持久
持久需要解决的是,当数据的生命周期
大于
程序运行周期时的问题。即我们的业务数据通常存在的时间会大于一段程序的运行周期,程序需要迭代和发布,就会带来停机的问题。
在对象模型中引入持久的概念导致了面向对象的数据库。
💡 持久是对象的一种属性,利用这种属性,对象跨越时间(例如,当对象的创建者不存在了的时候,对象仍然存在)和空间(例如,对象的位置从它被创建的地址空间移开)而存在。
3.2 对象的本质
对象具有瞬时的状态、可以执行的行为和唯一标志符
(比如内存地址或数据库唯一索引)
-
对象的状态
每个对象都会存储两种类型的属性:
动态
和静态
属性。动态属性指的是运行时会随着操作发生变化的值,静态属性则表示事物无法被修改的属性。
如一个仓库,静态属性为长和宽,他通常表示该仓库可以存储的容量,在运行时也无法被改变;动态属性是货架位置、货架上的物品数量等,他是可以在日常的多个行为操作以后被改变的值。
-
行为
行为是对象在
状态改变
和消息传递
方面的动作和反应
的方式,代表外部可以执行的动作。一般一个对象会提供这些行为:修改操作、查询操作、遍历操作(访问对象所有部分的操作)、构造操作、析构操作(释放对象)
行为和数据共同构成了对象承诺可以对外提供的服务,实现抽象的职责。
-
标志符
大多数程序设计语言和数据库语言使用变量名称来区分临时对象,混淆了定址能力和标识符。大多数数据库系统使用标识符主键来区分持久对象,混淆了数据值和标识符。
3.3 对象与对象之间的关系
两个对象之间的链接代表了具体的关联,通过这种关联,一个对象(客户)请求另一个对象(服务提供者)的服务,或者通过这种关联从一个对象导航到另一个对象。
链接的参与者,一个对象可能扮演以下三种角色之一
控制器
,该对象只调用其他的对象服务器
,该对象只被其他的对象调用,不去调用其他对象代理
,既可以被调用,也可以调用其他对象聚合
,如果一个对象是另一个对象的一部分,就意味着它到它的聚合体有一个链接。通过这个链接,聚合体可以向它的部分发送消息。
聚合可以保证一个模块设计的封装性
,而链接则可以松耦合
,两种情况应该根据实际不同需求进行设计。
四、类的本质
类是一组对象,所有该类实例出来的对象拥有一样的数据结构和行为动作
。
编程在很大程度上是一种
"制定契约"
:一个较大问题的不同功能通过子契约被分配给不同的设计元素,从而被分解成较小的问题。在编译器中可以快速发现某些违反契约的用法。
在 Java 语言中,通常会提供两种形式:抽象类
和接口类
前者可以理解为抽象多个子类的共同行为
,或提供一些约定的调用模式(如模板设计模式)
接口类则是类对外的视图
,表明某一类型的实现类可以对外提供的所有动作。
而具体实现类可以实现接口中约定的所有行为,并且封装内部需要使用的数据,甚至我们可以把实现类设置为可见性最低,交由 IOC 容器将实现类注入到依赖接口类的客户类中。
4.1 类之间的关系
关联关系:两个类相互产生联系,若无特殊说明方向,一般表示双向关联。关联可能有三种关系:一对一
、一对多
和多对多
继承关系:如果我们需要处理某一类对象的问题域时,我们即可以使用一个抽象类
来表示这一类型的对象。如:我们可以使用一个可计费的抽象来计算系统中所有订单金额、统计收入。
继承能够实现的另一种能力是多态
,当我们标记我们需要某个类型的对象时,可以在运行时才决定传入的具体实现类。
聚合关系:提供了类实例中的整体/部分关系。类之间的聚合关系与这些类的对象之间的聚合关系是并存的。
在选择聚合还是继承关系时,我们常常通过一个类是否"is a"另外一个类来判断,如果答案是否定的,那我们应该使用聚合。
依赖关系:表明一个类正常运行需要依赖另外一个类的正确性,当被依赖类发生变化时,需要重新编译客户类。一般出现在一段行为函数中引用了另外一个类的情况。
4.2 高品质的类和对象
在分析和设计的早期,开发者主要有两项重要的任务
- 从问题域的词汇表中确定出类;
- 创建一些结构,让多组对象一起工作,提供满足问题需求的行为。
根据经验,一个类无法在第一次设计时就趋近于完美,而是需要经过不停的迭代、增量来完善一个类的设计。
评判一个类的五个测量指标
-
耦合
一个模块与另一个模块之间建立起的
关联强度
的测量。如果多个类之间产生了强的关联性,那修改将会变得繁琐,需要充分测试相关关联的类才能保证修改成功。
耦合和继承存在着矛盾的关系,继承产生强耦合,但能够抽象类的共性,提升复用性,所以需要类的耦合程度通常需要人工保证合理的值。
-
内聚
内聚要求一个类或模块中的功能是具有共同性的,而不是随意的将两个不相关的语义聚合在一起,他提供了一个清晰的边界。
-
充分性:类或模块应该记录某个抽象足够多的特征,从而允许有意义的、有效的交互。
-
完整性:类或模块的接口记录了某个抽象全部有意义的特征。但也不需要提供多余的动作,有些动作可以让客户使用多个基础操作组合起来实现。
-
基础性:指定最底层接口(继承关系中最上面的那个接口)可以完成的操作,即为基础性操作,如
ArrayList
增加一个元素,其最底层的List.add
就是一个最基础性的操作。
基于以上几个质量,我们可以从以下几个方面入手,优化类的质量
-
类的动作函数
-
功能语义上的抽象,我们针对一个模型,对齐进行类抽象的接口时,应当考虑每个动作执行的大小 ,抽象出来的函数如果实现动作太大会导致后续管理困难,而如果实现的动作太小,则可能导致调用者的碎片化(如批量往集合插入需要调用多次
add
函数)我们可以通过这几个方面去衡量:
可复用性
:这个行为可以在多种上下文中使用吗?复杂性
:实现这个行为的难度有多大?适用性
:这个行为与打算放入的类型之间相关程度如何?实现知识
:这个行为的实现依赖于一个类型的内部细节吗?
-
时间语义和空间语义,需要注意并发可能产生的数据安全问题
-
-
类的关系
- 耦合是度量可访问程度的指标。
💡 Demeter法则 类的方法不应该以任何方式依赖于任何类的结构,除了它自己类的当前(顶层)结构之外。而且,每个方法只能够对一个非常有限的类集的对象发出消息.
在查看整个系统的类结构时,可能会发现它的继承关系
宽而浅
,或者窄而深
,或者比较平衡。类结构宽而浅通常代表由独立的类构成的森林,它们之间可以混合或匹配。类结构窄而深则表明各个类构成的树都与一个共同的祖先有关。- 可见性:决定对象之间的关系主要是设计这些对象进行交互的机制。应该根据具体的需求来设计不同类之间的可见性。
-
类的实现
类或对象的表示形式几乎总是该抽象封装起来的秘密。我们只要能够正确根据抽象类上的接口返回正确的数据即可。
但通常我们需要权衡时间和空间上的消耗,比如
ArrayList
,如果是注重空间的话,那么size()
每次需要遍历所有元素再计算正确的长度,而如果注重时间的话,那么可以使用一个变量存储当前size
值。
4.3 正确的分类
分类帮助我们确定类之间的泛化
、特化
、聚合
等层次结构。
有一说一,想要一个符合所有人的分类是很困难的,100个人就有100个看法。
经验表明,确定类和对象的过程既涉及发现,也涉及发明。通过发现,我们逐渐从问题域的词汇表中识别出关键抽象和机制。
通常需要经过多个周期才能迭代出合适的分类出来,讲究的是分的刚好,不多也不少。
常见的分类方式
-
经典分类
所有具有某一个或某一组共同属性的实体构成了一个
分类
。这样的属性对于定义这个分类是必要的,也是充分的这个分类需要
有确定的范围
进行划分,比如性别可以划分男女,但是如果说高的人划分一组,则需要定义一个明确的值,代表有多高才算高的范围。经典分类法利用相关的属性作为对象间相似性的判据。最有用的属性集合是其成员没有太多相互影响的集合。这解释了为什么下面的属性组合得到了广泛采用:尺寸、颜色、形状和物质。
在特定情况下应该考虑哪些属性,这与领域是高度相关的。
-
概念聚集
在这种方法中,类(一些实体的聚集)的产生首先是形成类的概念描述,然后再根据这些描述对实体进行分类。
概念聚集与模糊(多值)集理论是有密切关系的,在这种理论中,对象可以按不同的适合程度属于一个或多个分组。概念聚集通过关注
"最适合"
来进行绝对的分类判断。这个分类只要我们认为合理,即可划分一类,不过这个方式是偏主观的,不同的人会有不同的分类方法。当然也与相关的领域知识有很大的联系。
-
原型理论
对象的类是由一个原型对象来代表的,如果一个对象与这个原型表现出重要的相似性,那么这个对象就被认为是这个类中的一员。
在概念聚集中,我们根据明确的概念对事物进行
分组
。在原型理论中,我们根据事物与具体原型的关系对它们进行分组。
在实际分类中,我们可以按照这个顺序进行分类:
- 首先是根据
特定领域相关的属性
来确定类和对象的。这时,我们关注的重点是确定构成问题空间词汇表的结构和行为
; - 使用
概念聚集
重新划分或优化上一步的抽象; - 再尝试使用
原型理论
进行分类。
4.4 面向对象分析方法
-
经典方法
针对问题域的对象进行抽象分析。
通常需要记录人、事物、交互动作、发生的事件、
-
行为分析
这些经典方法关注问题领域中实实在在的事物,但面向对象分析的另一种思路是关注动态的行为,将这些行为作为类和对象的主要来源。
-
领域分析
通常需要经历几个步骤:
- 咨询领域专家,构建一个通用的模型草稿;
- 检查领域中原有的系统,以一种通用的格式展示出这方面的理解;
- 咨询领域专家,确定系统间的相似和差异;
- 细化通用模型,以包含原有的系统
-
用例分析
从执行用例中获取系统需要提供的功能,从而针对这些功能点进行系统的类抽象。
4.5 关键的抽象与机制
- 确定关键抽象
确定关键抽象包含两个过程:发现
和发明
。通过发现过程,我们意识到领域专家所使用的抽象。如果领域专家提及它,那么这个抽象通常很重要。通过发明过程,我们创造了新的类和对象,它们不一定是问题域的组成部分,但在设计或实现中也是很重要的。
- 命名关键抽象
尽量使用领域专家提及的词汇
对类或函数进行命名。
- 识别机制
关键抽象反映了问题域的抽象,而机制是设计的灵魂。在设计过程中,开发者不仅必须考虑单个类的设计,还要考虑这些类的实例如何一起工作。
五、UML 图

UML所有类型图
一般项目开始时我们会出一份部署图,这份图日常业务开发需要修改比较少。
然后进入业务开发时,日常业务迭代使用的图就比较多,会有:用例图、活动图(描述运行判定流程)、状态图(描述状态变更过程),以及开发常用的类图,用于描述类的集成、聚合、依赖等关系。
六、小总结
- 在这个类目中,我们需要把对象和类这两个概念拆开来进行对应的独立分析,对象是程序运行时存在的一块空间,并且可以通过调用另外一个对象的指令;
- 高质量类的设计,应该控制好对应的权限,并且类有继承、聚合两个依赖关系;
- 对日常业务分类的常用方法。
参考文献:《面向对象设计与分析》