【MyOO】面向对象设计与分析

零、简单说说

做了七年开发,使用面向对象多年,但我发现身边的人并不一定知道什么是抽象,有屎山的原因也好,也有面向结构编程思想的也好,我们总会把一堆逻辑堆在长篇代码中,每次修改一处即使经历过不同层次的测试,上线以后还是会出现问题。

但是其实我们使用的工具语言,是有能力解决这个问题的,主要原因是我们很多时候因为时间紧急或其他墨守成规的问题,无法发挥出他的巨大作用。

现在的程序越来越复杂,各路大佬也开始推广领域驱动开发模式(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)"的关系。那么人的所有能力,开发者都会拥有,但并不是所有人都有开发者的能力。

面向对象的主要要素:

  • 抽象

    抽象指的是从一个视角出发,提供了一个对象的基本描述,这个描述与其他的类型分离开来,可以单独处理某个问题域的能力。

    举个例子,一个订单信息描述的对象,在用户下单时和仓库发货时的两个视角来看,他提供的功能是不同的。

    抽象的程度不多不少,刚好满足系统的功能即可。

    抽象分类

    1. 实体抽象:问题域或解决方案中一个有用的模型;
    2. 动作抽象:封装了同一个问题域的不同处理方式,如订单的优惠计价模型,有不同的优惠策略;
    3. 虚拟机抽象:提供高层的通用操作,屏蔽底层的适配,如JVM虚拟机;
    4. 偶然抽象:毫无根据的,只是需要放在一起的实体的抽象
  • 封装

    承诺的调用和结果暴露给客户类,内部实现对客户类隐藏。信息隐藏是将那些不涉及对象本质特征的秘密都隐藏起来的过程。通常,对象的结构是隐藏的,其方法的实现也是隐藏的。高层次的抽象不应该知晓低层次的实现。

    💡 日常编码中,那该死的开发工具总是贴心的帮我们把所有创建的 Class 设定为 Public 权限,我们日常开发时应该思考,这个类真的有必要 Public 吗?

    举个例子:电饭煲,我们只需要知道他能够把饭煮熟,而不需要知晓他的电路板和工作过程。

  • 模块化

    模块化是抽象对象的更高一层封装,指定需要暴露的对象,隐藏部分内部使用的对象,是物理容器。

    一般会根据功能将一个大的系统分割成不同的小模块,通过多个模块协作完成大系统的功能。

    其目标是允许不同模块独立的设计和修改,从而减少软件的开发成本。

    如一个商城系统,可能会分割为产品模块、交易模块、支付模块等。

  • 层次结构

    层次结构一般分为继承聚合

    继承提供一种 "is a" 的对象关系,比如 Spring 中的抽象:

从图中就可以看到,一个 ApplicationContext 是一个什么东西,他具备了哪一些能力。

而另外一个层次关系是聚合,聚合允许实体控制子对象的生命周期,需要识别生命周期是否和主体一致,从而做好充分的准备和销毁动作。

如一个学校,学生与学校的关系生命周期不一致,可以独立学校存在,而学校与学期的关系就是强关系,当学校没了以后就没有学期的说法。

次要要素:

  • 类型

    类型和类在运行时并不总是等同的,类型是针对一个对象的类的强约束,不同类型的值不可以强制替换使用,他保证我们编码过程中的约束。

    强类型的语言有一个特征,即修改抽象类时,我们需要重新编译所有的子类。针对这个问题我们可以采用IOC容器的思路,让子类在运行时再被注入,即利用多态的特性。

    静态类型和动态类型

    静态类型(也称为静态绑定或早期绑定)意味着所有变量和表达式的类型在编译时就固定了,动态类型(也称为延迟绑定)意味着所有变量和表达式的类型直到运行时刻才知道。

  • 并发

    虽然面向对象编程关注数据抽象,但是封装继承并发关注了过程抽象和同步。每个对象(来自于真实世界的一个抽象)都可以代表一个独立的控制线程(一种过程抽象)。这样的对象被称为"主动的"。在基于面向对象设计的系统中,我们可以将世界概念化为一组协作的对象,其中某些是主动的,因此作为独立活动的中心。

    即并发协助我们识别系统中的主动对象被动对象

    我们在开发存在并发的系统时,通常需要考虑一个操作是否可能出现并发的问题。必须考虑死锁活锁饥饿互斥竞争条件等问题。

  • 持久

    持久需要解决的是,当数据的生命周期大于程序运行周期时的问题。

    即我们的业务数据通常存在的时间会大于一段程序的运行周期,程序需要迭代和发布,就会带来停机的问题。

    在对象模型中引入持久的概念导致了面向对象的数据库。

    💡 持久是对象的一种属性,利用这种属性,对象跨越时间(例如,当对象的创建者不存在了的时候,对象仍然存在)和空间(例如,对象的位置从它被创建的地址空间移开)而存在。

