发现类---面向对象领域(Domain)的分析
翻译:王咏武
设计过程简介
● 通过考察问题领域(Domain)来识别出对象和类
-- 通常对象(Object)应该首先被识别出来
-- 把对象分类就可以形成类(Class)
● 识别出对象之间以及类之间的关系
-- 结构型关系(Structural relationship)
-- 协作型关系(Collaborative relationship)
● 分配职责
-- 依据协作型关系分配
● 不断重复(Iterate)上述过程
● 应该为问题领域建模,而不是为解决方案(Solution)建模
职责驱动的设计
● 是面向对象分析最广泛采用的方法
-- 基于"客户机/服务器(Client/Server)关系
● 通常“客户机/服务器”有两种含义:
-- 用于分布式系统,服务器提供对共享资源的访问,客户机提供用户界面
-- 用于面向对象系统,服务器是提供服务的对象
-- 我们使用第二种含义
● 客户机和服务器合作完成任务
-- 一个对象可以在一种关系中是客户机,而在另一种关系中是服务器
● 服务器的职责是提供某些服务
● 通常,一个对象的行为应该是确定的
CRC卡
● CRC方法为每个类制作一张索引卡,卡上标明该类的职责,以及完成每个职责需发生关系的其他类。在卡的背面写上对该类的简要描述
● 在下面的例子中,为了完成职责"Do something",类Foo必须和类X和Y协作(发送消息给它们)
实例:"小棍"游戏
● 该程序是一种计算机上的两人玩的游戏。有一些按行排列的小棍,当游戏开始时,这些小棍按照如下排列:
1:|
2:| |
3:| | |
4:| | | |
游戏规则:
● 游戏者依次进行,每人每次可以从一个非空行拿走一个或多个小棍,拿到最后一根小棍的游戏者输
● 游戏开始时,以及每次移动后,程序会显示游戏的状态,哪个游戏者将进行移动,他想移动的那行的小棍数量,以及他想移动的棍数
● 当移动非法时程序会提示用户(例如:希望移动的数目超过该行中的现有数量)
● 思考:
-- 提取对象和类
-- 使用CRC卡
-- 谁应该负责跟踪游戏者的移动次序?
行(Row)的CRC卡
寻找对象和类
● 从需求/用例说明书开始,列出所有的名词,这些都是初始的候选对象和类
-- 对每个候选对象,建议形成一个类
-- 确定概念上和物理上的实体(Entity)
-- 消除重复(代表相同事物的名词)
● 细化候选类,代表一个类的初始状态的名词应该是该类的属性
-- 形容词可能是属性或子类
-- 分离大而复杂的类,合并过小的类
● 寻找关系
-- 列出所有的动词,它们是主语名词所代表的类的候选行为或职责
● 剔除系统外的事物
-- 明确什么是系统的一部分
-- 用户不应该是系统的一部分
● 问问自己:
-- 为了使软件在未来易于扩展需要什么样的底层结构(Infrastructure)?
● 注意:好的流程有助于创建好的模型
从问题域(Domain)到模型(Model)
● 每个真实世界的系统应该按下列几个明显的问题域设计:
-- 业务问题(Business problem)
-- 数据的持久化(Persistent)存储
-- 用户界面
-- 系统上下文(Context)
-- 功能划分
● 注意:不要混淆不同问题域中的职责
分层的架构模型
模型中的物理实体
● 对于控制物理设备的软件特别重要,即什么情况下软件需要为物理对象提供一个好的模型
● OO编程起源于用一种好的方式写一个模拟器,即程序试图精确地再现某些真实的物理流程
● 例子:
-- 问题域实体(Entity):汽车、飞机…
-- 系统实体:打印机、调制解调器、传感器、制动器…
模型中的概念实体
● 优先权,访问权限,数据格式转换,交易管理,内存管理,错误管理,工作流,数据结构(图、树、队列、堆栈、哈希表),数学对象,应用,底层结构
建模类
● 动物
-- 狗
※ 德国牧羊狗
※ 狮子狗
※ 野狗
-- 猫
● 磁介质
-- 磁盘
-- 磁带
● 按照自然分类通常会形成好的继承树
建模对象组
● 游戏布局
-- 行(Row)
※ 小棍
● 集团
-- 公司
※ 部门
◆ 雇员
● 对象的组应按照组合(Compsition)或聚合(Aggregation)关系建模,集合(Collection)是一种特殊的组
建模执行(Executive)或控制(Controller)实体
● 通常需要一个对象来管理(coordinate)其他对象。由需求说明书中的被动语态的动词暗示
● 例子:事件管理器、游戏裁判
建模你必须调用的模型外的事物
● 这一点在真实世界中非常重要
● 例子:
-- 遗留代码和数据源
-- 操作系统
-- 第三方软件包
-- 系统接口
● 标示外部事物提供的服务
● 生成提供服务的对象
● 考虑为远程服务提供一个代理(Proxy)对象来封装所有的分布式通讯
检查你的类
● 每个类都应该有一个清晰的、易于理解的名字,这个名字应该听起来象一件事物,而不是一个功能
● 每个类都应该有一个清晰的、易于理解的意图,该意图能适用于它的所有子类
● 相似但不同的类建议用继承,或者有可能的话代理到一个第三方的共享类
● 当多个类有继承关系时,它们才可能相互覆盖(Overlap),否则类之间不可能有覆盖关系。此时一个类完全覆盖其它的类
● 不同的类可能合作来完成职责
● 类应该有语义相关的属性和方法
结构型关系
● 组合
-- B是A的一部分
-- A拥有B
-- A包含B
-- A拥有B的一个集合(Collection)
● 子类/超类
-- A是B的一种
-- A是B的泛化(Generalization)
-- A是B的特殊情况
-- A的行为象B
协作型关系
● A依赖于B
● A使用B
● A代理B
● A需要B的帮助
● A通知B
● A和B地位相同
职责和协作
● 职责:
-- 一个类的实例提供给其他对象的服务
-- 一个类的所有实例有相同的职责
-- 职责包括:
※ 执行动作(方法、行为)
※ 维护和提供信息(状态、属性)
※ 约束条件
● 协作:
-- 客户机/服务器关系
-- 协作是某一个类调用其他类来协助完成职责
职责驱动的设计
● 类的职责定义了它的公共接口
-- 浏览用例,寻找对象间的交互
-- 对这些交互,确认客户机/服务器关系
-- 每个交互暗示着服务器类的一个职责
-- 从客户的角度用一个短语来表达职责,这就是支持该职责的服务器类的方法的名字
-- 如果需要的话创建新的类
※ 大的职责暗示服务器类应该把一些工作委派(Delegate)给其他类
※ 如果一个客户需要不属于任何一个已存在的类的服务时,创建一个新的类
分配职责
● 从这一步开始思考程序如何工作
● 铺开所有信息,行为和接口
● 尽可能的描述职责
● 保持信息的独立
● 职责可以被共享或代理
● 拿不准时,创建新类或删除旧类
● 没有任何职责的类是多余的
● 当你的设计太复杂时就重构它
● 检查设计看是否能支持所有的功能
-- 按照用例,模拟模型的行为
● 不断迭代,重复
实例:微波炉模拟器
● 需求描述:
-- 微波炉通过微波照射来加热食物,用户使用键盘设定加热时间和微波功率,键盘上还有一个开始按钮。当时间到时,微波炉会停止并且响铃。如果门打开微波炉也会停止。假设微波发生器能够控制电子速调管在一秒的工作周期中工作
● 请为该系统寻找类
微波炉-用例
● 设置时间
-- 用户输入一个数字,然后按设置时间按钮
-- 系统显示剩余的时间
-- 系统设置时间
● 设置微波功率
-- 用户输入一个数字,然后按设置微波功率按钮
-- 如果数字不在1-10之间,什么也不做
-- 系统显示功率级别
-- 系统设置微波发生器
● 开门
-- 停止时间计数
-- 禁止微波发生器生成微波
● 关门
-- 启动微波生成器
● 按开始按钮
-- 如果门是关闭状态
-- 开始计数
-- 当时间大于0,生成微波
-- 不断显示剩余的时间
-- 当时间等于0,停止并响铃
微波炉-类
● 微波炉
● 微波发生器
● 计时器
● 时钟
● 键盘
● 开始按钮
● 门
● 铃
● 其它按钮:清除,数字0-9,设置时间,设置微波功率
● 电子速调管
● 显示器
微波炉-架构
● 用户界面中有三种类型的事件:
-- 按钮被按下
-- 门被打开或被关上
-- 从时钟芯片来的计时事件
● 我们不想持续检查门来看它是开还是关,也不想持续检查时钟来看已过去多少时间。一种办法是当门打开或关上时,或时钟走过一个单位时,通知微波炉
● 事件驱动的架构好于轮询的架构
● 先假设你感兴趣的事件会造成你的类里的方法被调用。先不管如何来实现这一点
● 我们希望微波炉的所有部件(计时器、键盘等)能被其他种类的炉子重用。当然微波炉本身不能被重用,因为它和其他种类的炉子不同
● 按钮类没有系统控制的职责。我们会保持按钮类简单,它只有一个统一的接口函数:Push()。再次强调,我们希望有一个可重用的设计。因此,我们不想让按钮类关心系统中的其他部件
微波炉-职责
● 微波炉本身:
-- 知道系统的状态
-- 把按钮命令职责委派给聚合对象
-- 当门打开或关上时得到通知
※ 关闭微波发生成器
-- 当时间减到0时得到通知
※ 关闭生成器
※ 响铃
-- 当时间走一个单位时得到通知
※ 更新显示
※ 通知微波发生器
● 所有按钮:
-- 被按时,送一个命令给微波炉
● 微波发生器:
-- 知道功率强度
-- 如果启动,按照微波级别生成微波
-- 当时间走一个单位时得到通知
※ 按照微波强度(1-10之间的整数)在一个微波周期激活电子速调管
● 计时器:
-- 知道剩余的时间
-- 计数
-- 通知微波炉下列事件:
※ 时钟走了一个单位(十分之一秒)
※ 剩余时间为0
● 显示器:
-- 显示剩余的时间
-- 显示微波强度
-- 知道并且显示键盘当前输入的数字
● 键盘:
-- 按钮的容器类。注意:这是一个不确定的职责,从下一次迭代的模型中删除键盘类
● 门:
-- 通知微波炉开门和关门事件
● 铃:
-- 响铃
微波生成器的CRC卡
UML预览:微波炉
● 比较两种不同的微波炉系统的UML类图
● 注意:UML类图是静态的,所以它不能用来对系统的动态行为建模
微波炉类图1
● 轮询架构
● 工作良好,但是…
● 需要两个线程
● 高度的对象耦合
● 复杂
● 键盘不可重用
● 计时器不可重用
● …
微波炉类图2
比较两个微波炉模型
● 模型2有一个微波炉类(Mediator设计模式的一个例子),注意各部件之间的协作非常简单,因为它们是松散耦合的,所以更独立
● 模型1使用轮询架构,而模型2采用事件驱动
● 模型2根本没有一个键盘类,因为该类没有任何职责,但是在模型1,它有职责
● 模型1和模型2处理10个数字按钮的方式不同
● 模型1有一个秒表(StopWatch)类
● 模型2不但简单而且更灵活
词汇表
● 问题域 Domains
● 职责 Responsibilities
● 协作 Collaborations
● 代理,委派 Delegations
● 重构 Refactoring
● 关系 Associations
-- 特化 / 泛化 / 继承 Specialization / Generalization / Inheritance
-- 协作 / 聚合 / 组合 / 集合 / 依赖 Collaborations / Aggregations / Compositions / Collections / Dependencies
● 组 Group
-- 聚合 / 组合 / 集合 Aggregations / Compositions / Collections
● 遗留系统 Legacy Systems
● 代理 Proxies
● 中间人设计模式 The Mediator design pattern
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步