记一次对 CSS FlexBox 的误用

问题的介绍

最近开发某反馈系统的前端,偶然发现顶部导航栏的高度在 MacOS 或 iOS 下的 Safari 中计算异常:

首页的布局以及相关的样式大体如下:

1
2
3
4
<div class="rootContainer">
<nav class="navBar"></nav>
<div class="mainContainer"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
.rootContainer {
display: flex;
flex-direction: column;

.navBar {
flex: 0;
padding: 8px 0;
}

.mainContainer {
flex: 1 1 auto;
}
}

这里 rootContainer 使用了 FlexBox 布局,使得顶部导航栏在固定高度的情况下,下面的主体容器能够自动将高度扩展直到占满视口的剩余高度。

然而,在 Safari 中,顶部的导航栏的实际高度为 16px(上下 padding 之和),而不是 Chrome / Firefox 中正常的 40px(padding 的 16px 以及其中的内容高度 24px)。

问题的解决

仔细查找资料发现,问题出在导航栏样式中的 flex: 0flex 属性在仅给定单个值时,会将 flex-grow 属性设置为给定的值,并将 flex-shrinkflex-basis 分别设置为默认值 10,因此 flex: 0 等价于 flex: 0 1 0%。然而,正如我们在上文看到的,这种赋值方式在 Safari 中会出现让人意想不到的情况。

其实从根本上来说, flex: 0 的使用是自相矛盾的,因为 flex-grow: 0 表明此元素不参与伸长,而flex-shrink: 1却又表明此元素参与缩小,但 flex-basis: 0 又表明此元素在不参与伸长时的高度(或宽度)应该为 0。这样我们也就不难理解为什么 flex: 0 在 Chrome 和 Safari 中存在如此之大的差别了。

综上所述,问题的根源在于 flex: 0 的使用不合理,应该修改为 flex: none

下面列举一些 flexbox 常用的赋值缩写:

1
2
3
4
5
6
/* 等价于 flex: 0 0 auto */
flex: none;
/* 等价于 flex: 1 1 0% */
flex: 1;
/* 等价于 flex: 2 2 0% */
flex: 2 2;

探究 DDD 在前端开发中的应用(二):什么是 DDD

DDD 简介

DDD 是英文 Domain-Driven Design 的缩写(不是 DDL Driven Development),中文译名为领域驱动设计,诞生于本世纪初。它为人们提供了战略和战术上的建模工具,帮助人们准确把握业务需求,产生精准的建模设计,进而高效率地设计、维护软件。

对于在软件开发领域中缺乏经验的许多大学生来说,DDD 更是提供了让他们看待代码的另一双眼睛,这双眼睛不仅能看到他们写出的每一行代码,还能看到在这些代码之上形成的结构,启发他们思考代码与代码之间的关系,进而让他们对架构形成一定的认识。如果说《C Primer Plus》、《C++ Primer》这些书籍能教你成为一名士兵,那么 DDD 这样的建模工具则能教你成为一名战略指挥官。

受篇幅限制,我们接下来只能对 DDD 的基础概念进行简要的介绍,如果你有时间,请阅读关于 DDD 的经典书籍《实现领域驱动设计》。

通用语言与限界上下文

DDD 作为一个建模工具箱,当然是为了建模而生的。简单来说,DDD 是使用通用语言(Ubiquitous Language)区分出的限界上下文(Bounded Context)来对需求的子域(Sub-domain)进行建模的工具箱。

通用语言是一些单词或者短语的特殊含义,或者开发团队之间形成的各种约定。通用语言是既描述了业务需求,又在代码设计中得到体现。限界上下文则是一个语义和语境上的边界,其中的每个代表软件模型的组件都有着特定的含义,并且处理特定的事务。这里界定所谓“特定”的概念就是通用语言。

在大型项目中,一个限界上下文往往对应一个由程序员、领域专家和产品经理等角色构成的开发团队。团队成员之间会在工作交流中使用这种通用语言,并且程序员书写出的软件模型的源代码就是通用语言的书面表达方式。因此,限界上下文和通用语言既是软件结构层面的设计,还是团队结构的设计,这一点和康威定律(Convey’s Law)不谋而合。


从漫画中可以看到,负责两个限界上下文的人员的办公室里传出来的语言是明显不同的