3.2 对象的本质

对象具有瞬时的状态、可以执行的行为和唯一标志符(比如内存地址或数据库唯一索引)

  • 对象的状态

    每个对象都会存储两种类型的属性:动态静态属性。

    动态属性指的是运行时会随着操作发生变化的值,静态属性则表示事物无法被修改的属性。

    如一个仓库,静态属性为长和宽,他通常表示该仓库可以存储的容量,在运行时也无法被改变;动态属性是货架位置、货架上的物品数量等,他是可以在日常的多个行为操作以后被改变的值。

  • 行为

    行为是对象在状态改变消息传递方面的动作和反应的方式,代表外部可以执行的动作。

    一般一个对象会提供这些行为:修改操作、查询操作、遍历操作(访问对象所有部分的操作)、构造操作、析构操作(释放对象)

    行为和数据共同构成了对象承诺可以对外提供的服务,实现抽象的职责。

  • 标志符

    大多数程序设计语言和数据库语言使用变量名称来区分临时对象,混淆了定址能力和标识符。大多数数据库系统使用标识符主键来区分持久对象,混淆了数据值和标识符。

3.3 对象与对象之间的关系

两个对象之间的链接代表了具体的关联,通过这种关联,一个对象(客户)请求另一个对象(服务提供者)的服务,或者通过这种关联从一个对象导航到另一个对象。

链接的参与者,一个对象可能扮演以下三种角色之一

  • 控制器,该对象只调用其他的对象
  • 服务器,该对象只被其他的对象调用,不去调用其他对象
  • 代理,既可以被调用,也可以调用其他对象
  • 聚合,如果一个对象是另一个对象的一部分,就意味着它到它的聚合体有一个链接。通过这个链接,聚合体可以向它的部分发送消息。

聚合可以保证一个模块设计的封装性,而链接则可以松耦合,两种情况应该根据实际不同需求进行设计。

四、类的本质

类是一组对象,所有该类实例出来的对象拥有一样的数据结构和行为动作

编程在很大程度上是一种"制定契约":一个较大问题的不同功能通过子契约被分配给不同的设计元素,从而被分解成较小的问题。在编译器中可以快速发现某些违反契约的用法。

在 Java 语言中,通常会提供两种形式:抽象类接口类

前者可以理解为抽象多个子类的共同行为,或提供一些约定的调用模式(如模板设计模式)

接口类则是类对外的视图,表明某一类型的实现类可以对外提供的所有动作。

而具体实现类可以实现接口中约定的所有行为,并且封装内部需要使用的数据,甚至我们可以把实现类设置为可见性最低,交由 IOC 容器将实现类注入到依赖接口类的客户类中。

4.1 类之间的关系

关联关系:两个类相互产生联系,若无特殊说明方向,一般表示双向关联。关联可能有三种关系:一对一一对多多对多

继承关系:如果我们需要处理某一类对象的问题域时,我们即可以使用一个抽象类来表示这一类型的对象。如:我们可以使用一个可计费的抽象来计算系统中所有订单金额、统计收入。

继承能够实现的另一种能力是多态,当我们标记我们需要某个类型的对象时,可以在运行时才决定传入的具体实现类。

聚合关系:提供了类实例中的整体/部分关系。类之间的聚合关系与这些类的对象之间的聚合关系是并存的。

在选择聚合还是继承关系时,我们常常通过一个类是否"is a"另外一个类来判断,如果答案是否定的,那我们应该使用聚合。

依赖关系:表明一个类正常运行需要依赖另外一个类的正确性,当被依赖类发生变化时,需要重新编译客户类。一般出现在一段行为函数中引用了另外一个类的情况。

4.2 高品质的类和对象

在分析和设计的早期,开发者主要有两项重要的任务

  • 从问题域的词汇表中确定出类;
  • 创建一些结构,让多组对象一起工作,提供满足问题需求的行为。

