BlogWork(2)-FlightOrder

前言

这次的作业可以说是非常简单,至少个人是一遍过的,我觉得大部分人也都可以做到一遍过,这里的简单体现在算法上,毕竟基本没什么算法,用纯POP可能几行就写完了。
不过只有算法简单的题目才能更加专注去设计,这个场景在设计上并不简单,整体还算拥有一个比较庞大的背景,可以做很多的设计,同时因为要求较少,可以做很自由的设计。
这次的作业模拟了一个还算现实的业务场景,就是做一个类似于航空订单管理的后端,不过是简化版的,具体体现在:

  • 交互变成了StdIO,而不是更复杂的场景,比如图形化客户端,网络请求。
  • 处理输入的时候并没有产生(持久化)副作用,只是原样产生输出。

不过尽管如此,也算一个比较现实的场景了,关于设计的知识可以涉及到:

  • 面向对象编程(OOP)特性:封装、抽象(通过接口)、多态(接口的不同实现)
  • OOP设计原则:尤其是SOLID原则中的单一职责原则(SRP)
  • 常用设计模式:如数据传输对象(DTO/VO)、静态工厂方法、策略模式(雏形)
  • 核心设计理念:面向接口编程(Programming to Interfaces)

作业设计路径

之前提到了,这三次作业的整体算比较简单,至少通过是很简单的,但是设计并不会反映在当下的错误,而会反映在以后的复杂度上。

第一次作业

这次的作业大致有一点符合拥有逻辑,数据表示,数据显示的现实场景,所以考虑解耦这三个部分,在这三个部分的解耦上做的比较好的,且比较主流的是MVC架构,故尝试使用MVC架构实现。
虽然本次实现并未严格遵循MVC的全部规范和术语,甚至是一个比较简陋的实现,但其核心思想,也就是职责分离面向接口编程,贯穿始终。
由于使用了比较大体的设计框架,而这题的实现相对简单,导致了代码的复杂度相比纯POP稍微有些高。

分层与职责划分

这次作业将系统大致划分为以下几个逻辑层次/组件集:

  • 数据实体层 (Entities):
    • 代表类: ClientDataEntity, GoodsEntity, FlightEntity, OrderEntity
    • 职责: 这些类被设计为纯粹的数据容器(POJO),用于封装从输入源解析出来的原始业务数据。它们被设计为不可变对象(属性private final,仅提供getter),旨在增强了数据的稳定性和线程安全性(虽然这次作业中并发是不需要考虑的)。这体现了OOP的封装特性。
  • 业务对象/数据传输载体 (BO/DTO for service input):
    • 代表类: FlightDataBo (Business Object / Data Transfer Object for service input)
    • 职责: FlightDataBo聚合了处理一次完整订单所需的所有相关实体,作为数据从读取层传递到服务层的标准数据包。这简化了服务层方法的参数列表
  • 业务逻辑层 (Service Layer):
    • 代表类: FlightService (接口), FlightServiceImpl (实现)
    • 职责: 这是系统的核心,封装了所有的业务规则和处理流程。FlightServiceImpl的processFlightDataBo方法负责:
      • 计算每件货物的计费重量(考虑体积重)和运输费用(具体计算逻辑部分委托给GoodsVo)。
      • 汇总订单的总重量和总价格。
      • 执行业务校验,如判断订单总重量是否超出航班的最大载重。
      • 将处理后的数据转换为适合展示的视图对象(VO)。
    • 解耦:
      • 将业务逻辑和系统的其它部分解耦,将复杂的业务逻辑内聚于此。
  • 视图对象层 (View Objects - VOs):
    • 代表类: OrderVoInterface, OrderVo, GoodsVoInterface, GoodsVo
    • 职责: 这些对象专门为数据展示而设计。通过VO,将数据表示的细节与核心业务逻辑进一步分离。Vo不仅包含了从实体或业务处理结果中提取的数据,还包含了额外的、为展示服务的属性(如GoodsVo中的计费费率、单品价格)和格式化逻辑(如getFormatString()方法)。GoodsVo.fromGoodsEntity()和OrderVo.fromEntity()这样的静态工厂方法被用来封装VO的创建逻辑。
  • 数据聚合输出对象 (DTO for service output):
    • 代表类: FlightDataDto
    • 职责: FlightDataDto聚合了服务层处理完毕后,需要传递给输出层(Writer)的所有视图对象(OrderVo和GoodsVo数组)。
  • 数据输入/输出层 (简化的Data Access/Presentation Layer):
    • 代表类:
      • FlightDataReader (接口), FlightDataReaderScannerImpl (实现)
      • FlightDataWriter (接口), FlightDataWriterStdIOImpl (实现)
    • 职责:
      • FlightDataReaderScannerImpl: 负责从标准输入(System.in通过Scanner)读取原始数据,并将其解析、构造成FlightDataBo。
      • FlightDataWriterStdIOImpl: 负责接收服务层返回的FlightDataDto,并将其中的信息格式化后输出到标准输出(控制台)。
    • 解耦:
      • 这一层将系统与外部世界的具体I/O方式解耦。

