Day25-02|5.3.2 面向对象方法
Day25-02|5.3.2 面向对象方法:用对象、责任和协作表达复杂业务
本文对应《系统架构设计师教程(第2版)》第5章“软件工程基础知识”5.3.2“面向对象方法”。面向对象方法以对象为基本抽象单位,把数据和操作数据的行为封装在一起,通过类、对象、继承、组合、多态和消息协作构造软件系统。它不仅是一种编程方式,更是一套从需求分析、领域建模、架构设计到实现测试的系统化方法。系统架构师要重点掌握如何识别对象责任、控制耦合、表达变化和建立可演化边界。
一、学习目标与考试定位
完成本节后,应能够解释对象、类、属性、操作、消息、接口、继承、封装和多态等概念;掌握面向对象分析、设计和实现的基本过程;理解用例图、类图、顺序图、状态图、活动图等 UML 模型的用途;能够运用高内聚、低耦合、依赖倒置、开闭原则和组合优于继承等思想进行设计;能够分析继承滥用、贫血模型、循环依赖和上帝类等问题;能够在案例题和论文中描述面向对象方法如何支持架构复用与演化。
软考高级题目常给出一段业务描述,要求识别参与者、用例、核心对象、关系和状态变化,或者分析某个类设计为什么难以维护。答题时不能把“对象”简单理解为数据库表,也不能把“面向对象”变成罗列四大特征。应从业务责任、协作、边界、变化和质量属性出发。
二、面向对象的基本概念
对象是具有状态、行为和标识的实体。状态由属性及其当前值表示,行为由操作表示,标识用于区分不同对象。类是具有相同属性、操作和关系的一组对象的抽象。对象是类的实例,但并非所有对象都必须直接映射到数据库记录,也可以表示策略、服务、事件、控制器、设备或外部系统适配器。
封装把对象内部状态和实现细节隐藏起来,仅通过公开操作或接口与外部协作。封装的价值是保护不变量、隔离变化和降低耦合。继承表示一般类与特殊类之间的“是一种”关系,可以复用属性和行为;多态允许不同对象对同一消息作出不同实现,使调用方依赖抽象而不是具体类型。组合表示对象之间的“拥有”或协作关系,通常比继承更灵活。
面向对象的主线可以概括为:
识别职责 → 封装状态与行为 → 定义抽象接口 → 组织对象协作 → 隔离变化 → 持续验证
三、面向对象分析
面向对象分析关注业务问题域和系统职责,目标是理解系统做什么,而不是立即决定代码结构。首先识别参与者、业务目标和用例;其次从用例和领域语言中识别候选对象、概念、属性和关系;然后分析对象的职责、协作、状态和约束;最后建立分析模型并与利益相关者确认。
候选对象可以来自名词,但不能机械地把每个名词都建成类。应判断它是否有独立身份、状态、行为、生命周期和业务责任。动词常提示行为,但行为应归属拥有相关数据和不变量的对象,而不是集中到一个“管理器”类。分析时还要识别外部系统、边界对象、控制对象和实体对象的差异。
用例描述参与者为达到目标与系统交互的场景,包括前置条件、主成功场景、异常流程和后置条件。用例不是页面清单,而是用户可感知的业务目标。质量属性场景也应进入分析,例如高峰时提交订单、下游支付超时、用户重复提交和权限不足时系统如何响应。
四、面向对象设计
面向对象设计把分析模型转化为可实现的设计模型,确定类、接口、职责、协作、关系、数据访问、异常处理和部署边界。设计原则是让对象责任清晰,接口稳定,依赖方向合理,变化隔离,协作可测试。
高内聚表示类的职责彼此相关;低耦合表示类之间依赖少且明确。单一职责原则要求一个类因一个主要原因变化;开闭原则要求对扩展开放、对修改关闭;里氏替换原则要求子类型能够替代父类型而不破坏客户端预期;接口隔离原则要求接口精简、面向客户;依赖倒置原则要求高层策略依赖抽象而非低层细节。
这些原则不是绝对规则。过度拆分接口会增加理解和配置成本,过度抽象会造成复杂性,继承层次过深会让行为难以追踪。架构师应根据变化点、团队能力、性能和部署需求判断,优先保护真正稳定和重要的边界。
五、UML 常用图及用途
用例图展示参与者与用例的关系,适合表达系统边界和用户目标;类图展示类、属性、操作及关联、聚合、组合、泛化和依赖,适合表达静态结构;顺序图展示对象按时间顺序发送消息,适合分析调用协作和接口;通信图强调对象之间的链接和消息;状态图展示对象在事件触发下的状态变化;活动图展示业务流程、分支、并发和泳道;组件图展示可替换组件和依赖;部署图展示节点、执行环境和制品部署关系。
| UML 图 | 主要视角 | 典型问题 |
|---|---|---|
| 用例图 | 需求与边界 | 谁使用系统、目标是什么 |
| 类图 | 静态结构 | 有哪些抽象、责任和关系 |
| 顺序图 | 动态协作 | 谁先调用谁、消息如何传递 |
| 状态图 | 状态变化 | 事件如何导致状态迁移 |
| 活动图 | 流程与并发 | 业务步骤、分支和并行如何组织 |
| 组件图 | 实现结构 | 组件如何依赖、替换和集成 |
| 部署图 | 运行拓扑 | 制品部署在哪些节点 |
图的价值不在于数量,而在于回答问题。若画图只是把代码类名复制上去,却不能说明业务、协作、约束和变化,模型就没有发挥作用。不同图之间要保持一致,例如顺序图中的消息应能在类图或接口中找到,状态图中的状态应与业务规则和测试场景对应。
六、关系:关联、聚合、组合、泛化与依赖
关联表示对象之间存在结构联系,例如客户拥有订单;聚合表示整体与部分关系,但部分可以独立存在;组合是更强的整体与部分关系,部分的生命周期依赖整体,例如订单与订单明细;泛化表示一般与特殊关系;依赖表示一个元素的变化可能影响另一个元素,通常较弱且方向明确。
关系选择要依据生命周期、所有权和替换需求,不应为了图形好看随意使用聚合或继承。若一个订单明细离开订单毫无业务意义,可考虑组合;若不同支付方式只是可替换策略,应采用接口和组合,而不一定建立深层继承。关系越多,维护和理解成本越高。
七、状态和行为建模
许多业务对象的核心难点不在属性,而在状态变化和不变量。例如订单可能经历草稿、待支付、已支付、已发货、已完成和已取消,不同状态允许的操作不同。状态模型应定义事件、前置条件、动作、目标状态和异常处理,防止通过简单字段更新绕过业务规则。
状态机可以帮助识别非法迁移、重复操作、超时和补偿。对支付、库存、审批、工单和设备控制等领域,应把状态变化作为架构和测试的重要对象。分布式系统中还要考虑消息重复、乱序、延迟、故障恢复和最终一致性,不能假设所有事件严格按本地调用顺序到达。
八、对象协作与设计模式
对象通过消息、方法调用、事件或接口协作。调用方应只依赖必要抽象,避免知道被调用对象的内部数据和实现。常见设计模式提供可复用的协作结构:策略模式封装可替换算法,工厂模式封装对象创建,观察者模式支持事件通知,适配器模式连接不兼容接口,装饰器模式动态增加职责,模板方法固定流程骨架,状态模式封装状态相关行为。
设计模式不是必须使用的清单。只有当问题中确实存在可替换算法、复杂创建、不兼容接口或状态行为变化时才采用。为了“体现面向对象”而堆叠模式,会增加间接层和调试成本。架构师应说明模式解决的变化点、带来的代价和验证方式。
九、领域对象与贫血模型
贫血模型把对象设计成只有属性和 getter/setter,业务规则集中在服务类中,容易导致数据和行为分离、规则散落、事务边界模糊。对于复杂领域,应该让拥有数据和不变量的对象承担相应行为,例如订单负责检查可取消条件、账户负责余额约束、库存聚合负责库存变更规则。
这不意味着所有行为都必须放进实体。跨聚合协调、外部系统调用、查询编排和基础设施能力可以由领域服务或应用服务承担。关键是区分领域规则、应用流程和基础设施细节,避免一个服务类变成上帝对象,也避免实体直接依赖数据库、网络和框架。
十、面向对象方法与架构设计
面向对象建模可用于发现业务边界和服务边界。高内聚的聚合或领域模块可以成为服务候选,跨边界调用和共享数据揭示耦合;接口、事件和适配器可以隔离外部系统;多态和策略可以支持业务规则扩展;组件和部署图可以连接逻辑结构与运行拓扑。
但类模型不能直接等于微服务架构。类粒度、模块粒度、组件粒度和服务粒度关注点不同,直接把每个类部署成服务会造成网络调用泛滥、事务复杂和运维负担。架构师应根据业务能力、数据所有权、团队边界、独立演化和运行规模确定部署边界。
十一、案例分析:支付方式扩展
某电商系统最初只有银行卡支付,PaymentService 中写满了银行卡校验、请求支付、查询结果和异常处理。后来需要接入微信、支付宝、企业账户和海外支付,团队计划继续在同一个方法中增加条件分支。
问题在于支付方式的变化会持续修改核心服务,分支复杂度、测试组合和第三方耦合不断增加。可以抽象 PaymentStrategy 接口,定义授权、查询、退款和对账等稳定能力;每种支付方式通过适配器实现接口,隔离第三方协议;使用工厂或注册表根据支付类型选择策略;将订单状态机和支付状态机分开,使用幂等键处理回调和重复请求;把密钥、证书、超时和重试放到受控配置和基础设施层;对关键流程建立顺序图、状态图和异常测试。
架构师还要判断接口是否真的稳定。不同渠道的退款、分账和对账语义可能差异很大,不能用过度抽象掩盖业务差异。公共接口只抽象共同能力,特殊能力通过扩展接口或领域能力表达,并记录兼容和演化策略。
十二、课后练习
练习一:单项选择
面向对象方法中,封装的主要作用是( )。
A. 让所有属性公开;B. 隐藏内部实现、保护状态和降低耦合;C. 取消对象之间的协作;D. 强制所有对象继承同一父类。
答案:B。封装通过受控接口保护内部状态和不变量,隔离实现变化。
练习二:简答题
请说明组合为什么通常比继承更有利于变化隔离。
参考答案:组合通过对象协作和接口委托实现行为,可以在运行时替换组件,依赖关系较松;继承会形成编译期层次和父子契约,子类容易受到父类实现和变化影响,层次过深时行为难以理解。若业务关系不是稳定的“是一种”,应优先考虑组合;但对真正稳定、可替换且符合替换原则的类型关系,继承仍可使用。
练习三:案例分析
某审批系统把所有业务规则都写在一个 ApprovalService 中,新增审批类型只需继续增加 if-else,但代码已超过数千行且修改经常影响已有流程。请分析并提出面向对象改进方案。
参考答案:ApprovalService 承担了过多职责,规则与流程耦合,属于上帝类倾向;条件分支增加会扩大回归范围并违反开闭原则。应识别审批类型、审批节点、规则和状态等领域对象,将相对独立的规则封装为策略或规则对象;通过接口和工厂/注册机制选择审批策略;让审批单或聚合维护状态不变量,应用服务负责流程编排和事务;对外部系统使用适配器;用状态图和顺序图验证迁移与协作;建立单元、组合和回归测试,逐步迁移旧规则并保持接口兼容。
十三、论文素材
论文可写:在业务规则复杂且变化频繁的项目中,我采用面向对象方法从用例和领域语言中识别核心对象、职责、状态和协作关系,通过类图、顺序图和状态图澄清静态结构与动态行为;以封装、高内聚、低耦合、依赖倒置和组合优于继承为原则划分模块和接口,将易变规则封装为策略或领域对象,将外部系统通过适配器隔离。对订单、支付和审批等关键对象建立状态机和不变量,结合自动化测试、接口契约和需求追踪验证设计,避免上帝类、贫血模型和循环依赖,提升系统可维护性和扩展性。
十四、今日复盘
面向对象方法以对象的状态、行为和标识为基础,通过封装、抽象、继承、多态、组合和消息协作构建系统。分析阶段关注参与者、用例、领域对象和责任,设计阶段关注职责、接口、状态、耦合和变化;UML 不同图表达需求、静态结构、动态协作、状态、组件和部署视角。面向对象不是把每张表变成一个类,也不是堆砌设计模式,而是让责任和变化边界清晰。第5.3节至此完成,下一篇进入 5.4.1 测试方法。