根据经验,一个类无法在第一次设计时就趋近于完美,而是需要经过不停的迭代、增量来完善一个类的设计。

评判一个类的五个测量指标

  • 耦合

    一个模块与另一个模块之间建立起的关联强度的测量。

    如果多个类之间产生了强的关联性,那修改将会变得繁琐,需要充分测试相关关联的类才能保证修改成功。

    耦合和继承存在着矛盾的关系,继承产生强耦合,但能够抽象类的共性,提升复用性,所以需要类的耦合程度通常需要人工保证合理的值。

  • 内聚

    内聚要求一个类或模块中的功能是具有共同性的,而不是随意的将两个不相关的语义聚合在一起,他提供了一个清晰的边界。

  • 充分性:类或模块应该记录某个抽象足够多的特征,从而允许有意义的、有效的交互。

  • 完整性:类或模块的接口记录了某个抽象全部有意义的特征。但也不需要提供多余的动作,有些动作可以让客户使用多个基础操作组合起来实现。

  • 基础性:指定最底层接口(继承关系中最上面的那个接口)可以完成的操作,即为基础性操作,如 ArrayList 增加一个元素,其最底层的 List.add 就是一个最基础性的操作。

基于以上几个质量,我们可以从以下几个方面入手,优化类的质量

  • 类的动作函数

    1. 功能语义上的抽象,我们针对一个模型,对齐进行类抽象的接口时,应当考虑每个动作执行的大小 ,抽象出来的函数如果实现动作太大会导致后续管理困难,而如果实现的动作太小,则可能导致调用者的碎片化(如批量往集合插入需要调用多次 add 函数)

      我们可以通过这几个方面去衡量:

      • 可复用性:这个行为可以在多种上下文中使用吗?
      • 复杂性:实现这个行为的难度有多大?
      • 适用性:这个行为与打算放入的类型之间相关程度如何?
      • 实现知识:这个行为的实现依赖于一个类型的内部细节吗?
    2. 时间语义和空间语义,需要注意并发可能产生的数据安全问题

  • 类的关系

    • 耦合是度量可访问程度的指标。

    💡 Demeter法则 类的方法不应该以任何方式依赖于任何类的结构,除了它自己类的当前(顶层)结构之外。而且,每个方法只能够对一个非常有限的类集的对象发出消息.

    在查看整个系统的类结构时,可能会发现它的继承关系宽而浅,或者窄而深,或者比较平衡。类结构宽而浅通常代表由独立的类构成的森林,它们之间可以混合或匹配。类结构窄而深则表明各个类构成的树都与一个共同的祖先有关。

    • 可见性:决定对象之间的关系主要是设计这些对象进行交互的机制。应该根据具体的需求来设计不同类之间的可见性。
  • 类的实现

    类或对象的表示形式几乎总是该抽象封装起来的秘密。我们只要能够正确根据抽象类上的接口返回正确的数据即可。

    但通常我们需要权衡时间和空间上的消耗,比如 ArrayList,如果是注重空间的话,那么 size() 每次需要遍历所有元素再计算正确的长度,而如果注重时间的话,那么可以使用一个变量存储当前 size 值。

4.3 正确的分类

分类帮助我们确定类之间的泛化特化聚合等层次结构。

有一说一,想要一个符合所有人的分类是很困难的,100个人就有100个看法。

经验表明,确定类和对象的过程既涉及发现,也涉及发明。通过发现,我们逐渐从问题域的词汇表中识别出关键抽象和机制。

通常需要经过多个周期才能迭代出合适的分类出来,讲究的是分的刚好,不多也不少。