问题与反思

可以看出 这次作业我在数据表示上的数据实体层中 使用的是目前很常见的缺血模型 简而言之,缺血模型是一种只包含数据,不表示逻辑的类。
可以看出,这里的Vo层的职责,我写了很多东西,虽然都是向积极的方向写的,但是也暴露了一些问题。

  • 过多的内容意味着Vo层很,承担了很多逻辑,而一般认为Vo不应该承担很多逻辑,应当是薄的,这次的写法稍微没那么规范。
  • 而且工厂方法的设计虽然使得Vo的构造逻辑很清晰,但是也使得VoEntity形成不必要的耦合。

因此,在之后的作业,我认为最大的问题体现在Vo上,但是由于这次作业的场景而言,我认为在Vo内包含一些逻辑似乎也无所厚非。

类图

由于上文的设计部分比较基于代码,所以给出相应的类图,写的可能比较仓促,见谅。

第二次作业

第二次作业添加了一些需求,整体仍然不太难,而且有第一次的基础,这次作业甚至比第一次还简单。
但是由于我在改的时候意识到了之前代码的些许问题,所以这次修改的时间主要围绕着微调程序,同时我在那个时候稍微了解了一下DDD(领域驱动设计),故作为一个第一次的初实践。
这次在第一次的基础上,精简了一些Vo层,封装了逻辑到逻辑层和数据实体层之间的Domain内,且试着不为所有字段提供get/is/set方法

分层与职责划分

基于第一次作业的分层模型,第二次作业在保持整体框架稳定的前提下,对各层的职责和实现进行了如下的调整和深化,核心变化体现在领域相关对象的引入和VO层的“瘦身”:

  • 领域对象层 (Domain Objects):
    • 代表类:
      • 客户领域: AbstractClientDomain (抽象基类), IndividualClientDomain, CorporationClientDomain (具体子类)
      • 货物领域: AbstractGoodsDomain (抽象基类), NormalGoodsDomain, ExpediteGoodsDomain, DangerousGoodsDomain (具体子类)
      • 订单支付类型领域: AbstractOrderType (抽象基类), WechatOrderType, AliPayOrderType, CashOrderType (具体子类)
    • 职责: 这是本次迭代的核心。这些新引入或得到强化的领域对象不再仅仅是第一次作业中Entity那样的数据容器。它们被赋予了与其业务概念紧密相关的行为和逻辑,换一句话,它们是充血的:
      • 封装业务规则: 不同类型的客户(个人、企业)拥有不同的计费折扣率(getBillingRates() 方法被实现在各自的子类中)。不同类型的货物(普通、加急、危险品)有各自独立的计费费率标准和最终价格计算逻辑(getBillingRates() 和 getPrices() 方法)。
      • 体现多态性: 通过抽象基类和子类的继承体系,利用多态性简化了上层(如服务层)的处理逻辑。服务层在计算总费用时,可以直接调用 clientDomain.getBillingRates() 或 goodsDomain.getPrices(),而无需关心具体是哪种类型的客户或货物,具体的行为由对象自身决定。
      • 增强内聚性: 将与特定业务实体相关的计算逻辑(如货物真实重量计算 getTrueWeight())和决策逻辑(如费率确定)内聚到该实体对应的领域对象中,使得代码结构更清晰,相关逻辑更容易定位和修改。
      • 信息隐藏探索: 尝试减少不必要的 get 方法暴露。例如,在 AbstractClientDomain 和 AbstractGoodsDomain 中,id_ 字段没有直接的 getId() 方法,这体现了向更严格封装迈出的一步,即对象只暴露完成其职责所必需的接口。
  • 数据实体层 (Entities):
    • 代表类: FlightEntity, OrderEntity
    • 职责: 在引入了更智能的领域对象后,原先的 Entity 类(如 FlightEntity 和 OrderEntity)的角色有所调整。它们更多地回归到作为纯粹的数据结构,用于承载那些业务行为相对简单、或在当前业务场景下更多作为静态信息的数据。例如,FlightEntity 依旧存储航班的基本信息,OrderEntity 存储订单的通用描述性信息。它们的行为主要体现在数据获取上。
  • 业务对象/数据传输载体 (BO/DTO for service input):
    • 代表类: FlightDataBo
    • 职责: 依然作为数据从读取层传递到服务层的标准数据包
    • 演进: 其内部成员从第一次作业的纯 Entity 对象,演变为包含了新引入的领域对象(如AbstractClientDomain, AbstractGoodsDomain[], AbstractOrderType)和部分保留的 Entity 对象(FlightEntity, OrderEntity)。这使得服务层可以直接利用领域对象封装的业务逻辑。
  • 业务逻辑层 (Service Layer):
    • 代表类: FlightService (接口), FlightServiceImpl (实现)
    • 职责: 继续作为业务流程的编排者和协调者。
    • 演进: 由于核心计算逻辑(如单件货物价格、客户折扣率)已下沉到领域对象中,服务层 FlightServiceImpl 的 processFlightDataBo 方法变得更为简洁。它更多地负责:
      • 协调领域对象: 调用领域对象的方法获取必要的计算结果(如goods_data[i].getPrices(), client_data.getBillingRates())。
      • 执行跨领域对象的规则: 例如,计算订单最终总价时,将货物总价与客户折扣率结合起来。
      • 流程控制与校验: 如检查订单总重量是否超出航班的最大载重。
      • 组装输出: 将处理后的数据转换为适合展示的视图对象(VO)。
    • 解耦: 服务层对具体业务规则实现的依赖降低,因为它现在与领域对象的抽象接口(或基类)交互。
  • 视图对象层 (View Objects):
    • 代表类: OrderVoInterface, OrderVo, GoodsVoInterface, GoodsVo
    • 职责: 专门为数据展示而设计。
    • 演进:
      • 移除计算逻辑: 第一次作业中,GoodsVo 的静态工厂方法 fromGoodsEntity() 承担了计算货物计费重量、费率和价格的逻辑。在第二次作业中,这些计算逻辑被移至 AbstractGoodsDomain 及其子类中。因此,GoodsVo 的构造函数现在直接接收已经由领域对象计算好的各项数据(计费重量、费率、价格)。
      • 更纯粹的数据载体: OrderVo 和 GoodsVo 现在更加接近纯粹的数据载体,其主要职责是聚合展示所需的数据,并提供格式化输出的方法 (getFormatString())。它们不再包含复杂的业务决策或计算。
      • 构造方式调整: 由于计算逻辑的转移,VO的创建方式也相应调整。它们现在直接消费由服务层从领域对象获取或计算得来的数据。第一次作业中的静态工厂方法 GoodsVo.fromGoodsEntity() 被移除,因为其核心功能已被领域对象取代。
  • 数据聚合输出对象 (DTO for service output):
    • 代表类: FlightDataDto
    • 职责: 与第一次作业保持一致,聚合服务层处理完毕后,需要传递给输出层(Writer)的所有视图对象。
  • 数据输入/输出层 (简化的Data Access/Presentation Layer):
    • 代表类: FlightDataReaderScannerImpl, FlightDataWriterStdIOImpl
    • 职责演进:
      • FlightDataReaderScannerImpl: 其职责有所增强。由于引入了不同类型的客户和货物,读取器现在需要根据输入流中的类型指示字符串(如 "Individual", "Corporate", "Dangerous" 等)来决定实例化哪个具体的领域对象子类。这应用了简单工厂模式的思想,将对象的创建决策封装在读取器内部。
      • FlightDataWriterStdIOImpl: 职责基本保持不变,负责接收 FlightDataDto并将其格式化输出。

