我们认为提升前端代码质量的关键不仅在于规范、工具的应用,更重要的是在于各位“务实工程师”的思考,以及对设计原则的合理应用,对优秀软件工程方法的实践。下面,我们从业务需求开发的四个阶段出发,阐述对提升代码质量方法的思考:
- 需求分析和设计阶段:基于对业务的正确理解,合理地将需求拆解成不同的部分,为后续的所有环节提供最基础的输入。
- 编码阶段:统一目录、代码的风格;使代码组织跟业务保持一致,以最准确的业务语言命名;视图逻辑之间分层,使代码更容易复用,编码的关注点能够分离。
- CR 阶段:以设计阶段的产物为指导,检查需求分析、业务理解的正确性,检查代码组织、设计、实现的合理性。
- 线上监控阶段:以设计阶段的产物为指导,建设有效监控。
接下来我们将从一个 Todo List 的示例需求开始,完整的介绍面向对象分析、设计方法在前端业务需求中的应用,最后会讨论技术设计给整个开发流程带来的正向影响/价值。
分析(analysis)
对问题、需求的调查研究(不是解决方案)。例如,如果需要一个 Todo List,应该如何使用它?它应该具有哪些功能?
设计(design)
满足需求的解决方案(不是实现)。设计可以用代码实现,而实现(代码)则表达了完整的设计
根据ui示例需求
大部分的情况下根据ui能够明白交互上的大致逻辑
定义用例
看到这样一个需求,我们下意识的动作大概率是先拆解有哪些页面、哪些组件。然而,这个做法是有问题的:页面、UI 组件只是需求的视图产物。不能指导我们构建业务模型,要深刻的理解业务,应该从用户目标开始。 用例(use case)
一组成功和失败场景集合,用来描述参与者如何使用系统来实现其(参与者)目标。
参与者(actor)
某些具有行为的事物,可以是人(由角色标识)、计算机系统或组织。
场景(scenario)
场景是用例的一条执行路径,是参与者和系统之间的一系列交互。场景分为三类:
- 主成功场景(理想场景、最核心路径)
- 扩展场景
- 失败场景
根据上述的要素构建完整的执行用例。
拆解框架:目标-子目标模型
从需求文档出发,提炼**“目标-子目标模型”,这个模型具有三个层次**的目标:
- **概要目标:**偏概括性的,聚集用户目标,作为子应用、项目子模块划分的依据。
- 用户目标:描述“谁”要“做某事”,我们编写的代码,就是要支持用户目标的达成,视图、逻辑很自然的就以用户目标为边界进行组织。
- **子功能目标:表示用“哪些东西”**来完成目标的,可以理解为我们日常开发里面的组件。
下面以「绩效系统」为例,我们看看**“目标-子目标”**这个思想是如何分解系统的,相信通过这个例子,大家很容易就能够理解。
定义实体模型
这部分实操起来比较简单,直接跟后端对齐即可。需要注意的是,这个跟对齐接口定义不太一样,重点是要搞清楚业务实体,以便我们能在编码的过程中以最准确的业务语言来编码。
具体到业务就是对应的接口实体的声明
定义人机交互
用例描述了参与者具有一个目标**,希望与我们创建的系统进行交互。在交互中,参与者对系统发起系统事件(system event),通常需要某些系统操作(system operation)**对这些事件加以处理。前端编写的业务代码,绝大部分都是服务于交互的。因此,交互的分析是编码前的一个重要环节,完成交互分析,就能够直接指导编码了。
定义逻辑层
以用例为边界内聚事件处理器、状态
定义:对外暴露 UI 需要的状态以及事件处理函数的对象。
- 同一用例下的 UI 状态以及事件处理器,内聚在同一个 Logic 对象里面。因为它们都是都是服务于同一个目标达成的,不管在写代码或者 Review 其他人代码的时候,都很容易理解。
- 如果一个用例下的交互太多,怎么办?这个时候需要拆分逻辑,就像“目标”拆解为多个“子目标”一样,接下来我们会讨论如何拆解的话题。
type CreateTodoLogic = () => {
name: string;
content: string;
handleDidMount: () => void; // 处理:访问创建待办页
handleEditTodoName: (name: string) => void; // 处理:填写待办名称
handleEditTodoContent: (content: string) => void; // 处理:填写待办内容
handleSubmitTodo: () => void; // 处理:提交待办
};
export const useCreateTodoLogic: CreateTodoLogic = () => {
const [name, setName] = useState('');
const [content, setContent] = useState('');
return {
name,
content,
handleDidMount: useCallback(() => {}, []),
handleEditTodoContent: useCallback((content) => {
setContent(content);
}, []),
handleEditTodoName: useCallback((name) => {
setName(name);
}, []),
handleSubmitTodo: useCallback(() => {
// submit todo
}, []),
};
};
我们看下填写待办名称的场景有两个步骤:填写名称、点击下一步,接下来我们就把处理这两个交互的逻辑提取出来:
enum Step {
one,
two
}
export const useCreateTodoLogic = () => {
const [name, setName] = useState('');
const [content, setContent] = useState('');
const [step, setStep] = useState(Step.one);
return {
step,
name,
content,
handleDidMount: useCallback(() => {}, []),
handleEditTodoContent: useCallback((content) => {
setContent(content);
}, []),
handleEditTodoName: useCallback((name) => {
setName(name);
}, []),
handleSubmitTodo: useCallback(() => {
// submit todo
}, []),
// 通过 useEditTodoNameLogic 的 handleNextStepClick 触发
handleValidateNamePass: useCallback((name: string) => {
setName(name);
setStep(Step.two);
}, [])
};
};
export const useEditTodoNameLogic = () => {
const [name, setName] = useState('');
return {
name,
handleEditTodoName: useCallback((name) => {
setName(name);
}, []),
handleNextStepClick: useCallback((onValidatePass) => {
if (!name) {
console.error('请填写待办名称');
return;
}
onValidatePass(name)
}, []),
};
}
// 视图层
export const View: React.FC = () => {
const { handleValidateNamePass } = useCreateTodoLogic();
const { handleNextStepClick } = useEditTodoNameLogic();
return (
<div>
请填写待办名称:<input />
<button onClick={() => handleNextStepClick(handleValidateNamePass)}>下一步</button>
</div>
);
};
总结
至此,我们已经介绍了完整的技术设计方法,现在来回顾一下整个流程:
- 示例需求:通过ui辅助和需求分析明确需求场景和操作逻辑
- 用例:在框架下,使用用例分析方法,完善参与者完成目标的细节,包括主成功场景、扩展场景、失败场景。
- 实体:以用例分析的结果(用例场景),跟后端对齐业务实体的概念,共同维护一个术语表,以最准确的业务语言来组织目录、编写代码。
- 人机交互:前端系统就是处理人机交互的系统,基于用例分析的结果,可以很容易提取、聚合前端系统中的人机交互。分析至此,我们在写代码的思路基本上就非常清晰了。
- 逻辑层: 以用例为边界聚集逻辑层的代码,跟上面人机交互的分析保持一致,在实现逻辑层的时候已经就不需要太多的思考就可以完成编码。