常见的分类方式

  • 经典分类

    所有具有某一个或某一组共同属性的实体构成了一个分类。这样的属性对于定义这个分类是必要的,也是充分的

    这个分类需要有确定的范围进行划分,比如性别可以划分男女,但是如果说高的人划分一组,则需要定义一个明确的值,代表有多高才算高的范围。

    经典分类法利用相关的属性作为对象间相似性的判据。最有用的属性集合是其成员没有太多相互影响的集合。这解释了为什么下面的属性组合得到了广泛采用:尺寸、颜色、形状和物质。

    在特定情况下应该考虑哪些属性,这与领域是高度相关的。

  • 概念聚集

    在这种方法中,类(一些实体的聚集)的产生首先是形成类的概念描述,然后再根据这些描述对实体进行分类。

    概念聚集与模糊(多值)集理论是有密切关系的,在这种理论中,对象可以按不同的适合程度属于一个或多个分组。概念聚集通过关注"最适合"来进行绝对的分类判断。

    这个分类只要我们认为合理,即可划分一类,不过这个方式是偏主观的,不同的人会有不同的分类方法。当然也与相关的领域知识有很大的联系。

  • 原型理论

    对象的类是由一个原型对象来代表的,如果一个对象与这个原型表现出重要的相似性,那么这个对象就被认为是这个类中的一员。

    在概念聚集中,我们根据明确的概念对事物进行分组。在原型理论中,我们根据事物与具体原型的关系对它们进行分组。

在实际分类中,我们可以按照这个顺序进行分类:

  1. 首先是根据特定领域相关的属性来确定类和对象的。这时,我们关注的重点是确定构成问题空间词汇表的结构和行为
  2. 使用概念聚集重新划分或优化上一步的抽象;
  3. 再尝试使用原型理论进行分类。

4.4 面向对象分析方法

  1. 经典方法

    针对问题域的对象进行抽象分析。

    通常需要记录人、事物、交互动作、发生的事件、

  2. 行为分析

    这些经典方法关注问题领域中实实在在的事物,但面向对象分析的另一种思路是关注动态的行为,将这些行为作为类和对象的主要来源。

  3. 领域分析

    通常需要经历几个步骤:

    • 咨询领域专家,构建一个通用的模型草稿;
    • 检查领域中原有的系统,以一种通用的格式展示出这方面的理解;
    • 咨询领域专家,确定系统间的相似和差异;
    • 细化通用模型,以包含原有的系统
  4. 用例分析

    从执行用例中获取系统需要提供的功能,从而针对这些功能点进行系统的类抽象。

4.5 关键的抽象与机制

  • 确定关键抽象

确定关键抽象包含两个过程:发现发明。通过发现过程,我们意识到领域专家所使用的抽象。如果领域专家提及它,那么这个抽象通常很重要。通过发明过程,我们创造了新的类和对象,它们不一定是问题域的组成部分,但在设计或实现中也是很重要的。

  • 命名关键抽象

尽量使用领域专家提及的词汇对类或函数进行命名。

  • 识别机制

关键抽象反映了问题域的抽象,而机制是设计的灵魂。在设计过程中,开发者不仅必须考虑单个类的设计,还要考虑这些类的实例如何一起工作。

五、UML 图

UML所有类型图

一般项目开始时我们会出一份部署图,这份图日常业务开发需要修改比较少。

然后进入业务开发时,日常业务迭代使用的图就比较多,会有:用例图、活动图(描述运行判定流程)、状态图(描述状态变更过程),以及开发常用的类图,用于描述类的集成、聚合、依赖等关系。

六、小总结

  1. 在这个类目中,我们需要把对象和类这两个概念拆开来进行对应的独立分析,对象是程序运行时存在的一块空间,并且可以通过调用另外一个对象的指令;
  2. 高质量类的设计,应该控制好对应的权限,并且类有继承、聚合两个依赖关系;
  3. 对日常业务分类的常用方法。

参考文献:《面向对象设计与分析》

相关推荐
Gavynlee3 小时前
plantuml用法总结
设计模式
DKPT4 小时前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式
缘来是庄4 小时前
设计模式之迭代器模式
java·设计模式·迭代器模式
摘星编程6 小时前
深入解析迭代器模式:优雅地遍历聚合对象元素
设计模式·迭代器模式·软件开发·编程技巧·面向对象设计
DKPT11 小时前
Java桥接模式实现方式与测试方法
java·笔记·学习·设计模式·桥接模式
缘来是庄18 小时前
设计模式之中介者模式
java·设计模式·中介者模式
GodKeyNet1 天前
设计模式-责任链模式
java·设计模式·责任链模式
摘星编程1 天前
深入理解责任链模式:从HTTP中间件到异常处理的实战应用
http·设计模式·中间件·责任链模式·实战应用
鼠鼠我呀21 天前
【设计模式04】单例模式
单例模式·设计模式
缘来是庄2 天前
设计模式之访问者模式
java·设计模式·访问者模式