要进一步理解通用语言和限界上下文,我们不妨从它们的反面出发:在整个软件中随意使用语言,不划分任何模块。这样做会有什么后果呢?随意使用语言会提升开发团队的沟通成本,还可能导致误解的出现,甚至会导致产出的代码中产生严重的 Bug,或者偏离产品经理的预期。

例如在某个计分系统中,计分板代码中使用了 grade 这个词语来表示一个具体的分数,使用了 score()来表示教师的计分动作。然而,在计分板数据库代码中,使用了 score 来表示具体的分数。这样的结果就是,接手这些代码的程序员需要提高警惕以免混淆两边的 score,造成了注意力的浪费,并且在大规模的代码库中,长年累月的修改会使得两个 score 杂糅在代码中,留下了更大的隐患。

不严格划分模块,意味着负责不同功能的代码会在多次修改之后变得亲密无间,难分你我。这种行为或许在软件开发的初期有着较高的效率,但当软件开发进入维护阶段时,则会给维护软件的程序员带来噩梦,因为不同行为的代码混合在同一个文件夹甚至是同一个文件,导致程序员在下手修改某个行为前都需要仔细辨析相关的代码,甚至在修改后都在提心吊胆,生怕修改的代码会影响到其他功能的正常运行。


对付这样的代码就如同抽积木!

DDD 建模过程

从上文可以总结出,通用语言和限界上下文是相辅相成的关系。在开发的早期,面对给定的需求,开发团队会在头脑风暴时识别出一些存在差异的通用语言,然后将这些语言对应的组件划分给一个限界上下文。在开发过程中,甚至到了软件的维护阶段,限界上下文的发展又会反哺通用语言的进一步精细化、严谨化。

让我们先了解一下 DDD 建模的第一个步骤:开发团队首先进行讨论,发掘业务需求的全貌。然后开发团队会根据通用语言的不同划分出不同的子域,在不同子域中继续发展通用语言,让它们变得更精确严谨,使这个子域成为限界上下文。

开发团队中必须含有领域专家(Domain Expert)和软件开发人员。与软件开发人员关注于技术不同,领域专家更加关注实际的业务问题,他们会更多的从宏观角度分析业务需求,调动开发团队进行融洽的合作。他们非常了解如何进行领域设计来让公司的业务更上一层楼。

领域专家的存在使得软件开发人员不会在发展通用语言的过程中使其偏离实际的业务,导致整个项目充满了程序员间才懂得的语言,产品经理和程序员间出现沟通障碍等问题。因为领域专家有着和软件开发人员不一样的心智模型。DDD 中尤其强调拥抱业务需求本身,而尽量克制程序员“以技术为中心”的冲动。因此,软件开发人员需要和领域专家亲密合作,共同发展通用语言。


开发人员和领域专家所思考的事务往往是不同层面的

DDD 建模过程实例

让我们用一个实际一些的例子来说明这个过程,这个例子将在之后介绍 DDD 继续使用:假如有一个叫做 Vatrix 的课程平台被某公司正式立项,这个课程平台以课程为基础,提供程序设计作业的布置和在线评测功能。现在,假设我们是开发团队的一份子,让我们走进 Vatrix 开发团队的会议室中,参与 Vatrix 的头脑风暴!Vatrix 开发团队的需求分析会议正式开始,有请领域专家、程序员、产品经理等人进场。小明端庄地掏出粉色笔记本,向大家展示了它给出的领域设计草稿。团队成员们开始围绕业务需求进行探讨。

有人提出:课程平台应该来一个讨论区,让老师和同学之间可以对题目进行讨论。队员们普遍表示认同,于是平台的领域结构图被扩展了。其中的命名是领域专家和软件开发人员们共同商定的。

不一会,又有人跳起来说:咱们是不是忘记了一件重要的东西?我们应该提供一个题库功能供老师们创建和套用到课程中!扩展之后的设计图大家都很满意,于是 DDD 建模进入下一个阶段。

此时领域专家上台,将设计图按照通用语言的差异划分出了以下的子域。然后,开发团队将分为四组,各负责一个子域。工作开始后,小组内部会经过多次开发讨论,持续地为了实现业务需求发展子域的通用语言,限界上下文逐渐形成。发展通用语言的方式包括但不局限于:What-Why-How、Use Case、User Story 等,由于篇幅有限这里不作展开。

上下文映射