问题与反思

这里也有一些很明显的,等待改进的问题,最明显的是作为数据输入层的FlightDataReaderScannerImpl,它的复杂度和职责都非常多,甚至可以说有些违反SRP,而通过适当设计,可以进一步细化这些逻辑。
同时,可以尝试引入策略模式来对计费策略等预留可扩展点,引入Mapper层次也可以解决对之前的fromEntity方法在Vo层产生的紧耦合。

对比第一次作业的关键变化与反思

相较于第一次作业,第二次作业在设计上的主要进步体现在:

  • 逻辑的重新分配: 核心业务计算逻辑从VO层和部分服务层逻辑中剥离出来,并封装到新引入的领域对象层。这使得各层职责更加清晰,VO层回归其作为展示数据载体的本质。
  • DDD思想的初步引入: 通过创建包含行为的领域对象(充血模型),使得业务逻辑与其所操作的数据更加内聚,提高了代码的可理解性和模块化程度。
  • 多态的有效运用: 利用继承和多态,服务层代码可以更通用地处理不同类型的业务实体,降低了因类型判断带来的复杂性,并为未来扩展新的客户/货物类型提供了便利。
  • 封装性的提升: 有意识地控制领域对象中getter方法的暴露,旨在保护对象内部状态的一致性。

这次重构虽然增加了领域对象的定义,并且通过将逻辑更合理地组织,使得整体系统的耦合度有所降低,特别是服务层与具体业务规则细节之间的耦合。VO层的瘦身也解决了第一次作业中反思到的VO层过“厚”的问题。
当然,这只是DDD实践的初步探索,其中没有暴露的领域模型的完整性和聚合边界等更深层次的设计问题,还需要在项目设计内进一步思考和完善。

类图

由于上文的设计部分比较基于代码,所以给出相应的类图,作为对DDD的初实践,写的可能不太好,见谅。

posted @ 2025-05-24 23:15  NyanInt  阅读(47)  评论(0)    收藏  举报