程序设计的所有原则和方法论都是追求一件事——简单——功能简单、依赖简单、修改简单、理解简单。因为只有简单才好用,简单才好维护。因此,不应该以评论艺术品的眼光来评价程序设计是否优秀,程序设计的艺术不在于有多复杂多深沉,而在于能用多简单的方式实现多复杂的业务需求;不在于只有少数人能理解,而在于能让更多人理解。
系列文章
程序设计——方法论总结
程序设计——领域驱动设计
前言
本篇希望可以为你解决如下问题提供思路
- 代码很难阅读:命名不规范、不地道,方法和类太臃肿,类之间调用关系绕来绕去
- 类分类不清楚,不知道一个类应该放哪个包下
- 分层不清楚,不知道哪层应该放什么样的代码
- 业务代码比较分散
- 扩展性差,牵一发动全身
- 做需求分析、系统设计、甚至是与团队沟通方案时不知道使用什么图做描述,或者使用的图不够地道
本篇是程序设计方法论的总结,其中有些不详细的地方请谅解,还望诸君查阅更多资料。如果有表达不正确的地方,也请指出。
1. 设计原则
1.1 开闭原则(Open Closed Principle)
描述
Software entities like classes, modules and functions should be open for extension but closed for modification
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
适用场景: 模块、类、接口、方法
反例表现: 任何改动都只能在原来代码上删除和增加代码,牵一发而动全身,破坏原有代码的完整性,原来的功能都需要重新回归测试,才能保证功能没被影响。
好处: 扩展性好,代码变动风险低
如何做: 分离不变部分和易变部分,对不变部分做封装,对易变部分提供接口或者方法做扩展。
思考: 开闭原则是一切原则的总结,所有的原则都是为了实现开闭原则。
示例
先给出实现搜索功能的一个反例,如下
public class SearchService {
public Result search(Params params) {
// 关键词搜索
if ("keyword".equals(params.getType())) {
return searchByKeyword(params);
}
// 类目搜索
if ("category".equals(params.getType())) {
return searchByCategory(params);
}
// 促销搜索
if ("promotion".equals(params.getType())) {
return searchByPromotion(params);
}
return null;
}
}
修改后的代码,如下
// 定义一个搜索接口
public class ISearch{
Result search(Params params);
}
public class SearchService {
// 使用一个容器来管理每个搜索类型的实现
Map<String, ISearch> context = Map.of(
"keyword", new KeywordSearch(),
"category", new CategorySearch(),
"promotion", new PromotionSearch());
public Result search(Params params) {
// 从容器中获取对应的搜索对象
ISearch search = context.get(params.getEntryType());
if (search == null) {
return null;
}
return search.search(params);
}
}
1.2 单一职责原则(Single Responsibility Principle)
描述
There should never be more than one reason for a class to change
适用场景: 模块、类、接口、方法
反例表现: 代码结构不清,代码量大呈现臃肿,重复代码。后果可读性差,不可维护,牵一发而动全身。
好处: 代码粒度小,可读性好。
如何做: 通过横向和纵向划分职责,组织代码结构,可使代码层级结构更清晰
示例
下面代码违法单一职责
public interface UserService {
void register(String telephone, String password);
void login(String telephone, String password);
void sendEmail(String email);
}
修改后代码如下
public interface UserService {
void register(String telephone, String password);
void login(String telephone, String password);
}
public interface EmailService {
void sendEmail(String email);
}
1.3 依赖倒置原则(Dependence Inversion Principle)
描述
High level modules should not depend upon low level modules. Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
- 上层模块不应该依赖底层模块,它们都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
作用
3. 避免类之间耦合
4. 提高系统稳定性
如何做
面向接口编程、IOC(spring ioc)、SPI
示例
以用户注册成功后发布成功消息为例子
// 1. 定义一个用户注册完成消息发布接口
public interface IUserRegisteredPublisher{
void publish(User user);
}
// 2. 实现用户注册完成消息发布接口,把消息发布到kafka
public class UserRegisteredKafkaPublisher implements IUserRegisteredPublisher{
@Override
public void publish(User user) {
System.out.println("send info to kafka");
}
}
// 3. 定义用户注册服务
public class UserRegisterService{
IUserRegisteredPublisher publisher;
UserRegisterService(UserRegisterService publisher) {
this.publisher = publisher;
}
public void register(User user) {
// 用户验证、保存
publisher.publish(user);
}
}
// 4. 定义一个测试类
public class Test {
public void test() {
IUserRegisteredPublisher publisher = new UserRegisteredKafkaPublisher();
UserRegisterService service= new UserRegisterService(publisher);
service.register(new User());
}
}
1.4 接口隔离原则(Interface Segregation Principle)
描述
- Clients should not be forced to depend upon interfaces that they don`t use.
- The dependency of one class to another one should depend on the smallest possible.
1、客户端不应该依赖它不需要的接口。
2、类间的依赖关系应该建立在最小的接口上。
作用
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下优点。
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
如何做
按单一职责拆分接口,降低接口粒度
示例
一个臃肿的接口
public interface UserService {
void register(String telephone, String password);
void login(String telephone, String password);
User getUserById(long id);
User getUserByTelephone(String telephone);
void changePassword(long id, String password);
}
使用接口隔离原则进行拆分
// 1. 用户注册接口
public interface UserRegisterService {
void register(String telephone, String password);
}
// 2. 用户登录接口
public interface UserLoginService {
void login(String telephone, String password);
}
// 3. 用户查询接口
public interface UserQueryService {
User getUserById(long id);
User getUserByTelephone(String telephone);
}
// 4. 用户修改接口
public interface UserUpdateService {
void changePassword(long id, String password);
}
1.5 里氏替换原则(Liskov Substitution Principle)
描述
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it. 所有引用基类的地方必须能透明地使用其子类的对象
里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。
继承的优点:
- 子类拥有父类的所有方法和属性,从而可以减少创建类的工作量。
- 提高了代码的重用性。
- 提高了代码的扩展性,子类不但拥有了父类的所有功能,还可以添加自己的功能。
继承的缺点:
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
- 降低了代码的灵活性。因为继承时,父类会对子类有一种约束。
- 增强了耦合性。当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。
如何做:
里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。否则会出现逻辑混乱。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比- 父类方法的输入参数更宽松。(即只能重载不能重写,子类方法的参数类型应该是父类方法参数类型的父类,否则会出现本应调父类方法,却调到子类方法了)
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。(即子类方法返回类型应该和父类抽象方法返回类型一样或者是父类方法返回类型的子类)
示例
下面是一个违反原则的例子。由于正方形类重写了父类的方法,resize中如果传入正方形对象就会发生无限循环。改进做法是再添加一个父类,他们同时继承自这个父类。
// 3. 添加测试类
public class Test {
/**
* 重置长方形的大小。对长不短的增加直到超过宽
*/
public void resize(Rectangle r) {
while (r.getHeight() <= r.getWidth()) {
r.setHeight(r.getHeight() + 1);
}
}
}
// 2. 定义一个正方形类,继承长方形类
public class Square extends Rectangle {
public Square(int width) {
super(width, width);
}
@Override
public void setWidth(double width) { // 重写父类方法
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(double height) {// 重写父类方法
super.setHeight(height);
super.setWidth(height);
}
}
// 1. 定义一个长方形类
public class Rectangle {
private double width;
private double height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
}
1.6 迪米特原则(Law of Demeter)
描述
Talk only to your immediate friends and not to strangers。即一个对象只与它亲密的对象交互,而不应该关注其它的对象(其亲密对象的亲密对象)。
如何做: 从迪米特法则的定义和特点可知,它强调以下两点:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
作用:
- 降低类之间的耦合。由于每个对象尽量减少对其他对象的了解,因此,很容易使得各模块功能独立,相互之间不存在(或很少有)依赖关系。
示例
有以下基础类,用这些类来打印学生信息和学生的授课老师信息
public class StudentService {
public Student getByName(String name) {
// 省略实现...
return new Student();
}
}
public class TeacherService {
List<Teacher> getByIds(Collection<Long> ids) {
// 省略实现...
return Collections.emptyList();
}
}
@Getter
public class Student {
private String name;
private Set<Long> teacherIds;
}
@Getter
public class Teacher {
private String name;
}
反例:
下面违反迪米特法则,
public class StudentPrinter{
private StudentService studentService;
public void print(String studentName) {
// 获取学生和授课老师信息
Student student = studentService.getByName(studentName);
TeacherService teacherService = studentService.getTeacherService();
// 拼接和打印信息
StringBuilder msg = new StringBuilder("学生姓名:").append(student.getName());
List<Teacher> teacherList = teacherService.getByIds(student.getTeacherIds());
msg.append("\n授课老师:\n");
for (Teacher teacher : teacherList) {
msg.append("老师姓名: ").append(teacher.getName());
}
System.out.println(msg);
}
}
public class StudentService {
private TeacherService teacherService;
public TeacherService getTeacherService() {
return teacherService;
}
public Student getByName(String name) {
// 省略实现...
return new Student();
}
}
上面的问题是
- StudentPrinter不应该关心StudentService依赖的TeacherService,
- StudentService不应该暴露TeacherService
- StudentPrinter不应该关心Student的结构,Student的结构只应该由StudentService关心。
修改:
class StudentPrinterRightCase {
StudentService studentService;
public void print(String studentName) {
studentService.print(studentName);
}
}
class StudentService {
private TeacherService teacherService;
public TeacherService getTeacherService() {
return teacherService;
}
public void print(String studentName) {
// 获取学生和授课老师信息
Student student = getByName(studentName);
List<Teacher> teacherList = teacherService.getByIds(student.getTeacherIds());
// 拼接和打印信息
StringBuilder msg = new StringBuilder("学生姓名:").append(student.getName());
msg.append("\n授课老师:\n");
for (Teacher teacher : teacherList) {
msg.append("老师姓名: ").append(teacher.getName());
}
System.out.println(msg);
}
public Student getByName(String name) {
// 省略实现...
return new Student();
}
}
1.7 组合复用原则(Composite Reuse Principle,CRP)
描述
又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)尽量使用对象组合,而不是继承来达到复用的目的。
程序复用有两种方式,其一就是继承,复用父类属性和方法。其二就是通过组合其它对象来创建一个新对象,在新对象中调用组合对象的方法来达到复用的目的。
组合的优点:
- 耦合度低,新对象不需要关心组合对象的实现。
- 灵活性强,组合对象的具体类型可以在运行时绑定。
组合的缺点:
- 新对象的创建需要依赖组合对象,因此相比于继承,组合方式要管理更多的对象。
示例
下面先看一个反例:
public abstract class BaseService {
protected boolean validateId(Long id) {
return id != null && id > 0;
}
}
public class TeachService extends BaseService {
public void update(Teacher teacher) {
if (validateId(teacher.getId())) {
// ... 其它逻辑
}
// ... 其它逻辑
}
}
public class StudentService extends BaseService {
public void update(Student student) {
if (validateId(student.getId())) {
// ... 其它逻辑
}
// ... 其它逻辑
}
}
上面反例中TeachService和StudentService采用继承来复用validateId()方法,说实话这种方式很别扭,就好像老虎有翅膀,飞机会游泳,在实际项目中也不少看到这种类似继承方式。优化方式就是改用组合,如下面代码,validateId方法放入Validator类中,TeachService和StudentService分别组合Validator来复用验证逻辑。
public class Validator {
public boolean validateId(Long id) {
return id != null && id > 0;
}
}
public class TeachService {
private Validator validator = new Validator();
public void update(CRP.Teacher teacher) {
if (validator.validateId(teacher.getId())) {
// ... 其它逻辑
}
// ... 其它逻辑
}
}
public class StudentService {
private Validator validator = new Validator();
public void update(CRP.Student student) {
if (validator.validateId(student.getId())) {
// ... 其它逻辑
}
// ... 其它逻辑
}
}
2. 设计模式
2.1 构建型
用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”
- 单例(Singleton)模式: 某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
- 原型(Prototype)模式: 将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
- 工厂方法(Factory Method)模式: 定义一个用于创建产品的接口,由子类决定生产什么产品。
- 抽象工厂(AbstractFactory)模式: 提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
- 建造者(Builder)模式: 将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
2.2 结构型
用于描述如何将类或对象按某种布局组成更大的结构
6. 代理(Proxy)模式: 为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
7. 适配器(Adapter)模式: 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
8. 桥接(Bridge)模式: 将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
9. 装饰(Decorator)模式: 动态的给对象增加一些职责,即增加其额外的功能。
10. 外观(Facade)模式: 为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
11. 享元(Flyweight)模式: 运用共享技术来有效地支持大量细粒度对象的复用。
12. 组合(Composite)模式: 将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
2.3 行为型
用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。
13. 模板方法(TemplateMethod)模式: 定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
14. 策略(Strategy)模式: 定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
15. 命令(Command)模式: 将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
16. 职责链(Chain of Responsibility)模式: 把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
17. 状态(State)模式: 允许一个对象在其内部状态发生改变时改变其行为能力。
18. 观察者(Observer)模式: 多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
19. 中介者(Mediator)模式: 定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
20. 迭代器(Iterator)模式: 提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
21. 访问者(Visitor)模式: 在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
22. 备忘录(Memento)模式: 在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
23. 解释器(Interpreter)模式: 提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
3. 设计工具
3.1 作图工具
3.1.1 流程图
流程图将步骤显示为各种框,并通过用箭头连接框来显示它们的顺序。此图解表示说明了给定问题的解决方案模型。用于分析、设计、记录或管理各个领域的步骤或程序。通常,流程图从上到下,从左到右排列。美国国家标准协会(ANSI) 在 1960 年代制定了流程图及其符号的标准如下:
ANSI/ISO 形状 | 名称 | 描述 |
---|---|---|
流线(箭头) | 显示进程的操作顺序。一条线来自一个符号并指向另一个符号。 | |
终端框(起止符号) | 指示程序或子流程的开始和结束。表示为椭圆形或圆角矩形。它们通常包含“开始”或“结束”一词,或表示流程开始或结束的其他短语。 | |
处理框(执行框) | 表示计算、操作。表示为一个矩形 | |
判断框 | 显示一个条件操作,它确定程序将采用两条路径中的哪一条。该操作通常是一个是/否问题或真/假测试。表示为菱形 | |
输入/输出框 | 表示输入和输出数据的过程,如输入数据或显示结果。表示为普通平行四边形 | |
注释(评论) | 指示有关程序中某个步骤的附加信息。表示为一个空心矩形,用虚线或实线将其连接到流程图中的相应符号。 | |
子流程 | 显示在别处定义的流程。表示为具有双重垂直边缘的矩形。 | |
页面连接器 | 成对的带标签的连接器取代了流程图页面上冗长或混乱的线条。由一个小圆圈表示,里面有一个字母或数字。 | |
页外连接符 | 当目标在另一页上时使用的带标签的连接器。表示为五边形。 |
下面是ANSI/ISO 标准包括基本形状以外的符号
形状 | 名称 | 描述 |
---|---|---|
数据文件或数据库 | 由象征磁盘驱动器的 圆柱体表示的数据。 | |
文档 | 单个文档表示为带有波浪底的 矩形。 | |
多个文档 | 多个文档表示为一堆具有波浪底的矩形。 | |
手动操作 | 由顶部最长平行边的 梯形表示,表示只能手动进行的操作或过程调整。 | |
手动输入 | 以四边形表示,顶部从左到右不规则地向上倾斜,就像键盘的侧视图。 | |
准备或初始化 | 由一个细长的六边形表示,最初用于设置开关或初始化等步骤。 |
3.1.2 泳道图
泳道流程图与传统流程图的不同之处在于,流程和决策通过将它们放置在泳道中来直观地分组。泳道可以水平或垂直排列,平行(垂直)线将图表分成几条泳道,一条泳道代表每个人、组或子流程。泳道用于业务流程建模表示法(BPMN) 和 统一建模语言 活动图建模方法。
3.1.3 UML图
使用的部署图显示物理结构节点所属的构件,使用构件图显示该构件包含的类,使用交互图显示该类的对象参与的交互,最终到达某个用例。可以说,系统的不同视图是用来在总体上给出系统一个整体的、一致的描述
3.1.3.1 行为图
图名称 | 用途 | 描述 | 符号 | 案例 |
---|---|---|---|---|
用例图 | 1. 用于反应系统角色、以及角色拥有的功能 2. 可以很容易看出系统具有的功能 | 用例视图描述了系统的参与者与系统进行交互的功能,是参与者所能观察和使用到的系统功能的模型图 | 椭圆:用例(功能)名称 人形:参与者角色 实线箭头:请求数据流向 虚线右箭头:包含用例 虚线左箭头:扩展用例 | |
状态图 | 1. 用于反应对象状态变更的条件,以及各个状态之间的关系。 2. 和活动图相比,状态图反映的是对象状态的变更条件。 | 状态图表示某个类所处的不同状态以及该类在这些状态中的转换过程 | 实心圆:开始标志 实心空圆:结束标志 长方块:状态名 箭头:状态转换 箭头标识符:状态转换条件 | |
活动图 | 1. 表示多个对象(模块、系统)之间在处理某个功能时的流程 2. 和序列图相比,活动图更能够适合对较高级别的过程建模。 3. 和状态图相比,活动图反映的是完成一个特定任务所需要的流程。 | 与泳道图一样 |
3.1.3.2 结构性图形
图名称 | 用途 | 描述 | 符号 | 案例 |
---|---|---|---|---|
类图 | 1. 用于反应类与类之间的关系 2. 领域驱动模型设计常用此图来建模,用以表达模型关系 | 关系: | ||
对象图 | 对象图描述系统在某一个特定时间点上的静态结构,是类图的实例和快照,即类图中的各个类在某一个时间点上的实例及其关系的静态写照 | |||
包图 | 1. 反映包之间的关系 2. 反映包中的类 | |||
组件图(构件图) | 1. 表达功能存放的地方 2. 使用构件图显示该构件包含的类 | |||
部署图 | 1. 从物理结构的节点来显示属于该节点的构件 | 部署图是用于表示该软件系统如何部署到硬件环境中,它显示出在系统中的不同构件在何处运行,即彼此的物理连接和分布,以及如何进行彼此的通信。 | 正方体:节点 连线标识符:链接方式 |
3.1.3.3 交互性图形
图名称 | 用途 | 描述 | 符号 | 案例 |
---|---|---|---|---|
时序图(序列图) | 1. 表达一个用例(功能)的执行流程 2. 反映模块之间的调用关系。 | 序列图将交互关系表示为一个二维图。其中,纵向是时间轴,时间沿纵轴向下延伸。横向代表了在协作中各独立对象的角色。 | 人形:角色 竖虚线:角色生命线,表示未激活。 竖双道线:对象激活被状态 实线箭头:表示消息,从一个对象的生命线到另一个对象生命线。 虚线箭头:表示消息返回 | |
协作图 | 1. 表示了协作中各种角色对象所处的位置 | 协作图是对在一次交互过程中有意义对象和对象间的链建模,显示了对象之间如何进行交互以执行特定用例或用例中特定部分的行为 |
3.1.4 数据库设计相关图
3.1.4.1 E-R模型图
ER模型,全称为实体联系模型、实体关系模型或实体联系模式图(ERM)(英语:Entity-relationship model)。可用于反映实体与实体之间的关系。
作图
实体:长方形表示,实体通常为名词。
实体属性:椭圆表示,通过实线连接实体。属性通常为名词。
实体间关系:菱形表示,关系通常为动词。用两条实线把两个实体连接起来,同时用(1:1)或(1:n)或(m:n)标识关系类型:一对一、一对多、多对多。
示例:
3.1.4.2 Data Vault模型图
Data Vault模型包含三种基本结构 :
- 中心表-Hub :唯一业务键的列表,唯一标识实际业务,业务主体集合。如下示例图蓝色部分。
- 链接表-Link:表示中心表之间的关系,通过链接表串联整个业务关联关系。如下示例图绿色部分。
- 卫星表- Satellite:历史的描述性数据,数据仓库中数据的真正载体,是 Hub 的详细描述内容。乳如下示例图黄色部分。
Hub想像成人体的骨架,那么Link就是连接骨架的韧带组织, 而satelite就是骨架上的血肉。Data Vault是对ER模型更进一步的规范化。
3.2 技术实现相关图
3.2.1 系统功能图
主要发现系统需要对用户提供的功能
可使用的工具:用例图、UI原型图
3.2.2 系统架构图
主要反映系统运作时所需要的技术环境和业务环境,包括上游系统、下游系统、数据库、中间件等。粒度比较大,通常最小单元为一个应用,而多个应用又组合为一个系统。而在比较细的架构图中,应用框中还包含模块框。
可使用的工具:流程图
3.2.3 应用架构图
主要是反映应用内部组成,及各个功能模块之间层次和调用关系。粒度比架构图小,通常按业务功能和代码功能划分模块,多个模块又组合为一个应用。
可使用的工具:流程图、构件图、包图
3.2.4 功能流程图
主要反映一个功能的执行所需要的关键步骤。粒度比模块图小,通常按执行的关键点划分步骤,有时候多个步骤组合为一个系统接口或者函数。
可使用的工具:流程图、泳道图(活动图)、时序图
3.2.5 数据结构图
主要反映数据对象之间的关系,比如一对一、一对多、多对多。粒度通常为一个类。
可使用的工具:类图、E-R图、DataVault图
3.2.6 部署图
整个系统所在机房、机器数目、配置、网络带宽等
可使用工具:部署图
3.3 业务理解相关图
3.3.1 业务架构图
用于反映一个业务闭环过程中各子业务之间的协作关系。一个业务必须能够闭环,否则这个业务就不完整。
可用工具:流程图、泳道图
3.3.2 业务流程图
主要用于反映完成一件业务所需要的步骤和每一个步骤的参与者。
可使用工具:泳道图、序列图
4. 系统结构设计思想
4.1 模块化设计思想
把完成同一业务功能的构件归为一个模块:
- 在系统架构上,一个应用可表示为一个模块。模块与模块之间代码通常是以文件夹隔离的。
- 在应用模块设计上,一个功能表示为一个模块。模块与模块之间代码通常是以文件隔离的。
- 在代码设计上:一个包表示一个模块,一个包下由若干类文件组成。
模块设计的难点在于如何降低两个关联性比较大的模块的耦合性,关于这个问题,DDD中通过划分边界上下文可以做到。
好的模块化设计能够体现出高内聚低耦合的特点。即对外部依赖可控,且内部关系完整,联系紧密。模块化的设计原则通常包括:
- 为降低软件的开发和维护成本,每个模块必须可以被独立地进行设计和修改;
- 每个模块的结构都应该足够简单,使它更容易理解;
- 可以在不知道其他模块的实现细节和不影响其他模块行为的情况下,修改某个模块的实现
应用:架构设计、应用功能模块设计、代码模块设计
4.2 面向对象设计思想(封装、继承、多态)
对象和类
对象(Object) 就是将一组数据(属性值)和与这组数据有关的操作(方法)组装在一起所形成的一个完整的实体。对象中的数据被称为对象的属性,一个对象的所有属性的值被称为这个对象的状态。对象的操作则被称为对象的行为,也称为对象的外部接口。
类(Class) 可以被定义成具有某些相同的属性和方法的全体对象构成的集合。类是对一组具有相同属性和方法的对象的一种抽象描述,其本质在于这些对象所具有的相同属性和方法。因此,类也可以被定义成是一组属性和方法构成的一个整体。
对象模型构成
对象模型包括抽象、封装、模块化、层次结构、类型、并发和持久七个基本构成要素,其中抽象、封装、模块化、层次结构为主要要素。
抽象 是对客观事物所具有的基本特征的概念性描述,通常仅关注客观事物的重要细节,而忽略事物的非本质特征的细节或枝节。
封装(Encapsulation) 是指:在构造对象的结构和行为的过程中,需要明确地定义对象的外部可见部分和不可见的部分,这可以使对象的接口部分和实现部分相分离,从而降低对象与其客户之间的耦合。抽象和封装是两个互补的概念,抽象通常描述的是对象的外部可见行为,而封装关注的则是这些外部行为的实现。
模块化: 在面向对象方法中任务通常演变成了对类和对象通过打包(Package)的方式来进行分组,此时每一个包都可以被定义成若干个类(或对象)构成的集合,而且包里面还可以包含其他的包。
层次结构: 描述类与类的继承关系和对象与对象的组合关系。继承将多个不同的类中相同属性和方法迁移到它们共同的基类(或超类)中,从而减少这些属性和方法的冗余。
类型: 即数据类型,类型可以分为静态类型和动态类型两种。静态类型指变量和表达式的类型在编译时就能确定具体类型。而动态类型(迟后绑定)指变量和表达式的类型在程序运行时刻才能确定具体类型
多态(Polymorphism)也是动态类型和继承互相作用时所表现出来的一种情形。即一个名字(或变量)可以代表许多个不同类型的对象,这些类具有某个共同的超类。多态区分了面向对象编程和传统的抽象数据类型编程。
面向对象方法
面向对象方法的哲学思想是:客观世界是由事物与事物之间的联系构成,它将客观世界描述为对象与对象的关系。它将对象作为分析问题和解决问题的基本元素。面向对象方法分为:面向对象分析、面向对象设计、面向对象编程。
面向对象分析
面向对象分析是指在软件开发过程中,应用建模语言对获取的业务模型进行细化,建立目标系统的需求模型,即系统的功能模型和面向问题域的结构模型,并以此作为面向对象设计的基础。面向对象分析中主要使用UML的用例模型描述系统的功能结构模型,使用类图描述系统的概念结构模型。
面向对象分析的实质,就是从问题域中抽象出对实现系统目标有意义的对象。这些对象可能包括问题域中的实体对象、通用操作对象、业务逻辑对象、偶然对象等,因此,面向对象分析的过程实质就是一系列的抽象过程。
面向对象设计
面向对象设计是一种软件设计方法,其基本内容包括一个面向对象分解的过程,也包括一个用于展现目标系统结构的逻辑模型、物理模型、静态模型和动态模型的表示法。其本质是:以在系统分析阶段获得的需求模型和概念模型为基础,进一步修改和完善这些模型,设计目标系统的结构模型和行为模型,为进一步实现目标系统奠定基础。
4.3 领域驱动设计思想
领域的概念大到一个行业,小到一个功能模块。
领域驱动设计不是增加软件开发复杂度的,也不是降低软件复杂度的,而是实现复杂软件的指导思想。对于简单的软件,可以领域驱动为参考进行设计,而无需全部遵从其思想指南。
领域驱动设计思想并不独立于其它设计思想,它是集所有思想于一身,指导着这些思想在相应的过程中发挥其作用。它就像一个指挥官,哪个地方需要什么思想什么方法,它就安排它们去那里干活。
概念 | 描述 | 示例 |
---|---|---|
Entity | 实体,具有连续性状态的对象 | |
Value Object | 值对象 | |
Service | 服务对象,应用层服务、领域层服务 | |
Repository | 仓库:数据存储和提取 | |
Aggregate | 对数据具有一致性关联关系Entity和Value Object进行合并 | |
Factory | 创建复杂对象 | |
Specification(规格) | 1. 测试任何对象以检验它们是否满足指定的标准 | 1. 验证对象 2. 用于筛选查询对象 |
Bounded Context | 1. 一个模型只在一个上下文中使用。 2. 上下文可以是代码的一个特定部分,也可以是某个特定团队的工作 : | |
分层设计 | 基本原则是层中的任何元素都仅依赖于本层的其他元素或其下层的元素 1. 应用层 2. 领域层 3. 基础设施层 4. 作业层 5. 能力层 6. 潜能层 7. 策略层 8. 决策支持层 9. 防腐层: 1. Facade:隐藏系统细节 2. Adapter:对请求做转换 3. Converter:做对象转换,与Adapter配合 | 4层结构说明 六边形结构,6层 菱形结构 防腐层 |
4.4 分层设计思想
是对所有业务执行流程进行抽象,把具有同样职责(角色)的模块划分为一层,层中的任何元素都仅依赖于本层的其他元素或其下层的元素。
优点:业务上清晰的反映了执行流程,维护上层层相扣,职责清晰。
应用:架构设计、应用功能模块设计、代码模块设计、代码包设计
4.5 包分类设计思维
包层次设计是分层设计的子领域,包把同性质的文件放在一块,同性质如何区分,往往看划分的维度,比如按文件扩展名划分。进一步同一个包内的文件也有其它性质的区别,比如按处理流程上的角色划分、按功能划分。这样层层往下拆,直到拆到适合的维度或者不能再拆为止。
如果把模块比作房间,那包则是房间中的储物柜、摆放台。子包就是储物柜里面又分为格子、抽屉。为了很好的知道包里面存放的物品,往往会为包取一个贴切的名字,比如梳妆台往往摆放化妆用品,保险柜里存放贵重物品,衣柜里放的是服装,但衣柜的每个格子又根据不同用途摆放,比如摆男士和女士服装往往是分开摆放的,衣服和裤子也是分开摆放的,等等。
分类方式:
- 按性质分,同性质的放一起,优点:能快速找到一个类的位置。如domain、dao、service、controller、util
- 按功能分,一起完成某一功能的组件放在一起,优点:代码内聚比较高,对外耦合低。
- 按公众习惯分,优点:习惯的就是好的,因为顺手。比如对包外公开的接口往往放在包的第一层级,而不应该放在子包,甚至层级更深的包。
5. 代码设计
5.1 代码规范和风格
- Alibaba Java编码规范: https://github.com/alibaba/p3c
- Google编码风格 https://google.github.io/styleguide/
5.2 代码优化
- 逻辑判断中出现的单词,最好与代码分支下描述的业务一致,比如:
分支处理success的内容,if判断写if success 就比写 if not failure 好 - if嵌套不要太深,比如
if a:
if b:
xxx b
else:
xxx not b
else:
xxxx not a
应该改为:
if not a:
xxxx not a
return
if b:
xxx b
return
xxxx not b
- for与if嵌套也不能太深,解决方式和上面一样,return改为continue或者break就是
- 多层for循环嵌套,如果里面的for代码比较长,这个for循环最好用函数封装
- 函数代码不要太长。一般来说单一职责的函数,代码都不会很长,如果代码很长,必定是职责划分太宽泛了,需要对函数中的多个职责进行拆分再组合成各个职责的函数调用
- 在命名类和操作时要描述它们的效果和目的,而不要表露它们是通过何种方式达到目的的
- 尽力把最复杂的计算提取到STANDALONE CLASS(独立的类)
- 尽量创建无副作用的函数,即不要修改入参的状态
- 约定大于俗成,使用大家都知道的概念和方式。非必要时,不要引入或创造新的概念。
- 减少类、接口、方法代码量
- 减少流程中间环节
- 减少函数调用的深度
- 避免过度设计,过度设计会造成需要理解的内容增多,从而难以理解