当限界上下文被发展出来之后,DDD 项目的核心域需要和其他限界上下文进行集成,这就是 DDD 的上下文映射(Context Mapping)。如果没有集成,那么我们的团队最后开发出来的成品只是四个孤立的组件而已,只有经过集成才能将它们连接到一起,成为一个能够正常工作的软件。例如,Course 限界上下文需要集成 Problem Database 限界上下文来实现业务需求中的出题功能。

上下文映射不仅表示了两个限界上下文的集成关系,还表示了两个团队之间的动态关系。由于不同的限界上下文的通用语言差异较大,因此团队之间处理上下文映射时尤其应当注意克服通用语言差异的鸿沟带来的潜在问题。两个团队之间存在的动态关系可能表现为以下的其中一种:合作关系(Partnership)、共享内核(Shared Kernel)、跟随者(Conformist)、开放主机服务(Open Host Service)。由于篇幅原因只列举了最常见的几种。

上下文映射的关系

合作关系是指两个限界上下文的团队之间具有一些重合度较高的目标以及实现思路。这样的团队会对开发进程的同步有较高的要求,因此经常会需要开会沟通,协同进度。

共享内核是指两个团队之间通过共享一个小规模但通用的模型来实现限界上下文的集成。例如负责资源监控限界上下文的团队要求负责邮件发送限界上下文的团队共享一个发布邮件的模型,来让前者能够在资源监控出现问题时发布邮件通知用户。

跟随者模式是指,当一个团队需要集成一个已经存在且规模庞大的限界上下文时(通常发生在下游团队和上游团队之间),下游团队无力翻译上游上下文的通用语言,而只能直接套用上游上下文的组件,因此下游团队成为了上游团队的追随者。例如我们在开发微信小程序时必须顺应微信团队的各种约定和要求。

开放主机服务是指团队定义一套协议或者接口,让自己的限界上下文可以作为一个服务让其他团队集成。这样的团队往往会提供一套详细的文档方便其他团队的开发。例如 Twitter 提供的公开 RESTful API 可以让每个开发者向自己的软件中集成 Twitter 的某些功能。

聚合

现在,让我们进入限界上下文内部,看看 DDD 为限界上下文内部的设计提供了什么样的工具。首先值得介绍的就是聚合(Aggregate)的概念。它指的是限界上下文中一个个的代码集合体,由实体或值对象构成,其中有一个处于根节点的实体被称为聚合根(Aggregate Root)。聚合根控制着所有聚集在其中的其他元素,并且它的名称就是整个聚合的名称。一个限界上下文可以存在多个聚合,下面只列出了一个。

从聚合开始,DDD 将触角伸向微观层面,指导我们如何结合系统分析,对每一个类进行设计。聚合中有一个非常重要的概念:每个聚合是事务一致性的边界。 所谓事务一致性,指的是在一个聚合中,所有被提交到数据库的事务执行后,整个聚合的所有组成部分必须严格符合业务规则。事务一致性的确定需要开发团队紧密结合业务需求来确定聚合何时处于有效状态,以防止破坏业务的操作发生。

看上去,这和数据库概念中的完整性约束、谓词约束等有相似之处,后者对数据表中的实体取值按照维护者的意愿进行限制。事实上,数据库的这些约束手段可以作为实现聚合的事务一致性的手段之一。不过,需要注意聚合的事务一致性更加强调:只能在一次事务中修改一个聚合实例并提交。

聚合的事务一致性约束开发团队在设计聚合时必须紧密结合业务需求,按照让聚合维持一致性和提交成功的方式来进行。这呼应了开头的那句话:“设计并不仅仅是感观,设计也是产品的工作方式。”此外应该注意,聚合的事务一致性必须由业务规则驱动,而不应该由技术驱动,否则我们将背离 DDD 的初衷。
这里列举一些设计聚合时值得参考的几条原则:

  1. 在聚合边界内保护业务规则的不变性。
  2. 聚合应该设计得小巧。
  3. 只能通过标识符引用其他聚合,而不是对象引用之类的。
  4. 使用最终一致性更新其他聚合。

领域事件

四条原则中的最后一条“使用最终一致性更新其他聚合”应该如何实现呢?例如,假设一位学生用户在 Vatrix 中加入了某个课程,那么 Account 限界上下文中的 Account 聚合应该提交对「已加入课程」的更新,但这只维护了这个聚合内部的事务一致性,我们还需要维护课程限界上下文中的成员列表聚合,向其中加入这位学生的学号才算是完成了业务需求的「学生可以加入课程」这一项功能。

