我们知道,软件设计包括软件的整体架构设计和模块的详细设计。
在上一篇文章(见 《架构技能(五):软件设计(上)》)谈了软件的整体架构设计,今天聊一下模块的详细设计。
模块的详细设计,重点体现在需要设计几个具有明确职责的角色,以及角色之间应该设计成什么样的关系,这里的角色一般是一个很大的类;然后对每一个角色继续分析需要设计几个类来实现角色职责,以及类与类之间应该设计成什么样的关系。
模块的详细设计,应该像架构设计一样,由高到低,逐层进行。
在之前的文章(见《架构技能(一):软件架构》)中,分析过软件架构,见下图。
软件架构是指软件系统的顶层结构,包含具有明确职责的角色,这些角色通过相互协作使软件系统提供业务能力。
一个模块应该设计成几个类,取决于业务场景;但是类与类之间的关系是固定的模式,从原子化不可再分的角度看,共包括六类关系:依赖、关联、聚合、组合、继承、实现;“继承” 和 “实现” 更多是为了多态特性的实现需要,在基础框架中应用普遍,在业务系统中以前四种类关系应用为主。
依赖
依赖方与被依赖方是 “使用” 关系,从代码上看,被依赖方对象往往出现在依赖方对象的方法内部。比如:依赖方是 Person,被依赖方是 Bus,人 “使用” 公共汽车,见下图。
关联
关联方与被关联方是 “拥有” 关系,从代码上看,被关联方对象往往出现在关联方对象的成员变量上。比如:关联方是 Person,被关联方是 Phone,人 “拥有” 手机,见下图。
聚合
聚合方与被聚合方是整体与个体之间的关联关系,整体 “拥有” 个体,个体 “聚合” 成整体。从代码上看,被聚合方对象出现在关联方对象的成员变量上。比如:聚合方是 Company,被聚合方是 Staff,公司 “拥有” 员工,员工 “聚合” 成公司,见下图。
组合
组合方与被组合方仍是整体与个体之间的关联关系,整体 “拥有” 个体,个体 “组合” 成整体,与聚合关系不同的是,被组合方不能独立存在。从代码上看,被组合方对象出现在组合对象的成员变量上,且在组合对象内部进行初始化。比如:组合方是 Computer,被组合方是 Cpu,计算机 “拥有” CPU,CPU “组合” 成了计算机,且 CPU 离开计算机不能独立存在,见下图。
总结一下在软件模块详细设计中,类与类之间最常用的四种关系:
-
依赖的核心是 “使用”,两个对象在同一层次上;
-
关联的核心是 “拥有”,两个对象在同一层次上;
-
聚合的核心是 整体与个体之间的 “拥有” 与 “聚合”,个体离开整体后对象的生命周期可以继续;
-
组合的核心是 整体与个体之间的 “拥有” 与 “组合”,个体离开整体后对象的生命周期结束。
理解这四类对象关系,并烂熟于心,那就可以对软件业务系统模块进行详细设计了。
这里,我们以 基于时间轮的 HTTP 长轮询获取消息为例,描述软件模块详细设计的过程。
先简述一下 HTTP 长轮询,见下图。
-
http 客户端向 http 服务端发起 http 请求;
-
http 服务端 hold 住该请求,不会立刻返回 http 响应;
-
http 服务端只有满足两个条件中的任何一个才会返回 http 响应: 要么超过一定时间(超时),要么产生了属于客户端的消息
-
http 客户端收到响应后,再次发起 http 请求,重复上述过程。
在这个场景里, http 服务端应该如何设计呢?
分析上述时序图,可以很容易得出:http 服务端需要包含三个角色,即 http服务、消息模块和时间轮模块,三个角色之间的关系,见下图。
-
http 客户端向 http 服务端发起 http 请求;
-
http 服务端 hold 住该请求,不会立刻返回 http 响应;
-
http 服务端只有满足两个条件中的任何一个才会返回 http 响应: 要么超过一定时间(超时),要么产生了属于客户端的消息
-
http 客户端收到响应后,再次发起 http 请求,重复上述过程。
在这个场景里, http 服务端应该如何设计呢?
分析上述时序图,可以很容易得出:http 服务端需要包含三个角色,即 http服务、消息模块和时间轮模块,三个角色之间的关系,见下图。
-
时间轮对于 http 服务来说是一个工具,http 服务使用时间轮这个工具进行管道的注册,所以 HttpService 类 “依赖” 于 Wheel 类;
-
消息模块产生新的消息时,根据客户端的 uid 从时间轮中获取客户端的管道,所以 MsgManager 类 “依赖“ 于 Wheel 类;
-
消息模块与 http 服务之间没有直接关系,消息模块只是通过管道对象将消息传输给 http 服务而已。
明确了第一层的类关系,就可以对第二层的类关系进行展开分析和设计了。http 服务与消息模块留给大家进行设计,我们看时间轮模块如何设计。
对类内部的设计,需要从类对外提供的能力来入手分析。
通过对 http 服务、消息模块、时间轮模块三个角色之间的关系分析,我们知道时间轮对外提供了三项能力:
-
对客户端管道对象的注册;
-
对客户端超时信息的发送;
-
根据客户端 uid 获取用户的管道。
注册客户端管道时,需要包括用户 uid、用户管道、注册时的时间轮刻度等信息;对客户端超时信息发送时,需要根据时间轮刻度找到用户管道;而获取用户管道时,需要根据客户端 uid 获取。所以,为了方便查询,用户 uid 与时间轮时间应该是一个双向映射的关系;同时,便于维护,将用户管道与时间轮时间封装成用户信息对象。
这里的管道对象,在不同的语言中可以采用不同的实现方式,比如 Go 语言中可以采用 chan ,Java 语言中可以采用队列。时间轮模块内部原理,在之前的文章(见《单体架构 IM 系统之长轮询方案设计》)中有详细描述,见下图。
时间轮模块类 Wheel 内部应包含三个类: 时间盘(TimeDisk)、用户信息映射(UserInfoMap)、时间轮刻度映射(TimeUserMap),这几个类之间的关系,见下图:
-
时间轮对象 “拥有” 时间盘、用户信息映射和时间轮刻度映射三个对象,这三个对象组合成了时间轮对象,所以 Wheel 与 TimeDisk、UserInfoMap、TimeUserMap 是组合关系,离开了 Wheel,这三个对象毫无意义;
-
时间盘 TimeDisk 对象,提供时间指针每秒走一格的能力;下一格的时间刻度所映射的所有客户端列表就是超时的客户端;
-
用户信息映射 UserInfoMap 对象提供用户 uid 对用户信息对象的映射能力;
-
时间轮刻度映射 TimeUserMap 对象提供时间轮时间刻度对用户 uid 列表的映射能力;
-
TimeDisk、UserInfoMap、TimeUserMap 三个对象之间没有任何关系。
我们继续对 UserInfoMap 设计第三层类关系,TimeDisk 和 TimeUserMap 留给大家分析和设计,见下图。
-
UserInfoMap “拥有” UserInfo,UserInfo “聚合” 成了 UserInfoMap,UserInfoMap 和 UserInfo 是聚合关系;
-
UserInfo 中封装了管道 UserChan 对象,UserInfo “拥有” UserChan 对象,两者是关联关系。
对业务模块的类关系,通常设计到第三层,整个类关系脉络应该就非常清晰了;http 服务端,将上述三层类关系进行整合,见下图。
在这个简单的整体类关系图中,依赖、关联、聚合、组合四种类关系全部进行了应用。明确了整个模块中关键类之间的关系后,剩下的就是对方法逻辑的编写了,这对于初级程序员来说就不是个事了!
最后,总结文中关键:
-
模块的详细设计,重点体现在需要设计几个具有明确职责的角色(类),以及角色(类)之间应该设计成什么样的关系;
-
模块的详细设计,应该像架构设计一样,由高到低,逐层进行;
-
类之间包括六类关系:依赖、关联、聚合、组合、继承和实现,在业务系统中以前四类为主:
(1)依赖的核心是 “使用”;
(2)关联的核心是 “拥有”;
(3)聚合的核心是 整体与个体之间的 “拥有” 与 “聚合”,个体离开整体后对象的生命周期可以继续;
(4)组合的核心是 整体与个体之间的 “拥有” 与 “组合”,个体离开整体后对象的生命周期结束;
-
对类内部的设计,需要从类对外提供的能力来入手分析;
-
基于时间轮方案实现的 http 长轮询中,关键类之间的关系如下:
(1)HttpService 依赖 Wheel;
(2)MsgManager 依赖 Wheel;
(3)Wheel 组合 TimeDisk;
(4)Wheel 组合 UserInfoMap;
(5)Wheel 组合 TimeUserMap;
(6)UserInfoMap 聚合 UserInfo;
(7)UserInfo 关联 UserChan。