这种对聚合之间最终一致性的需求催生了领域事件(Domain Event)的概念。它是一个聚合发布的、由利益相关的限界上下文订阅的事件,通常经由消息机制以发布/订阅的方式实现。下面我们结合上面的例子看一个图例。

如上图所示,CourseTook 聚合通过发布 StudentTookCourseEvent 这样一个领域事件到消息机制中,然后对此感兴趣的 MemberList 聚合将接受这个事件,并作出反应。它会从事件中读取到对应的学生学号,并将它加入到课程的成员列表中,最终一致性由此实现。

需要强调的是,领域事件在维护聚合一致性中如此重要,以至于它的任何接受者都必须贯彻落实相应的动作,不得因为任何原因放弃执行。同时领域事件的发布和传递必须高度可靠。

保障领域事件正确运行

无论是使用 MQ 还是其他方式实现承载领域事件的消息机制,必须强调的议题是消息机制的可靠性。当服务器机房的天花板漏水,或者是空调出现故障导致服务器,又或者是中大的施工队再次挖断电缆,导致服务器不得不中断运作时,我们当然不希望那些仍在传播中的领域事件消失,破坏系统的最终一致性。

因此,应该对各种领域事件进行有效的持久化处理,这同时也能作为一种聚合实例发生变更的历史记录,从而我们不仅能在意外出现时重建聚合,还能拥有一定的溯源能力,提高系统的可靠程度。

聚合的实现例子

下面让我们通过一段代码来实现上面所说的 CourseTook 聚合,假设它的构成如下图所示,其中 GPA 和 Description 是值对象,Assignments 是实体。我们的第一步是实现聚合的根节点 CourseTook 对应的类。然后再实现聚合根下的各种实体。

具体的实现请看本人写的一段 TypeScript 代码。请注意这样的一个类是如何拥抱业务需求,封装业务逻辑的。其中,聚合的实体类一般都会继承一个公共的基类 Entity,这个基类一般会负责维护唯一的实体 ID、比较逻辑,以及为子类提供发布领域事件的 API 等。

像这样设计聚合之后,我们的路由处理的逻辑将被大大简化,请看下面的 NestJS 的一段代码。

与那些直接将业务逻辑写在路由处理方法中的传统做法不同,我们将业务逻辑全部地保留到了实体本身中,而路由处理方法仅负责组装响应。这种做法与 DDD 中以业务需求为本的思想相统一,实体类中既封装数据,也含有业务需求中对数据做出的约束。这也使得业务逻辑与访问方式脱钩,也就是说,不仅是像上面的 RESTful API 可以复用实体创建逻辑,像 GraphQL 的 Resolver 也可以直接复用。

DDD 的整体架构

聚合的实现例子反映出了 DDD 不同的代码架构,请看下面的对比图。

在 DDD 中,Domain 层位于最核心的位置,并且有着一个严格的依赖规则:外层可以依赖内层,但内层不能依赖外层。这就是为什么 Domain 层需要通过领域事件去发布事件的原因。


来自https://www.jdon.com/ddd.html

小结

对 DDD 的基础概念介绍总算是告一段落了。遗憾的是由于篇幅原因,DDD 中还有许多经典的概念并没有纳入讨论范围。如果你有兴趣,推荐你阅读 Vaughn Vernon 写的《领域驱动设计精粹》和《实现领域驱动设计》。

本节内容的参考:

链接

探究 DDD 在前端开发中的应用(三):探究 DDD 在前端开发中的应用

前端之于 DDD

无论是 SSR 还是 SPA,结合上文讨论的 DDD 基础概念我们可以发现,从整个 DDD 软件的结构上看,前端主要处于 Presentation 层。也就是说,前端的主要任务是显示业务数据,并且支持用户交互动作,绝大多数的业务逻辑其实存在于服务端的 Domain 层中。当然前端有时候会存在一些业务逻辑,但它们大多都是服务于或者同构于服务端的,例如某些验证逻辑。不过总的来说,DDD 的重点,Domain 层位于服务端,而不是前端。

不过,这并不意味着 DDD 在前端就没有任何应用的空间。在 SPA 项目的职责不断变多、规模不断壮大,复杂程度不断提高的今天,DDD 奉行的一些理念以及倡导的一些范式依然能够在前端中派上用场。事实上,已经有一些先驱者为我们探索了 DDD 在 SPA 项目中的应用。

他们认为在 SPA 中,我们依然有理由去构筑一个 Domain 层,其中含有与服务端通信的数据模型,以及某些前端必要的业务逻辑,还有一些数据的验证逻辑等。我们可以在 SPA 中实现一个“残血版”的 DDD。下面,我们将探讨 Manfred Steyer 的利用 Angular 和 Nx Workspace 实现的 DDD,他为此特地编著了《Enterprise Angular》,电子版可以在网上免费下载。

Angular & Nx 简介

本节讨论的 Angular 是指由 Google 推出的 MVVM 框架 Angular 2,它使用 TypeScript 编写。虽然在全球范围内的人气不及 React 和 Vue 等竞品,甚至在国内都近乎无人问津(不过,中大教务系统却是使用它开发的)。但 Angular 依然凭借它的特色获得了大批程序员的好评。

它主要有两大特色:一是引入了一套依赖注入(Dependency Injection)系统,让代码块的复用变得更为简单;二是将组件分为模板(Template)、组件类(Component)、样式三个部分,分别对应.html、.ts、.css(或.scss 等)文件。

Nx 是由 Angular 开发组和前 Google 职员组成的组织 Nrwl 共同开发的,旨在为前端程序员提供一个打造 Monorepo 的工具箱。

Nx Workspace

Nx Workspace 为 Angular 开发者提供了一个开箱即用的 Monorepo 构建工具,它使用 Library 的概念来分割代码组件,强调不同的 app 可以复用组件以提升开发效率,减少重复代码。例如,对于一个预定机票的网页 App 项目,要求同时提供客户端和管理端,那么它的软件架构可能是这样的:


客户端 App 和管理端 App 分别利用了一个共享的 Library 和一个独享的 Library

Nx Workspace + DDD

Manfred Steyer 提出,我们可以使用 Nx Workspace 中的 Library 来组成 DDD 中的限界上下文。常用的几种 Library 的类型:

  • domain:含有业务数据模型,以及一些业务逻辑,数据验证等。
  • feature:负责用例的实现的 Smart Component。
  • ui:提供与实际用例无关的、可复用的 Dumb Component。
  • util:提供以下状态无关的服务、管道或者一些纯函数。
  • shell:这个限界上下文的入口 Library,负责路由等。

Manfred 同时强调,这些 Library 的逻辑层级存在区别,因而要严格控制它们之间的依赖关系。请看下面的例子,图中的箭头表示依赖关系。从 UI 相关代码的逻辑分层看,位于最高层的是 feature library,而像 util library 这样的则处于低层级。feature library 中的 Smart Component 是用户直接看到的组件,而 data-access 和 util library 中的代码则是被 feature library 依赖,但前者不能反过来依赖后者。这种依赖规则可以通过 Nx Workspace 提供的 library 依赖 lint 规则来实现,具体因篇幅有限不做阐释。


图片来自 Manfred 的《Enterprise Angular》

Domain Library

在数据层面上,feature library 会依赖 domain library 中供给的数据模型,以及验证逻辑等,而 domain library 则是整个前端软件中直接与服务端相连接的层级。这样的分层关系符合 DDD 中的要求,并且也契合了 DDD 中的以业务需求为中心的理念。下面,我们来仔细研究一下 domain library。

Domain library 中包含三个层面的代码,分别是 Application、Domain 和 Infrastructure。其中的 Domain 就是 DDD 中的 Domain 层,内含 Entity,存有数据模型,以及前端需要的部分业务逻辑。


图片来自 Manfred 的《Enterprise Angular》

下图是 Manfred 给出的 Domain 层的聚合例子,可以看到它和前面介绍 DDD 时给出的聚合例子几乎相同。这里再强调一下,DDD 抵触那些只有数据模型的实体类,并将它们成为「贫血领域模型」。有了 Domain 层的这些实体类之后,不要忘记我们正在编写前端代码,所以接下来我们要思考的是如何让我们的组件与服务端沟通,接收并显示数据。让我们来看看 Application 层中的 Facade。


代码来自 Manfred 的《Enterprise Angular》

严格来说,Facade 并不属于 DDD 的内容,但却是一个在 Angular 圈子里比较流行的实践,相当契合 DDD,有着以下优势:

  • 封装复杂的数据逻辑
  • 负责状态管理
  • 简化 API

而 BehaviorSubject 是 RxJS 中的概念,它可以存储数据,并通过订阅者模式向订阅了这个 Facade 数据的组件推送数据。


代码来自 Manfred 的《Enterprise Angular》

有了 Facade 和实体类之后,我们其实同时也实现了 DDD 中的领域事件:我们借助了 RxJS 的 Observable 实现了一个前端的消息机制。

一般来说,同一个限界上下文的 Domain 层仅为当前限界上下文的 feature library 服务。如果需要跨限界上下文的领域事件,可以另外使用一些消息机制实现,例如一些简单的事件发布/订阅库。至于 Infrastructure 层则比较简单,只是封装了对服务端提供的接口的 XHR 调用。

Nx Workspace + DDD 实例

接下来我们来看看一个实际项目的例子,取自 Matrix 即将上线的新版反馈系统。代码写于本人学习 DDD 的早期,对 DDD 的概念还不算熟练的时期,所以其中存在在一些瑕疵,我会在讲解时指出并给出符合 DDD 的修改方法,如果你有不同的意见,欢迎指出并讨论。我将在讲述 Library 时使用通知页面对应的限界上下文作为例子。


反馈系统的通知页面

为了使用 Nx Workspace 以及 DDD 来实现这样的一个反馈系统,我设计了如左图所示的几个限界上下文。其中通知页面的限界上下文中含有四个 library,形成的依赖关系图如下图所示。这里 feedback、notification 等限界上下文和服务端的限界上下文的划分是一致的。只不过服务端的 domain 层之外还存在有持久化层等。


右边是这些 Library 形成的逻辑层级,箭头表示依赖关系,可以看到在 notification 限界上下文中我们得到了一个清晰的 Library 的依赖关系,这将有利于我们区分代码职责,以及后续的维护。

下图是 domain library 中的 notification.entity.ts 和 update-notification-status.facade.ts。


我在 Notification 实体类中还引入了 class-transformer 包的修饰器,它可以自动将服务端传来的 string 类型的 time 转换为 Date 类。

下图是通知列表组件类,可以看到它是如何通过 Angular 的依赖注入来与 Facade 进行沟通的。
值得一提的是,markNotificationAsRead 方法中的行为我认为并不恰当。UpdateNotificationStatusFacade 应该在更新成功之后自己通过领域事件来通知 GetUnreadNotificationCountFacade 重新获取数据,而不是让组件类手动进行,导致这部分的业务逻辑泄露到了 feature 层中。

小结

关于 DDD 在前端开发中的应用,我依然在探索当中,上述内容很可能只是冰山一角。 Manfred 为我们提供了一条可行的道路,而在 Matrix 新反馈系统的实践中我依然遇到了一些比较棘手的问题,例如我起初一直无法在采用了 DDD 的 Nx workspace 项目中正确地配置 Angular 的路由模块以及懒加载,又如 Angular 的依赖注入系统在适配到懒加载的 library 时存在陷阱等。不过经过十几天的搏斗,Matrix 新反馈系统最终还是完成了开发,在这个过程中我不仅增长了对 DDD 的认识,也学习了很多 Angular 的冷门知识点,可以说是大有所益了。

Nx Workspace 除了支持 Angular 之外,其实也支持 React,并且 Github 上已经有一些 Star 很多的项目在应用着了。如果你有兴趣,可以在学习 DDD 之余看看这些项目。总的来说,DDD 是软件工程里的一座丰碑,启发了无数对架构感兴趣的程序员。

链接

探究 DDD 在前端开发中的应用(一):从软件开发的本质讲起

软件开发的本质

大多数普通人在听到“软件开发”这个词语的时候,会联想到这样一幅画面:一群穿着格子衫、桌子上摆着二次元手办的又瘦又高的眼镜仔坐在电脑前,对着屏幕上各种千奇百怪的代码敲打键盘,从白天直到黑夜。

这些给程序员加上的定语我们暂且不提,但软件开发并不是仅仅意味着编写代码。无论是在公司里上班,还是大学里几个伙伴的合作,软件开发的本质都是:编写代码来满足某种需求。公司雇佣程序员,是为了让程序员创造能够提升公司业绩的软件,进而满足公司盈利的需求;大学的伙伴合作,是为了创造一些好玩的软件,满足伙伴之间求知探索的需求。

由此我们可以看到,代码的编写是一种过程,服务于作为最终目标的需求。在软件开发中,需求占据了主导地位。

软件开发的难点

在软件开发中,人们往往会受到时间、成本、资源上的各种限制,或者是受到市场变化的影响,从而修改既定的需求。在软件开发的维护阶段,人们又经常会发现既有的代码设计难以让他们编写新的代码来满足新的需求。所有的这些问题都会导致生产效率的降低,进而导致公司的盈利空间的萎缩,程序员失业风险增加!

让我们看看一个例子:假如你是张三,老板给你下达了一个任务,让你编写代码,完成一个微信的投票小程序。当你信誓旦旦地接下了任务,埋头苦干了几天之后,老板突然告诉你:我们不做小程序了,能不能直接做成一个投票网页,DDL 不变行不行?你很可能无法说“不”,只能拼命想办法把现有的代码迁移到网页上。你会希望自己编写的一部分代码能够被直接复用,这样就不需要再重新编写。

因此,软件开发的难点在于:如何更好地适应不断变化的需求。

软件开发的救赎之路

自计算机诞生以来,前前后后几代的程序员都在不懈的探索这个难点的解决之道。在这个旷日持久的斗争中,人们将自己的实践经验总结下来,形成了一个个经典的编程范式。从某种意义上来说,这些编程范式就是一个个建模工具,对复杂多变的问题进行建模,从而得出一些普遍性的、不变的规律,进而简化人们的工作。

例如,我们在初中时学到:牛顿为了解释物体的运动,建立了经典力学模型,大大推动了人类天文学等学科的发展。类似地,在上世纪 80 年代,Smalltalk 的程序员为了解决桌面程序的代码过于繁复杂糅的问题,创造性地提出了 MVC 模式,为适应桌面程序中复杂多样的需求提供了一个优秀的工具箱。

MVC 模式全称 Model-View-Controller,将桌面程序划分为模型、视图和控制器三个层面,分别进行编码,使得程序员可以在同一个时间点仅关注一个层面的代码,符合关注点分离原则(SoF),同时写出的代码很容易符合单一职责原则(SRP),大大提升了程序员的编码以及维护的效率。

对那些还没有踏入企业工作的大学生来说,这些经典的编程范式也并非毫无价值,它们可以指导你作为一名程序员,将 if-else 摆放在合适的地方、将复杂的逻辑进行合理的分割等,别忘了,这些范式都凝结着老一辈的的程序员的宝贵经验。

我们今天要讲的 DDD 也是一种凝聚了几代程序员的心血的经典编程范式。它诞生于本世纪初,立足于改善当时 J2EE 或 Spring+Hibernate 等事务性编程模型只关心数据,造成失血模型不能很好地结合业务需求的问题。

在本节的末尾,我想引用一句史蒂夫·乔布斯的话:”设计并不仅仅是感观,设计也是产品的工作方式“。这句话不但阐释了他个人对设计的理解,也十分契合我们所讲的内容以及接下来将介绍的 DDD。在后文中,我会再次提到这句话,并向大家诠释 DDD 作为一种设计是如何塑造软件的工作方式的。

链接

探究DDD在前端开发中的应用:前言

此文章文字稿取自我在学校某实验室的公众号上发表的技术分享文章。

一直以来,软件建模都是一个非常热门的概念,它提供了一张将要开发的软件的蓝图,在系统需求和系统实现之间架起了一坐桥梁,而程序员实际编写出的代码能否很好地满足需求,很大程度上取决于选用的建模方法是否合理。

我们今天要介绍的 DDD(“领域驱动设计“的英文缩写)是软件工程中的一个著名的开发实践,给人们提供了战略和战术上的软件建模工具,能够有效地提高人们把握业务需求、生产精准的建模设计的能力,从而能够让软件的开发和维护更有效率。

本次技术分享将从软件开发的本质讲起,为大家简要介绍 DDD 产生的背景。然后我们将学习 DDD 的基础概念,对 DDD 形成一个大概的认识。最后我们用一个 Angular 结合 Nx workspace 的前端项目来讲解笔者对 DDD 在前端开发中的应用的探索。

由于笔者研究 DDD 的时间并不很长,若出现错误欢迎指正,有疑惑之处也欢迎讨论。

链接