面向对象编程：这里我说了算！自 9 岁拥有第一台 Commodore 64 家用电脑起，我就开始编程了。然而，面对“如何写出好代码”这个问题，我仍然感觉自己有很多需要学习的地方。在探索如何提高编程水平的过程中，我学习了很多种语言，其中大多数是以面向对象（Object-Oriented, OO）为主的。然而，令人惊讶的是，在我读过的大多数书本、杂志和网络文章中，存在着大量糟糕透顶的代码，但它们却被当作面向对象的范例。在这些代码中，我看到被违反得最频繁的原则是 “命令，不要去询问”（Tell, Don't Ask） 原则。这个原则的核心在于：一个对象应该命令其它对象该做什么，而不是去查询其它对象的状态来决定自己要做什么。查询其它对象的状态来决定做什么，也被称作 “功能嫉妒”（Feature Envy）。在面向对象编程中，一个对象被定义为由“对象状态”和“操作这个状态的方法”组成。在《Holub on Patterns: Learning Design Patterns By Looking At Code》这本书里，Allen Holub 在第一章中有一节的标题是“为什么 getter 和 setter 方法有害”。他在 JavaWorld 上的一篇文章里也谈论了这个问题。对所有的面向对象程序员来说，这应该是一篇“必读”文章。我有一些程序员同事，他们在为一个对象声明了属性后，第一步就是添加 getter 和 setter 方法。JavaBean 规范对于这种文化的推广负有很大责任。人们认为这是一种能让你写出可复用的模块化组件的好方法，但这已是很多年前的事了，时过境迁。编写带有 getter 和 setter 方法的类会导致过程式代码。通过 getter 和 setter 来获取数据进行操作的逻辑最终会遍布整个应用，进而经常导致应用内的重复（这违反了另外一个原则：DRY——不要自我重复 (Don't Repeat Yourself)）。这会致使产生很难维护的代码，当你对一个类做任何修改时，都会在整个应用内造成连锁式的牵连。用这种方式来暴露数据还会妨碍你重构你的类，因为对这样的属性的任何修改都意味着会影响到访问了这个属性的其它类。违反“命令，不要去询问”原则的另外一个副作用是，你的探询最终会变成严重依赖状态信息并带有很多前提条件。这会让人很难理解你究竟询问的是什么。你很可能会最终违反的第三个原则是，最少知识（Least Knowledge） 原则，也叫作 得墨忒耳定律（Law Of Demeter）。这个定律可以总结为下面一句话：一个类应该只跟它的直接朋友通话，不要跟陌生人说话。在类里面加入 getter 方法，你的代码最终会写成这样：if (person.getAddress().getCountry() == "Australia") {这违反了得墨忒耳定律，因为这个调用者跟 Person 过于亲密。它知道 Person 里有一个 Address，而 Address 里还有一个 country。它实际上应该写成这样：if (person.livesIn("Australia")) {这并不违反得墨忒耳定律，因为这个调用者只跟它的直接朋友 Person 通话，而且它不知道内部情况。从“命令，不要询问”的视角来看，这也是更好的做法，因为确定这个 person 是否居住在 Australia 的逻辑被隐藏到了 Person 里。如果我们改变 Person 里存储国家信息的方式，这将不会影响任何依赖这个信息的其它类。另外一个这样写代码的好处是，它把我们的意图显示得很清楚。这是我会在以后的时间里讨论的另外一个话题。为了避免违反“命令，不要去询问”原则，有几样事情你要记在心里：在 IDE 里敲击快捷生成键前，询问自己 ‘五个为什么’，问问自己为什么一上来就添加 getter 和 setter 方法。不要询问对象的状态。要做什么，告诉它们该怎么做。操作所属的对象拥有这些操作的数据。进一步阅读我的一个朋友是这个思想的大力倡导者，他用 East Oriented 方式精彩地解释了这个原则。《Tell Don't Ask and the effect on testing》，出自 Steve Freeman 和 Nat Pryce，他们也是《Growing Object-Oriented Software》、《Guided by Tests》两本书的作者。[英文原文：I give the orders around here! ]说明：本文原文发表于 2012 年左右，文中提到的部分观点（如 JavaBean 规范的影响）具有特定的时代背景，但核心设计原则（Tell, Don't Ask、得墨忒耳定律）在当今面向对象设计中依然适用。
前言领域驱动设计（Domain-Driven Design，简称 DDD）近年来备受关注，尤其是随着 2B 业务和 SaaS 模式的兴起。然而，不少开发者在阅读 Eric Evans 的《领域驱动设计》或 Vaughn Vernon 的《实现领域驱动设计》后，常感到内容过于抽象，难以落地。本文将在介绍 DDD 基本概念的同时，通过一个虚拟的公司和项目场景，详细介绍领域驱动设计的落地实践。什么是领域我习惯将领域描述为财务领域、人力领域、房地产领域、金融领域等。在领域内，各种业务规则与业务知识盛行。如何有效把控规则的变化、应对复杂知识？有一个关键的四字词语：分而治之。分治法在很多场景下体现了其强大的作用力。领域本身很大很复杂，那就拆分，得到更小的领域，也即子域。如同递归调用一般，将一个复杂问题拆分单独求解，最终将解汇总得到复杂问题解。怎么拆？拆成什么样合适？依据什么拆？这些在领域驱动设计中都有了一套答案。虽然领域驱动设计不是银弹，但可以说是一套极好的系统方法论，或称为架构设计的方法论。领域驱动设计常以战略设计与战术设计来将整个领域展现得淋漓尽致，其作用范围既面向业务也面向技术。从战略角度的整体性（个人更喜欢称其为上帝视角）去规划系统、划分领域；而从战术角度则从技术层面来指导我们该如何去设计。为什么需要建立一个领域模型领域驱动设计告诉我们，在通过软件实现一个业务系统时，建立一个领域模型是非常重要和必要的，因为领域模型具有以下特点：抽象与边界：领域模型是对具有某个边界的领域的一个抽象，反映了领域内用户业务需求的本质；领域模型是有边界的，只反映了我们在领域内所关注的部分。业务无关技术：领域模型只反映业务，和任何技术实现无关；领域模型不仅能反映领域中的一些实体概念（如货物、书本、应聘记录、地址等），还能反映领域中的一些过程概念（如资金转账等）。逻辑集中：领域模型确保了我们的软件的业务逻辑都在一个模型中，都在一个地方；这样对提高软件的可维护性、业务可理解性以及可重用性方面都有很好的帮助。知识转化：领域模型能够帮助开发人员相对平滑地将领域知识转化为软件构造。全程贯穿：领域模型贯穿软件分析、设计，以及开发的整个过程；领域专家、设计人员、开发人员通过领域模型进行交流，彼此共享知识与信息；因为大家面向的都是同一个模型，所以可以防止需求走样，可以让软件设计开发人员做出来的软件真正满足需求。共同构建：要建立正确的领域模型并不简单，需要领域专家、设计、开发人员积极沟通共同努力，然后才能使大家对领域的认识不断深入，从而不断细化和完善领域模型。表达方式：为了让领域模型看得见，我们需要用一些方法来表示它；图是表达领域模型最常用的方式，但不是唯一的表达方式，代码或文字描述也能表达领域模型。核心价值：领域模型是整个软件的核心，是软件中最有价值和最具竞争力的部分；设计足够精良且符合业务需求的领域模型能够更快速地响应需求变化。领域通用语言（Ubiquitous Language）我们认识到由软件专家和领域专家通力合作开发出一个领域的模型是绝对需要的，但是，那种方法通常会由于一些基础交流的障碍而存在难点。开发人员满脑子都是类、方法、算法、模式、架构等等，总是想将实际生活中的概念和程序工件进行对应。他们希望看到要建立哪些对象类，要如何对对象类之间的关系建模。他们会习惯按照封装、继承、多态等面向对象编程中的概念去思考，会随时随地这样交谈，这对他们来说太正常不过了。但是领域专家通常对此一无所知，他们对软件类库、框架、持久化甚至数据库没有什么概念。他们只了解他们特有的领域专业技能。比如，在空中交通监控样例中，领域专家知道飞机、路线、海拔、经度、纬度，知道飞机偏离了正常路线，知道飞机的发射。他们用他们自己的术语讨论这些事情，有时这对于外行来说很难直接理解。如果一个人说了什么事情，其他的人不能理解，或者更糟的是错误理解成其他事情，又有什么机会来保证项目成功呢？在交流的过程中，需要做翻译才能让其他的人理解这些概念。开发人员可能会努力使用外行人的语言来解析一些设计模式，但这并不一定都能成功奏效。领域专家也可能会创建一种新的行话以努力表达他们的这些想法。在这个痛苦的交流过程中，这种类型的翻译并不能对知识的构建过程产生帮助。领域驱动设计的一个核心的原则是使用一种基于模型的语言。因为模型是软件满足领域的共同点，它很适合作为这种通用语言的构造基础。使用模型作为语言的核心骨架，要求团队在进行所有的交流时都使用一致的语言，在代码中也是这样。在共享知识和推敲模型时，团队会使用演讲、文字和图形。这里需要确保团队使用的语言在所有的交流形式中看上去都是一致的，这种语言被称为“通用语言（Ubiquitous Language）”。通用语言应该在建模过程中广泛尝试以推动软件专家和领域专家之间的沟通，从而发现要在模型中使用的主要的领域概念。虚拟的业务场景这里我们虚拟一个业务场景，用于本文的演示用例。下文中的知识介绍以及代码演示，都基于这个业务需求背景。假如你的公司叫"RabbitTech"，公司的主营业务是做知识付费，这个知识付费的产品叫做"RabbitAdvisors"（名字是 AI 帮我起的）。公司会邀请一些业界专家来当作者，作者会在"RabbitAdvisors"中开设课程专栏，用户可以选择自己感兴趣的专栏进行付费订阅，用户可以阅读订阅过的专栏文稿。"RabbitTech"会根据最初的合同给作者佣金分成。这里提一个问题，假如你是"RabbitTech"的 CTO，你将会怎样来对"RabbitAdvisors"进行架构设计，又如何给团队分工呢？在这里可以停下来几分钟动笔画一下。看完本文后，再回过头来看看这个问题和你最初想法是否一致。本文的代码本文的代码是基于业界流行的技术，项目基于上文的虚拟公司和虚拟产品，不涉及公司的隐私数据。后端微服务 1：https://github.com/eyebluecn/smart-classroom-misc后端微服务 2：https://github.com/eyebluecn/smart-classroom-subscription前端项目：https://github.com/eyebluecn/smart-classroom-front在线演示地址：https://classroom.eyeblue.cn/本文的行文逻辑本文内容比较多，这里把主要的内容简要介绍一下，让大家有一个大的逻辑脉络印象。好了，背景介绍完毕，下面回归到我们的正题。DDD 概述传统 MVC 模式的局限那我们就先看看没有 DDD，软件开发都是怎么做的？拿大家熟悉的 MVC 模式举例，这里的 Model 是数据库模型，业务逻辑在 Controller 中实现（一般会由 Service 来辅助实现），View 层主要负责视图展示。对于业务逻辑不复杂的软件开发，MVC 是简单高效的方法。但是随着业务逻辑愈来愈复杂，MVC 会开始力不从心。主要体现在这几个方面：MVC 模式仅仅反应了软件层面的架构，它不包含业务语言，无法使用该设计直接和业务对话。MVC 模式天然切割了数据和行为，然后用数据库实现数据，用服务实现行为，容易造成需求的首尾分离。缺乏明确的边界划分，至少在顶层设计层面没有边界划分的规范要求，更多地是靠技术负责人根据经验进行划分，大规模团队协作容易出现职责不清晰、分工不明确。传统的开发模式或多或少都存在上面的问题。DDD 就应运而生了。DDD 定义领域驱动设计（英文：Domain-Driven Design，缩写 DDD）是一种模型驱动设计的方法，通过领域模型捕捉领域知识，使用领域模型构造更易维护的软件。模型在领域驱动设计中，有三个重要用途：通过模型直接反映软件实现的结构。以模型为基础形成团队的统一语言。把模型作为精粹的知识，用于传递。DDD 的价值因此 DDD 能够带来这几方面的价值：统一语言：团队（业务方、产品、设计、技术等）在一个限定的上下文中有意识地形成对事物统一的描述，从而形成统一的概念（模型）。统一语言用于需求文档、PRD 文档、系分文档、代码以及日常沟通中，统一的概念和术语可以极大地提升沟通效率和工作效率。面向业务建模：领域模型和数据模型分离，业务复杂度和技术复杂度分离。DDD 聚焦于领域模型，将技术实现细节从模型中剥离出来，能够更好地降低业务和技术的耦合度。边界清晰的设计方法：通过对需求的识别及分类，划分出领域、子域和限界上下文，进而指导团队成员分工协作，从而做到将复杂的问题分而治之地解决。业务领域的知识沉淀：通过模型与软件实现关联，统一语言与模型关联，反复论证和提炼模型，使得模型与业务的真实世界保持一致，从而促使业务知识通过模型得以传递和沉淀。DDD 的基本概念在 DDD 中涉及到很多概念，对于新手来说，往往会目不暇接，但是了解熟悉这些概念又是必须的，所以我尽可能地使用通俗易懂的语言，并结合实例来说明。统一语言团队（业务方、产品、设计、技术等）在一个限定的上下文中有意识地形成对事物统一的描述，从而形成统一的概念（模型），这些统一的描述和统一的概念就是统一语言。统一语言主要源自于领域模型的概念与逻辑，作为对业务维度的补充和展开，也会将限界上下文、系统隐喻等纳入到统一语言中。以"RabbitAdvisors"商业模式中的这句话“用户可以选择自己感兴趣的专栏进行付费订阅”，进行简单的建模。根据这个模型，我们可以形成统一语言：用户（User）：是指所有在"RabbitAdvisors"注册过的人。（来自领域模型概念）订阅的专栏（Subscription）：是指用户付费过的专栏。（来自领域模型概念）用户可以订阅多个专栏。（来自领域模型逻辑）订阅。（来自限界上下文）通过定义与解释，我们使这些词语在其所使用的上下文中没有歧义。再通过这些基础词汇，去描述业务的行为或者规则，慢慢就可以将其确立为跨业务与技术的统一语言了。统一语言是在使用中被确立的。有了统一语言后，我们就可以很方便的描述需求：用户 (User) 可以查阅自己订阅过的专栏（Subscription），也可以查看其中的教学内容。也可以用来描述测试用例：当用户 (User) 已购买过某个专栏（Subscription），那么当他访问这个专栏时，就不需要再为内容付费。这里仅仅举了两个使用统一语言的场景，当所有工种角色都接受它，用它去描述业务和系统的时候，它才会成为真正的统一语言。战略设计在 DDD 中可以分为战略设计和战术设计，各自包含的内容如下图所示：战略设计指的是对整个领域进行分析和规划，确定领域中的概念、业务规则和领域边界等基础性问题。在战略设计中，需要对领域进行全面的了解和分析，探究业务的规则和本质，并且需要考虑到领域的未来发展趋势和可能的变化。领域、子域和限界上下文属于战略设计的范畴。领域领域（Domain）其实就是一个组织所要做的整个事情，以及这个事情下所包含的一切内容。这是一个范围概念，而且是面向业务的（注意这里不是面向技术的，更不是面向数据库的持久化的），每个组织都有自己的人员、规则和流程，当你为该组织开发软件的时候，你面对的就是这个组织的领域。例如，在"RabbitTech"的"RabbitAdvisors"中，领域就是知识付费领域（或者付费咨询）。子域子域是指在一个大的领域中，可以进一步划分出来的独立的业务子领域，它们有着自己的业务概念、规则和流程等。例如，在"RabbitAdvisors"中，子域可以有订阅域、金融域、专栏域等。为了区分重要性的不同，我们又会将子域划分成核心域、通用域以及支撑域。核心域：决定公司和产品核心竞争力的子域就是核心域，它是业务成功的主要因素。核心域直接对业务产生价值。例如，在"RabbitAdvisors"中，订阅域就是核心域。通用域：没有太多个性化的诉求，同时被多个子域使用的、具有通用功能的子域就是通用域。通用域间接对业务产生价值。例如，在"RabbitAdvisors"中，权限域、登录域就是通用域。支撑域：支撑其他领域业务，具有企业特性，但不具有通用性。支撑域间接对业务产生价值，例如，在"RabbitAdvisors"中，专栏域、评论域就是支撑域。限界上下文限界上下文就是业务边界的划分，这个边界可以是一个子域或者多个子域的集合。如何进行划分，一个行之有效的方法是一个界限上下文必须支持一个完整的业务流程，保证这个业务流程所涉及的领域都在一个限界上下文中。限界上下文是微服务拆分的依据，即每个限界上下文对应一个微服务。例如，在"RabbitAdvisors"中，可以有一个限界上下文叫“专栏订阅上下文”，它就包含了订单域和订阅域。战术设计战术设计则是在战略设计的基础上，对领域中的具体问题进行具体的解决方案设计。战术设计关注的是领域中的具体情境和场景，需要针对具体的问题进行具体的分析和设计，以满足业务需求。实体、值对象、聚合、工厂、资源库、领域服务和领域事件就属于战术设计的范畴。实体定义：实体是拥有唯一标识和状态，且具有生命周期的业务对象。实体通常代表着现实世界中的某个概念，实体与领域模型密切相关，它是领域模型中多个属性、操作或者行为的载体。例如：在"RabbitAdvisors"中，专栏、课程文章、订阅都是实体。实体的代码形态一般有四种形态：失血模型：模型仅仅包含数据的定义和 getter/setter 方法，业务逻辑和应用逻辑都放到服务层中。这种类在 Java 中叫 POJO。贫血模型：贫血模型中包含了一些业务逻辑，但不包含依赖持久层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层中。充血模型：充血模型中包含了所有的业务逻辑，包括依赖于持久层的业务逻辑。胀血模型：胀血模型就是把和业务逻辑不想关的其他应用逻辑（如授权、事务等）都放到领域模型中。结合团队以及兄弟团队的实践，建议实体采用贫血模型，实体和领域服务共同构成领域模型。这样可以使得实体具备业务知识，但又不至于太过臃肿。值对象定义：通过对象属性值来识别的对象，它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面，并且是一个没有标识符的对象，叫作值对象。值对象没有唯一标识，没有生命周期，不可修改，当值对象发生改变时只能替换。值对象的业务形态：大多数情况下实体具有很多属性，这些属性一般都是平铺，但有的属性进行归类和聚合后能够表达一个业务含义，就将这些属性封装到一起形成值对象，从而方便沟通而无需关注细节，因此可以说值对象就是用来描述实体的特征。当然实体的单一属性也是值对象。值对象的代码形态：值对象有两种：单一属性的值对象，例如字符串、整型、枚举等；多个属性的值对象，这时候设计成 class，包含多个属性，但是没有 ID，值对象中可以嵌套值对象。聚合和聚合根聚合是一种更大范围的封装，把一组有相同生命周期、在业务上不可分隔的实体和值对象放在一起考虑，只有根实体可以对外暴露引用，这个根实体就是聚合根，聚合也是一种内聚性的表现。领域、子域、限界上下文、聚合都是用来表示一个业务范围，那他们的关系是怎样的呢？领域、子域、限界上下文属于战略设计，而聚合属于战术设计，聚合的范围是小于前三者的，范围大小图如下：工厂工厂是一种重要的设计模式，DDD 只是拿来主义，用到了工厂。考虑使用工厂的主要动机：将创建复杂对象和聚合的职责分配给一个单独的对象，该对象本身并不承担领域模型中的职责，但是依然是领域设计的一部分。工厂应该提供一个创建对象的接口，该接口封装了所有创建对象的复杂操作过程，同时，它并不需要客户去引用那个实际被创建的对象。对于聚合来说，我们应该一次性地创建整个聚合，并且确保它的不变条件得到满足。资源库资源库（Repository）是一种模式，用于封装数据访问逻辑，提供对数据的持久化和查询。它旨在将数据访问细节与领域模型分离，使领域模型更加独立和可测试。资源库提供了一种统一的接口，使得领域模型可以与不同的数据存储方式（如关系数据库、文档数据库、内存数据库等）进行交互，同时也提供了一些查询操作，以便在领域层中进行数据查询。如果我们使用 MyBatis 的话，Mapper 就是对资源库的一种实现。领域服务有些领域中的动作看上去并不属于任何对象。它们代表了领域中的一个重要的行为，不能忽略它们或者简单地把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来时，推荐的实践方式是将它声明成一个服务，这个服务就是领域服务。例如，在"RabbitAdvisors"中，订阅（Subscribe）行为是一个非常重要的领域概念，它涉及到订单创建、支付、增加订阅记录等和多个实体相关联的操作，将该行为放到任何一个实体中都不合适，在这种情况下，将“订阅”识别为领域服务是比较合适的。领域事件领域事件是发生在领域中且值得注意的事件。而领域事件通常意味着领域对象状态的改变。领域事件在系统中起到了传递消息、触发其他动作的作用，是解耦领域模型的重要手段之一。我们往往利用消息队列来传递领域事件。例如，在"RabbitAdvisors"中，当用户订阅了一个专栏后，会产生一个“专栏订阅成功”的领域事件，用户成长域会根据这个领域事件决定增加用户积分。领域建模到这里，DDD 的核心概念就已经介绍完毕，如果还是有种不明所以的感觉也是正常的，不要着急，下面的内容将会逐步转到实践了。领域驱动设计的核心在于领域建模，架构师的水平高低在很大程度上也体现在领域建模水平上。领域建模的主要目的是捕捉业务知识，形成统一语言，沉淀领域模型。好的领域建模就意味着对业务要有深刻的理解，能够洞察问题本质。领域建模的产出物一般有以下内容：领域模型：包含领域对象、属性、关系、行为、边界范围等各个方面，用于描述业务的本质，这也是最重要的产出物。用例图：用于明确系统的功能。数据模型：描述系统的数据结构和关系，包括实体关系模型、关系数据库模型等。状态图：用于描述系统各个状态及其转移条件。活动图：用于描述系统流程中的各个活动及其关系。序列图：描述系统中各个对象之间的交互过程和消息传递序列。架构模型：包含系统的物理和逻辑结构，包括组件、模块、接口等。下面介绍两种常见的领域建模方法。事件风暴建模简介我们可以把事件看做行为的印记。比如支付这个行为。我们不需要直接描述支付这个行为，而是通过捕捉这个行为前后的事件：支付发起（Payment Started）和支付完成（Paid）。要知道，事件自身能表达的含义有限，但是将一系列事件按照发生顺序排列起来，就能还原发生过的行为。通过寻找事件，以及事件背后的领域概念，就能完成对领域概念的挖掘和建模，这就是事件建模的底层逻辑。事件建模法是一种元方法，事件风暴建模就是事件建模的一种。事件风暴是意大利人 Alberto Brandolini 在 2012 年创造的一种事件建模方法。在形式上，事件风暴是一种互动式建模工作坊，通过将不同背景的项目参与方汇聚一堂，集思广益，从而形成有效的模型。这也是事件风暴名称的由来——因为它本质上就是一种头脑风暴。本文事件风暴建模法主要参考引用自甫田的《事件风暴与领域建模在阿里的实践》。事件风暴语法事件风暴通过事件、命令与策略之间的响应关系来组织逻辑。它定义了一套彩色贴纸的”语法”: 不同颜色的贴纸都有定义。浅黄色代表角色 (Actor)、蓝色表示命令 (Command)、粉色代表业务规则 (Policy)、紫色代表系统（System）、橙色代表事件（Event）, 绿色表示阅读模型 (Read Model)、红色代表热点问题 (HotSpot)。每种语法的具体含义如下：行动者（Actors）：是系统的使用者。这里使用者是一个相对模糊的概念，可能是现实中的人也可能是别的系统。命令（Command）：是由行动者发起的行为。它代表了某种决定，通常是事件的起因，也称作行动者触发命令（AIC，Actor Initiated Command）。事件（Event）：就是我们前文讨论过的事件。系统（System）：指代的是不需要了解细节的三方系统。因为不需要了解细节，所以我们可以将它们看作一个整体。阅读模型（Read Model）：用以支撑决策的信息。通常与界面布局有关。策略（Policy）：是对于事件的响应，通过策略可以触发新的命令，由策略触发的命令，被称作系统触发命令（SIC，System Initiated Command）。热点问题（HotSpot）：是业务痛点，瓶颈，模糊点。如果有争论，可以将双方观点用热点问题的形式记录。事件风暴的这一系列语法的制定目的，是为了寻找到合适的载体来描述清楚业务逻辑和关键流转过程，从而更方便地在不同角色间传递领域知识。接下来我们以"RabbitAdvisors"中订阅这个流程作为实例，看一下如何进行事件风暴。事件风暴操作流程准备工作准备物料：彩色贴纸、笔纸、一个足够大的房间等。房间里不要有椅子，因为在事件风暴过程中，我们希望大家都全神贯注的投入，而不是坐在椅子上开始放松。邀请正确的人：有问题的人和有答案的人。程序员、交互设计师、测试等都是有问题的人，需要通过事件风暴理解业务和产品；有答案的人通常是用户、业务或产品，他们通常能回答业务的背景，诉求和目标。开场介绍在事件风暴中，有一个特殊角色是主持人，一般也是事件风暴的组织者。主持人有几个重要职责，主持事件风暴、保持参与者的专注、通过提问驱动交流、总结提炼事件风暴建模成果。在事件风暴正式开始前，由主持人介绍事件风暴是什么、有什么好处以及彩色贴纸的用法。然后介绍本次事件风暴讨论的范围和目标。例如，此次事件风暴讨论"RabbitAdvisors"中用户订阅专栏的场景，目标是理清从用户发起订阅到用户查看订阅专栏的整个的业务流程。事件风暴的方式沟通业务第一步：梳理事件（橙色贴纸）事件是已发生且重要的事情。事件必须是既成事实，且业务关注的事情。通常主持人会先准备第一个事件（可以是系统中任一事件），然后把它贴到墙上。假设第一个事件是：专栏已订阅。接下来主持人通过提问引导大家找到更多的事件：事件发生前有哪些事件？（“专栏已订阅”前须有“订单已支付”事件）事件发生后下一个事件是什么？提问会引导参与事件风暴的同学将新发现的事件不断补充到墙上。事件要保持整体的时间顺序：先发生的事情贴在左边，后发生的事情在右边。通常大家容易关注系统的正常流程，也就是 Happy path。这时候主持人需要引导大家关注业务的非正常流程 Unhappy path。边界条件，异常情况通常是业务复杂性的重要原因，也是非常容易被忽视的部分。事件一定会发生吗？（订单一定可以创建成功吗？不是，贴上“订单创建失败”事件）追问 unhappy path 梳理出业务的完整视图，当大家发现新事件的速度接近停滞的时候，就应进入梳理业务规则的阶段了。第二步：业务规则（粉色贴纸）业务规则或者业务逻辑，是业务中最重要的部分，主持人会提出以下问题：事件是否一定成功？如果不是，那么成功的前提条件是什么？该事件是否会导致其他事件的发生？例如“订单已创建”事件的业务逻辑：订单已创建的前提条件是专栏可订阅，同时用户未订阅过该专栏。订单创建后，会导致发起支付。第三步：行动者（浅黄色贴纸），命令（蓝色贴纸）、阅读模型（绿色贴纸）和系统（紫色贴纸）主持人通过问题引导：是什么触发了事件，是命令还是规则是谁执行了动作，是人还是系统做出动作前，用户需要获取到哪些信息通过类似上面的问题，逐步引导大家找到 Actor, Command, Read Model。第四步：热点问题（红色贴纸）业务痛点、瓶颈、模糊点可以用红色贴纸记录，这些问题需要业务带回去讨论确定清楚，不要在事件风暴中尝试解决所有的 hotspot。第五步：故事串讲邀请一名现场成员，按事件发生的时间顺序串讲业务，过程中，听众注意到不一致的地方，提出问题；大家一起讨论，调整相关的事件、逻辑来达成一致。第六步：产出架构通过事件风暴，业务流程和处理逻辑应该已经很清楚了，接下来就由架构师产出对应的架构。可以依据事件风暴产出领域模型、用例图、状态图、活动图、序列图等关键架构交付物。事件风暴小结事件风暴法通过头脑风暴发现领域事件，以“对于事件的响应”为主要维度寻找事件间的关联，它是一种简单明快的事件建模方法。但是事件风暴也有一些不足之处，一方面是事件风暴的模式偏重，需要不同角色的成员集体参与，涉及的人员多、流程长。另一方面，也是最关键的一点，事件风暴的成功关键在于收敛逻辑。在发散阶段，所有参与者可以天马行空。不过这样的方法在产生有效信息的同时，也会产生大量的噪音。但是在收敛阶段，则会按照某一逻辑主线，合并相似概念，过滤无用信息。那么我们可以很容易地想到，如果主持人采用不同的逻辑去收敛事件，最后获得的结果也可能不尽相同。因此事件风暴法极度依赖主持人的经验与判断，最终结果自然就会存在一定的随意性。这也使得事件风暴法变成了那种“一学就会，一用就废”的方法。有经验的老手越用越顺手，而初学者往往不得要领。既然最终事件流的质量取决于收敛逻辑，那么我们为什么不直接从收敛逻辑出发，通过引导 - 分析直接获取事件流呢？的确可以这么做，而四色建模法也正是这样一种从收敛逻辑出发的强分析法。四色建模法简介四色建模法的起源有点复杂。最早的源头是 Peter Coad 在 1992 年研究对象模式时，为了提取可复用的对象组件而发明的四原型法。在 1997 年的时候，作为一种补充策略，Peter Coad 开始提倡在模型中引入不同的颜色，从而将模型的动态交互展示在静态的模型中，形成了彩色建模法（Color Modeling）。而后，徐昊（Thoughtworks 中国区 CTO）在 2005 年左右，将事件建模与彩色建模法结合形成了四色建模法，用以获得更具业务含义的模型，以便直接使用模型作为统一语言。四色法主要通过三个关键逻辑寻找领域事件。这三个逻辑源自企业运营与管理实践：如果有现金收入，表示承担了义务。拿钱办事，需要收集证据，说明义务履约成功。如果有现金支出，表明拥有权利。花钱消灾，需要检查对方是否按时履约。对于没有现金往来的，可以通过目标——实际对比，产生类似履约的约束。所谓目标——实际对比，就是设立一个目标 / 计划，然后追踪实际执行的结果。寻找领域模型，提取统一语言，做分层与隔离，目的都是为了有效地控制变化和传播。那么我们更应该从源头入手，找到业务中相对稳定的部分去建模，而不是选择一段变化的业务去稳定地构造它的模型。毕竟我们期待领域驱动设计能够为我们带来的一切优点，都依赖于尽早地从业务侧隔离开稳定与易变的部分。业务模式在企业生命周期内，是相当稳定的。而业务模式主要通过收入流和成本结构来体现。四色建模法的核心逻辑就源自收入流和成本结构，通过现金权责识别与收入流有关的事件，通过目标——实际对比发现与成本结构关联的事件。这是四色建模法与其他建模法的主要不同：完全拥抱业务视角，以业务的内在逻辑作为发现事件的主要逻辑。本文四色建模法主要参考引用自徐昊的《如何落地业务建模》。四色建模法语法四色建模法之所以叫四色建模，主要是因为它里面存在四种主要的对象原型，且每一种原型都有一种颜色与之对应，一共有四种颜色，由此而得名。四色建模法操作流程Step1: 寻找关键现金往来，构造一个凭证表示它。然后在凭证上罗列关键数据项，主要有发生的时间点、金额等。Step2: 针对每一项关键数据项，寻找它的来源。来源只能有三个：用户输入，由前序凭证提供，或者根据算法计算。我们需要以此为依据去寻找前序凭证。Step3: 回到现金凭证，思考它所对应的权利与责任。这些权利与责任需要哪些凭证证明，并以此为依据，寻找后续凭证。Step4: 无论是何种凭证，必须罗列关键数据项，并保证数据项获取的顺畅。Step5: 如果与现金往来关联不大，那么寻找关键 KPI 指标，并构造一个验收凭证表示它。其余步骤与现金往来一致。Step6: 在获取了相互关联的凭证流之后（实际就是事件流），我们可以进入模型细化阶段，围绕每个凭证，寻找参与其中的角色。Step7: 思考哪些参与方可能扮演这些角色，并将他们加入模型中。Step8: 通过描述对象，为模型添加补充说明。可以看到四色建模法对最终模型的构造有更为明确的要求：凭证必须围绕现金往来或 KPI；凭证间必须通过关键数据项明确关联关系。这也是说四色建模是一种强分析方法的原因：可经由明确的逻辑推导，不需要依赖于发散 - 收敛式的探索。接下来依然用"RabbitAdvisors"作为例子，来演示如何使用四色建模法建模。寻找现金往来和凭证首先我们需要思考一下在"RabbitAdvisors"专栏的模型中，有哪些现金往来（Payment 和 Commission Payment）？很容易想到的就是读者购买专栏以及专栏作者分成这两项，构造的模型如下：如图所示，我们构造了两个凭证去表示这两个现金往来，然后分别列出它们的关键数据项：时间点和金额。接着就可以开始追溯前序凭证了。我们先看一下这两个凭证中相对简单的一个：读者购买专栏的凭证。它的关键数据项金额（amount）显然不可能由读者输入，而应该由前序凭证提供。支付的前序凭证是什么呢？是订单，因为是针对订单进行支付的。因此支付的金额应该与订单的金额相等。而订单的金额显然也不可能是由读者输入的，那么订单的前序凭证是什么呢？是专栏报价，订单的金额应该与专栏报价相等。那报价哪里来的呢？是由编辑输入的。到此为止，我们就完成了一条完整的追溯，如图所示：在寻找完前序凭证之后，Payment 有什么履约凭证吗？那我们要看一看，从客户那里收到的钱表示了什么权责。根据业务上下文，读者购买的是阅读专栏的权利，那么我们就需要一个凭证表示读者可以在后续阅读对应专栏里的内容。于是我们可以引入凭证订阅（Subscription），它具有关键数据项开始时间（start_time），也就是从何时开始，用户可以阅读对应专栏的内容。我们知道这个时间必须晚于支付完成，不能允许读者在支付完成前就阅读专栏，哪怕早一毫秒都不行：现在我们再来看专栏作者分成，和上面支付单类似，佣金支付单也有一个前序凭证，是佣金。这里的难点在于佣金的关键数据项金额（amount）。显然不可能让作者自己输入要提取多少分成。这个分成通常是按某个规定好的时间段作为账期，比如每月一次，或者每季度一次。然后按照账期找到所有账期内读者购买的凭证，求出总销售金额，然后再按约定好的分成比例，计算作者的分成。不过这里就有一些数据项来路不明了：分成比例（percentage）、账期日（payment days）和上次分成付款日（last paid）。那么如果我们往前追溯，可以发现包含分成比例、账期日的凭证，是专栏撰写合同，而上次分成付款日则是通过计算得到的（分成支付凭证中最晚的那个。如果没有，则默认是合同签约日）。于是我们可以得到：到此为止，我们获得了一条由通过关键数据项彼此关联的凭证链，同时也是领域事件流。这条凭证链表示了从作者签约、专栏定价、读者付费、作者分成的全过程，捕捉了"RabbitAdvisors"专栏这项业务的收入流，我们称之为业务的脊梁（Backbone of the business）。寻找角色和参与者接下来寻找与凭证相关的角色，然后再寻找能够扮演这些角色的参与者，以及与凭证相关的标的物，如图所示：获得领域模型按照关键数据项间的关联，将模型连在一起，稍加润色，就能得到领域模型了：验证业务脊梁以及领域模型的有效性找到了业务脊梁以及初步得到领域模型后，我们需要再验证一下他们的有效性，看业务能否顺畅进行。首先我们可以将所有的凭证，转化为真实的单据；然后通过角色扮演游戏，让业务方模拟业务的经营。在验证的过程中，除了正常流程之外，我们主要需要模拟的是各种业务异常引起追溯的例子，看看被业务脊梁捕捉的数据是否能够保证我们对权责的证明。比如我们之前讲到专栏定价修改、读者不满意的情况。类似的还有作者对分成不满意，质疑分成结果，等等。这是四色建模另一个不同于其他建模方法的地方：它认为软件系统是业务流程的加速和优化，但是模型是要用以支撑业务运营的，并不仅仅为了构造软件而设计的，因而天然地更容易被业务方接受，成为统一语言。如果在验证过程中有问题，那么就回到前面的步骤进行修改迭代。至此，我们就使用四色建模法完成了领域建模。四色建模法小结四色建模法是从业务的基础逻辑，也就是收入流和成本结构出发，建模业务的经营，并从中提取领域模型的。使用四色建模法，会有一种建模建出了职业经理人的感觉。因为它更容易从纯业务角度说服业务方，采纳模型，作为统一语言。前面举的例子是从现金流出发，通过四色建模法完成建模，围绕 KPI 的建模流程也大同小异。可能很快你就有疑问了，业务脊梁的建模一般都是公司 CTO 干的事，自己日常工作中可能只做一个很小的子域，四色建模如何来指导日常领域建模呢？我的建议是，没有现金流或者没有明确 Kpi 指标的系统，没有必要强行使用四色建模法，不过借鉴四色建模法中的权责追溯，事件推导的思想对日常的建模还是大有裨益的。落地实践至此，理论部分就介绍完了，从本节开始，将介绍"RabbitAdvisors"的落地实践。方案设计领域模型从前面的四色建模法中，我们已经得到了领域模型，稍加整理后得到下图：读者、编辑以及作者都可以看做是用户，但是三者在系统中所从事的动作，拥有的能力差异巨大，用户的共性仅体现在登录等极少数场景，因此本文建模更倾向于将“读者”、“编辑”以及“作者”视为三个独立的模型。下文中将不再出现“用户”。子域划分针对上面的领域模型进行子域划分，子域划分时尽量要体现出聚合根，因此子域往往只包含一到两个领域模型，子域中的实体会以值对象的方式聚合其他子域的实体。子域划分如下图：根据子域对于业务重要性的不同，可以分为核心域、支撑域和通用域：核心域包括：订单域、订阅域支撑域：专栏域、专栏报价域、金融域、签约域、佣金域通用域：读者域、小二域、作者域为了行文简洁，还有一些其他必须的支撑域和通用域没有画出来，比如消息通知域、公告域、评论域等。限界上下文划分限界上下文中划分的一个技巧就是考虑一个完整的业务流程，保证这个业务流程所涉及的领域都在一个限界上下文中，例如“专栏订阅上下文”中包含了用户订阅这个业务流程的关键领域对象。限界上下文是指导微服务系统拆分的依据，上图中根据限界上下文的划分，需要拆分成 5 个微服务系统，分别是 专栏订阅系统、专栏信息系统、签约分佣系统、金融系统以及用户信息系统。到这里，顶层的架构设计已经完成了，也回答了文章开头的提问：假如你是"RabbitTech"的 CTO，你将会怎样来对"RabbitAdvisors"进行架构设计，又如何给团队分工呢？关于架构设计，领域建模部分前面已经给出，应用架构将会在“代码实施”一节介绍。给团队分工可以根据限界上下文，比如这里由五个小组分别负责每个限界上下文的内容。本节下文就是系分中的内容啦，这也是软件工程师们日常最熟悉的内容了。下面我们重点围绕“专栏订阅系统”来做具体实施。用例图用例图可以清晰的表示出系统的功能，例如针对专栏订阅系统，用例图如下：状态机逻辑清晰的状态机是刻画实体生命周期的关键，例如针对专栏订阅系统，状态机如下：活动图活动图也叫流程图，是用来展示具体的业务流程，可以描述清楚业务具体的处理逻辑，一般流程图采用泳道图的形式表达。例如订阅和取消订阅的流程图如下：时序图时序图描述了完成某个业务流程，系统中各个对象之间的交互过程和消息传递序列，时序图可以帮助厘清系统间的依赖和调用。例如订阅的时序图如下：ER 图ER 图是描述的数据库建模，可用于直接指导数据库建表。对于习惯面向数据库模型编程的工程师来说，可能画 ER 图是需求研发的第一步，但是在领域驱动设计中，ER 图只是仓储实施细节，只会在领域建模完成后才进行 ER 设计。例如专栏订阅域中 ER 图如下：代码实施终于到了编码的环节了。应用架构上文在“限界上下文划分”一节中建议分成 5 个微服务。为了尽可能演示出分布式业务系统，但同时又不至于让示例工程太复杂，在演示工程中就只分为两个应用，本文重点演示“专栏订阅”中的实践，将其他限界上下文统一放到了"smart-classroom-misc"系统中。应用模块分层在每个微服务应用分层有多种方法，一个核心原则就是“高内聚，低耦合”，本文参考 COLA 架构并结合实际工作经验，总结了如下的应用分层：基本思想为了保证核心业务逻辑的稳定，领域层应作为最纯粹、最少对外依赖的层次，只包含业务知识和业务规则，不该过多关心技术细节的实现（如存储、消息等）。分层以领域层为核心来分层建设。模块职责测试套件（testsuite）：测试模块，所有单测和集成测试写在该模块。它通过直接和间接依赖，可以访问到每个模块的代码，也即所有模块对测试层都是可见的。如果项目中有类似 AliGenerator 的代码自动生成工具，也应该放在该模块中。主程序（main）：应用的启动入口，包含启动相关的配置。接口门面层（facade）：该模块是对外发布的 API 包，对外暴露的 SOA/RPC 接口放在这里，外部应用使用我们的服务会依赖该包。一个应用可能会按照用途不同，设计多个 facade 模块。接口门面实现层（facade-impl）：实现了用户界面层（facade）中的接口，主要负责向用户显示信息和解释用户指令。处理输入的解析、验证、转换，还有输出的序列化。如果应用有多个 facade 模块，可以使用一个 facade-impl 来实现，也可以是多个。控制器层（controller）：可选。如果应用直接对外提供 http 服务，那么 http 的接口入口就在该层中，例如 spring-boot 中的 Controller 都放在这一层。控制器层虽然依赖于 facade-impl 层，但是只能调用 facade 中声明的方法，这样可以防止业务逻辑从 facade-impl 层泄露到 controller 层中。一般说来，http 服务由专门的网关应用对外提供，微服务系统中这一层不需要。应用层（application）：定义要完成的业务功能，由该层负责协调和编排领域层的原子方法。因此应用层主要负责：事务控制查询仓储（注意：只能查询仓储，不能写仓储，写仓储是领域层的能力）领域事件（domain event）的触发和监听操作日志安全认证注意一点，应用层虚线依赖仓储层和基础设施层，应用层要调用仓储查询或者使用中间件都应该使用领域层声明的接口，不能够直接使用仓储层和基础设施层的实现，例如应用层中不能直接使用 Mapper，而应该使用 Repository 接口。虚线依赖是因为仓储层和基础设施层必须挂到主程序这个根上，整个项目模块才能被组织起来。领域层（domain）：表达业务概念、业务状态、及业务规则。一个聚合（aggregate）一个 package。领域层负责以下内容：实体（entity）值对象（value object）领域服务（domain service）领域事件（domain event）仓储（repository）接口定义，读写仓储依赖的外部服务（anti-corruption layer）的接口定义工厂（factory）结合团队以及兄弟团队的实践，建议实体采用贫血模式，实体和领域服务共同构成领域模型。仓储层（repository）：仓储层负责数据查询及持久化，仓储层本质上也属于一种基础设施，但是仓储层作为系统中重要的一环，因此从基础设施层中独立开来。DO 对象只存在于仓储层，通过内部定义的 Converter 转为领域对象后供上层使用。基础设施层（infrastructure）：倒置依赖领域层，负责 RPC 服务以及中间件服务的调用和具体实现。经典 DDD 分层中依赖的外部服务的防腐层（anti-corruption layer）就在这里。工具层（utility）：和业务无关的工具方法都放在这里，比如 Utils 类，全局通用 Exception 定义等。utility 会被整个项目其他模块直接或间接依赖，必须保证其无业务语义。编码规范这里给出的编码规范是我基于团队实践总结出来的，不同团队可以有自己的规范，这里供参考：POJO 规范【强制】POJO（Plain Ordinary Java Object）简单的 Java 对象，区别于 Spring Bean。POJO 的对象后缀做如下约定。DO 数据库模型：XXDO 代表的是数据库模型，其中的字段就是数据库表字段的平铺格式。Model 领域模型：XXModel 代表某个领域模型，这是我们的逻辑核心。DTO 外部传输对象：XXDTO 是对外的数据传输对象，一般 DTO 是在 facade 层定义的，在 facade-impl 层完成转换。Info 内部传输对象：XXInfo 是在 application 层和 domain 层之间的数据传输对象，和 DTO 的功能类似，区别在于 DTO 对外，Info 对内。VO 值对象：XXVO 代表了值对象，VO 是 Value Object 简写，通过值对象可以接收其他系统 facade 传递过来的 DTO。注意 VO 不是 View Object(视图对象)，在项目中没有视图对象，因为是前后端分离，没有类似于 jsp 这种视图，另外 DTO 已经完全承载了和前端或者其他服务交互的职能，不再需要视图对象。Query 查询对象：XXQuery 是仓储接口接收的查询参数封装的对象，一般仓储查询包含 3 个及以上的参数就应该封装。XXQuery 和仓储接口都是定义在领域层，在仓储层进行实现的。Request 请求对象：XXRequest 是 facade 层中定义的查询对象，一般一个 facade 接口包含 3 个或者以上的参数就应该使用 Request 进行封装。Converter 转换器：各种类型的对象涉及到大量的转换，Converter 结尾的类就是转换器，XXAA2BBConverter 就代表了 XX 这个实体的 AA 对象类型转成 BB 对象类型。转换器放在对应的分层中，例如 Model2DOConverter 和 DO2ModelConverter 就放在仓储层，Model2DTOConverter 就放在 facade-impl 层中。注意：Converter 建议逐个字段手写转换，不建议使用 BeanUtils.copyProperties 或者 MapStruct 对象转换工具，理由是使用了这类工具后，字段发生变化没法在编译阶段感知到，容易导致生产事故，所以推荐转换就使用笨方法，逐个字段转换。Bean 规范【强制】应用采用了多层的结构，如果没有统一的命名规约，势必容易造成名字冲突的情况，这里对于 Spring Bean 做如下命名约定。Controller 控制器：对外提供 http 服务的控制器，只存在于控制器层。Facade 门面服务：通过门面的方式对外提供的服务接口一般直接以 Facade 结尾，可以细化为 XXWriteFacade: 操作服务 和 XXReadFacade: 查询服务。Facade 对应的实现命名为 XXFacadeImpl。FacadeImpl 门面服务实现：Facade 门面服务对应的实现，XXFacadeImpl 位于接口门面实现层 (facade-impl)。一般会将 FacadeImpl 发布成 RPC 服务。AppService 应用层服务：应用层的服务以 AppService 结尾，可继续细化为 XXWriteAppService: 操作服务 和 XXReadAppService: 查询服务。DomainService 领域服务：领域服务均以 DomainService 结尾。Repository 仓储接口：仓储接口定义在领域层，其实现在仓储层，实现文件命名为 XXRepositoryImpl。RepositoryImpl 仓储接口实现：仓储接口实现在仓储层，这是倒置依赖的体现。Mapper 数据库查询接口：这是 MyBatis 的查询接口，是应用最底层的数据库操作文件，只允许在仓储层调用。每个 XXMapper 都会继承其基类 XXBaseMapper，在基类中定义了基本的增删改查方法。Client 中间件服务依赖接口：通过定义 Client 接口来消费中间件服务，Client 定义在领域层，其对应的实现为 ClientImpl。这是防腐的设计思想。ClientImpl 中间件服务依赖实现：这是对领域层中声明的依赖的实现，ClientImpl 都是在基础设施层。FacadeClient 外部 RPC 服务依赖接口：通过定义 FacadeClient 接口来消费 RPC 服务，FacadeClient 定义在领域层，其对应的实现为 FacadeClientImpl。这是防腐层的设计思想。FacadeClientImpl 外部 RPC 服务依赖实现：这是对领域层中声明的依赖的实现，FacadeClientImpl 都是在基础设施层。Configuration 配置：XXConfiguration 是应用中的配置类，各层负责自己层的配置，例如 DataSourceConfiguration 关于数据源的配置，放到仓储层；WebConfiguration 负责 http 的 json 配置，就放到控制器层。各种类型的对象及 Bean 所处位置如下图所示：方法命名【建议】Service/Repository/Client/DAO 方法命名规约获取单个对象的方法用 query 作前缀。获取多个对象列表的方法用 list 作前缀，复数结尾，如：listObjects。获取多个对象分页的方法用 page 作前缀，复数结尾，如：pageObjects。获取统计值的方法用 count 作前缀。插入的方法用 insert 作前缀。删除的方法用 delete 作前缀。修改的方法用 update 作前缀。更多编码规范项目中更多的编码规范请参考 《阿里巴巴开发规约》。编码规范多说两句上面约定的模块分层和编码规范可能会给你一种比较冗长的感觉，光数据从数据库传输到 http 层都需要经过多个 Bean 以及多次 POJO 的转换，是不是没有必要这么麻烦呢？其实不然，这一套应用模块分层是面向团队协作设计的，如果仅仅是做个人小项目完全没有必要使用这一套分层框架，MVC 模式就够了。这一套设计尽量贴合了领域驱动设计，每一层都有自己特定的职责，这样可以应对复杂的项目，用规范约束了编码风格，在一定程度上统一了团队的研发姿势，可以做到在团队中各司其职，严格的规范保证了团队代码质量的下限。Saber 工具Saber 工具位于 testsuits 中，它的作用是根据数据库表的元信息，自动生成 Mapper 和 DO 文件，省去逐字段手写的麻烦。如果增减字段，也可以重跑 Saber 工具就实现了字段和数据库一致。Saber 工具类似于 Aligenerator。操作方法在 SaberConfig 文件中修改一些配置，主要包括表前缀，包名，jdbc 信息等。在 x 文件夹下，写一个 XXCommand 的类，该类包含 main 函数，可以执行。在该类中指定表名，分页查询字段，枚举映射关系，忽略字段等配置。运行上面写的 XXCommand 类，这时候就能在项目下看到自动生成的 XXBaseMapper.xml, XXMapper.xml, XXBaseMapper.java, XXMapper.java, XXDO.java 五个文件。进阶用法如果你希望根据数据库表字段信息生成前端文件或者数仓 SQL 文件，你可以自定义一个 BlaBlaArtwork，写好对应的模板文件 TemplateBlaBla.vm，将 BlaBlaArtwork 注册到 SaberHelper 中即可。这个时候再运行上面的 XXCommand 就能够得到 BlaBlaArtwork 中定义的产物了。项目演示后端微服务 1：https://github.com/eyebluecn/smart-classroom-misc后端微服务 2：https://github.com/eyebluecn/smart-classroom-subscription前端项目：https://github.com/eyebluecn/smart-classroom-front在线演示地址：https://classroom.eyeblue.cn/演示项目中可以用读者和小编两种身份登录，小编可以创建专栏，读者可以订阅专栏和查看已订阅的专栏。演示项目分为两个后端应用，smart-class-subscription 是“专栏订阅系统”，完全按照微服务应用建设。smart-class-subscription 就是所有其他微服务的合集，同时还包含一个控制层，用于提供 http 服务。后端应用技术栈为：SpringBoot, MyBatis, MySQL, Dubbo, Nacos, RocketMQ, Maven。前端项目技术栈为：React, TypeScript, ReactRouter, Antd, Less, Vite。后记我认为领域驱动设计是一种软件工程的思想，它不是一套模板，它的思想精髓值得软件工程师以及架构师们领会，即：直接面向业务进行领域建模，将业务知识沉淀到领域模型中。业务知识的沉淀不是一蹴而就，应该反复提炼，持续演进；为了让演进提炼的过程高效顺畅，团队使用统一语言来沟通、描述需求和设计方案。高内聚、低耦合是应对软件复杂度的不二法则。领域、子域、限界上下文、聚合都是为这条宗旨服务的工具。领域驱动设计也不是银弹，在软件开发过程中没有必要完全 DDD 一把梭，对于一些不复杂的项目，使用 MVC 模式开发反而更简单高效。同时业务本身的复杂度不是依靠某种软件设计思想或者设计范式就能规避的，DDD 只是架构师们在架构设计过程中的一种指导思想，它本质上是一种工具。深刻理解业务，洞察问题本质才是一个架构师最核心的能力体现。当然，如果本文能够带你了解甚至是在工作中实践领域驱动设计，那本文的目的也就达到了。参考引用Eric Evans《领域驱动设计》Vaughn Vernon《实现领域驱动设计》徐昊《如何落地业务建模》Erich Gamma《设计模式：可复用面向对象软件的基础》EventStorming迄今为止最完整的 DDD 实践聊一聊，我对 DDD 的关键理解事件风暴与领域建模在阿里的实践阿里技术专家详解 DDD 系列 第一讲 - Domain Primitive限界上下文：冲破微服务设计困局的利器 - 阿里云开发者社区复杂度应对之道 - COLA 应用架构 cola 扩展点张建飞（Frank）的博客-CSDN 博客从壹开始微服务 [ DDD ] 之三 ║ 简单说说：领域、子域、限界上下文 - 老张的哲学 - 博客园领域驱动设计详解：是什么、为什么、怎么做？领域驱动设计 (DDD)-基础思想阿里巴巴开发规约说明：本文涉及的技术栈（如 SpringBoot、MyBatis 等）为通用主流版本，具体实现细节可能随版本迭代有所调整，建议结合实际项目环境参考使用。
揭秘“山姆黄牛”背后的技术逻辑：用Java实现会员管理系统的防黄牛策略
揭秘“山姆黄牛”背后的技术逻辑：用 Java 实现会员管理系统的防黄牛策略在浙江绍兴的山姆超市外，“黄牛”现象曾引发广泛关注。这些“黄牛”通过提供带入和结账服务，让未办理会员卡的消费者也能进入超市购物。这一行为不仅扰乱了市场秩序，也对山姆会员商店的会员管理系统提出了严峻挑战。今天，我们就来探讨一下，如何用 Java 实现一个更为健壮的会员管理系统，从技术层面有效防止“黄牛”现象的发生。一、问题背景与需求分析山姆会员商店的会员制度是其核心竞争力之一，原则上会员需要通过会员卡才能进入超市购物。然而，“黄牛”利用系统漏洞，通过多次带人进入和结账，从中牟利。为了打击这种行为，我们需要对会员管理系统进行升级，使其具备以下核心功能：会员身份验证（Member Validation）：确保只有合法会员才能进入超市。消费频率监控（Consumption Monitoring）：对会员的消费频率进行实时监控，及时发现异常消费行为。黑名单管理（Blacklist Management）：将确认的违规“黄牛”会员加入黑名单，禁止其再次进入超市。二、系统设计为了实现上述功能，我们可以设计一个基于 Java 的会员管理系统。系统主要包含以下三个核心模块：会员验证模块：负责验证会员身份的真实性。消费监控模块：负责监控会员的消费频率，识别异常模式。黑名单管理模块：负责黑名单的添加、查询和删除操作。三、系统实现1. 会员验证模块会员验证模块主要通过会员卡号和密码进行身份验证。为了演示方便，我们使用 Java 中的 HashMap 来存储会员信息，其中键为会员卡号，值为会员密码。在实际生产环境中，这些数据应从数据库中读取。import java.util.HashMap; import java.util.Map; public class MemberValidator { private Map<String, String> members; public MemberValidator() { members = new HashMap<>(); // 初始化会员信息，这里以硬编码为例，实际应用中应从数据库读取 members.put("123456", "password123"); members.put("654321", "password321"); } public boolean validateMember(String cardNumber, String password) { return members.containsKey(cardNumber) && members.get(cardNumber).equals(password); } }2. 消费监控模块消费监控模块主要通过记录会员的消费时间和次数，来监控会员的消费频率。我们使用 HashMap 存储会员的消费记录，键为会员卡号，值为消费时间列表。当单位时间内的消费次数超过阈值时，系统将判定为异常。import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class ConsumptionMonitor { private Map<String, List<Long>> consumptionRecords; private static final int THRESHOLD = 5; // 设定消费频率阈值，例如 5 次 private static final long INTERVAL = 3600 * 1000; // 设定时间间隔，例如 1 小时 public ConsumptionMonitor() { consumptionRecords = new HashMap<>(); } /** * 记录消费并检查是否异常 * @return 如果消费行为正常返回 true，否则返回 false */ public boolean recordConsumption(String cardNumber) { long currentTime = System.currentTimeMillis(); consumptionRecords.putIfAbsent(cardNumber, new ArrayList<>()); List<Long> record = consumptionRecords.get(cardNumber); // 清理过时记录 record.removeIf(time -> currentTime - time > INTERVAL); // 记录当前消费时间 record.add(currentTime); // 检查是否超过阈值 if (record.size() > THRESHOLD) { System.out.println("Warning: Member " + cardNumber + " has exceeded the consumption threshold!"); return false; // 超过阈值，禁止此次操作 } return true; } }3. 黑名单管理模块黑名单管理模块主要负责黑名单的维护操作。我们使用 HashSet 来存储黑名单中的会员卡号，以保证查询效率。import java.util.HashSet; import java.util.Set; public class BlacklistManager { private Set<String> blacklist; public BlacklistManager() { blacklist = new HashSet<>(); } public void addToBlacklist(String cardNumber) { blacklist.add(cardNumber); } public boolean isInBlacklist(String cardNumber) { return blacklist.contains(cardNumber); } public void removeFromBlacklist(String cardNumber) { blacklist.remove(cardNumber); } }4. 系统整合与测试最后，我们将上述模块整合到一个系统中，并进行模拟测试。系统会在验证身份后检查消费频率，若发现异常则拒绝进入，并可进一步将其加入黑名单。public class MemberManagementSystem { private MemberValidator validator; private ConsumptionMonitor monitor; private BlacklistManager blacklistManager; public MemberManagementSystem() { validator = new MemberValidator(); monitor = new ConsumptionMonitor(); blacklistManager = new BlacklistManager(); } public boolean checkMemberEntry(String cardNumber, String password) { // 1. 检查黑名单 if (blacklistManager.isInBlacklist(cardNumber)) { System.out.println("Member " + cardNumber + " is in the blacklist, access denied!"); return false; } // 2. 验证身份 if (validator.validateMember(cardNumber, password)) { // 3. 记录消费并监控频率 return monitor.recordConsumption(cardNumber); } else { System.out.println("Invalid member credentials, access denied!"); return false; } } public static void main(String[] args) { MemberManagementSystem system = new MemberManagementSystem(); // 模拟会员进入超市 String cardNumber = "123456"; String password = "password123"; // 模拟多次进入，触发阈值 for (int i = 0; i < 6; i++) { boolean allowed = system.checkMemberEntry(cardNumber, password); if (!allowed) { System.out.println("Access denied at attempt " + (i + 1) + ". Adding to blacklist..."); // 将异常会员加入黑名单 system.blacklistManager.addToBlacklist(cardNumber); break; } } // 再次尝试进入超市（此时应在黑名单中） System.out.println("--- Trying to enter again ---"); boolean result = system.checkMemberEntry(cardNumber, password); System.out.println("Member " + cardNumber + " access result: " + result); } }四、总结与展望通过上述设计和实现，我们构建了一个基础的会员管理系统原型，该系统能够通过身份验证、频率监控和黑名单机制，有效防止“黄牛”现象的发生。当然，这只是一个基础版本。实际生产应用中还需要考虑更多的因素，例如：数据持久化：与数据库（如 MySQL）或缓存（如 Redis）集成，避免内存数据丢失。并发处理：在高并发场景下保证数据的一致性和线程安全。系统安全性：加强密码加密、防止接口滥用等。同时，我们也可以利用大数据和机器学习技术，对会员的消费行为进行更深入的分析和预测，从而进一步提高系统的准确性和可靠性。系统架构示意图：+----------------------+ | 会员管理系统 | +----------------------+ | 1. 会员验证模块 | | - 验证会员身份 | +----------------------+ | 2. 消费监控模块 | | - 监控消费频率 | | - 异常消费警告 | +----------------------+ | 3. 黑名单管理模块 | | - 添加黑名单 | | - 查询黑名单 | | - 删除黑名单 | +----------------------+希望这篇文章能够帮助你更好地理解如何用 Java 实现会员管理系统的防黄牛策略，并为你的项目开发提供灵感。说明：本文代码示例主要用于演示技术逻辑，采用内存存储（HashMap/HashSet），适用于学习参考。实际生产环境请结合数据库、分布式缓存及安全框架进行开发。
项目开发全流程实战：经验、挑战与应对策略
一、项目开发流程解析在当今数字化浪潮中，项目开发是一场充满挑战与机遇的征程。成功的项目交付不仅依赖精湛的技术，更离不开科学的管理和高效的协作。以下将深入探讨项目开发过程中的关键环节，分享实战经验与应对策略，助力项目团队提升交付质量与成功率。（一）签订合同：明确项目边界合同条款的关键意义在项目启动初期，合同如同项目的“宪法”，明确规定了项目的范围、功能以及各方的权利和义务。一份清晰、详细的合同是项目顺利推进的基石，能够有效避免后期的诸多纠纷和风险。案例警示：在某软件项目中，由于合同对功能范围界定模糊，导致开发过程中客户不断提出新需求，项目范围无序扩大（Scope Creep），而预算未相应增加，最终导致进度严重延误、成本大幅超支。应对策略与技巧明确边界：签订合同时，务必详细描述系统的边界和功能范围，避免使用模糊措辞。可采用列表、图表等直观方式，清晰呈现项目涵盖的具体功能模块及不包含的内容（Out of Scope）。变更管理：针对需求变更，制定明确的变更管理条款，规定不同级别变更的处理流程及费用调整机制。例如，小型变更在一定工作量范围内可由团队消化但需告知客户；重大变更则需重新评估计划、成本和时间，并获得客户书面确认。方案同步：建议将解决方案与合同一并签署，使客户对项目的实现方式有清晰预期，减少后期因理解差异导致的需求变更。（二）团队建设：打造高效协作团队项目经理的核心作用项目经理（Project Manager, PM）是项目团队的核心，其综合能力对项目成败起着至关重要的作用。优秀的 PM 不仅需具备扎实的专业知识和项目管理经验，更要善于沟通协调、激励团队成员，引领团队朝着目标前进。在项目立项后，应尽早确定项目经理，并赋予其足够的权力和资源，以便其能够有效地组织和管理项目团队。团队成员的选择与协作客户协同：尽力争取让客户的相关人员加入项目团队作为统一联系人，避免多头指挥导致的混乱。同时，确保客户方内部在与项目组沟通前达成一致意见，减少因客户内部矛盾给项目带来的干扰。优势互补：项目经理应根据项目特点和需求，精心挑选团队成员。例如在电商项目中，需配备熟悉业务流程的业务专家、擅长架构设计的技术高手、精通前后端开发的程序员以及严谨负责的测试人员，组成优势互补的团队。氛围营造：注重团队成员之间的沟通与协作，定期组织团队会议、技术交流活动等，促进信息共享和知识传递，营造良好的团队氛围。（三）需求调研：精准把握客户需求需求调研的重要性与常见问题需求调研是项目开发的源头，其质量直接影响项目的方向和成果。然而在实际项目中，常因对客户组织架构、业务流程和人员需求了解不足，导致需求不完整、不准确，后期频繁变更，给项目带来巨大风险。案例警示：某企业资源规划（ERP）项目，由于前期需求调研不深入，未充分考虑不同部门之间的业务协同需求，开发出的系统无法满足企业整体运营管理要求，不得不进行大规模返工，造成时间和成本的严重浪费。改进措施与方法干系人分析：全面了解项目干系人，绘制组织结构图，明确各方关系和职责，确保需求调研覆盖所有相关人员。与建设单位的业务管理部门和实际使用者进行深入交流，收集原始资料。流程文档化：详细描述业务流程，对现有系统进行深入调研，分析其优缺点和用户操作习惯。将业务流程文档化，与客户反复讨论、确认，确保准确性和完整性。可视化调研：采用可视化需求调研方法，根据不同客户群体提供相应的图表工具，如系统总体框架图、业务流程图、数据流图、用户界面图等，引导客户挖掘潜在需求。规格说明书：强调需求文档的重要性，《功能规格说明书》应具备完整性、正确性、可行性、必要性、优先级明确、无二义性、可验证性、一致性、可修改性和可跟踪性等特点，为项目验收提供有力依据。（四）架构设计：确保系统稳健性与可扩展性架构设计的挑战与风险架构设计是项目的技术核心，直接关系到系统的性能、可维护性和可扩展性。若缺乏充分的沟通和协作，容易导致设计偏离需求，后期改动困难，甚至影响项目进度和质量。案例警示：在某大型分布式系统开发中，由于架构师未与开发人员充分沟通，对系统性能需求估计不足，导致系统在高并发情况下频繁出现卡顿和崩溃，不得不重新优化架构，耗费大量时间和精力。全员设计理念与实践责任共担：全员设计理念强调将详细设计任务合理分解，让更多团队成员参与其中，共同承担设计责任，从而发挥专业优势，提高设计质量。任务分解：技术主管根据需求特点，将其分解为多个设计任务分配给合适的程序员。每个设计者在透彻理解需求的基础上，结合系统架构和产品现状，撰写详细设计文档。交叉审阅：设计文档完成后，通过交叉审阅机制，邀请其他程序员提出意见和建议。设计者汇总反馈并修正设计，经过多轮迭代直至文档达到最优状态，再交付开发。（五）沟通：项目成功的桥梁沟通在项目中的关键作用有效的沟通是确保团队协作顺畅、客户需求准确理解和项目目标达成的关键。无论是团队内部成员之间，还是与客户之间，信息的及时传递和共享都至关重要。案例警示：在某移动端应用开发项目中，由于开发团队与设计团队之间沟通不畅，导致界面设计与功能实现脱节，用户体验不佳，需反复修改，严重影响项目进度。沟通策略与方法定期机制：建立定期的沟通机制，如项目例会、站会（Stand-up Meeting）等，及时通报项目进展、问题和解决方案。多样化工具：采用多样化的沟通工具，根据信息的紧急程度和重要性选择合适的沟通方式，如面对面沟通、即时通讯工具、电子邮件、项目管理软件等。客户反馈：注重与客户的沟通，及时反馈项目进展情况，获取客户的反馈和意见。在需求变更时，与客户充分沟通变更影响，共同寻求最佳解决方案。（六）计划执行控制：确保项目按计划推进项目计划的制定与监控制定详细的项目计划是项目成功的重要保障，计划应包括任务分解（WBS）、时间安排、资源分配和里程碑设定等内容。在项目执行过程中，密切跟踪任务进度，及时发现和解决偏差。采用项目管理工具，如甘特图（Gantt Chart）、看板（Kanban）等，可视化展示项目进度。应对计划变更的策略项目执行过程中难免会遇到需求变更、技术难题、资源调整等因素导致计划变更。当变更发生时，要及时评估对项目进度、成本和质量的影响，制定相应的调整措施。建立变更管理流程，对变更请求进行严格审批和控制。同时，及时更新项目计划和相关文档，通知项目团队成员和利益相关者，保持信息的一致性。（七）风险管理：提前识别与应对风险风险识别与评估风险管理贯穿项目开发的全过程。在项目启动阶段，要全面识别可能影响项目成功的风险因素，如技术风险、市场风险、人力资源风险、需求变更风险等。对识别出的风险进行评估，分析其发生的可能性和影响程度，确定风险的优先级。对于高可能性和高影响程度的风险，要重点关注并制定详细的应对策略。风险应对措施针对性策略：对于技术风险，可采取技术预研、备用方案准备等方式降低风险；对于人力资源风险，提前做好人员培训和储备计划；对于需求变更风险，加强需求管理和变更控制流程。监控机制：建立风险监控机制，定期对风险状态进行评估和更新，及时调整应对策略，确保风险始终处于可控范围内。（八）测试：保障项目质量的最后防线测试工作的重要性与现状测试是项目质量保障的关键环节，通过全面、系统的测试，可以发现并修复软件中的缺陷，提高系统的稳定性和可靠性。然而在国内许多中小公司，测试工作往往得不到足够的重视，导致项目上线后问题频发。案例警示：某在线教育平台上线后，由于测试不充分，出现了课程播放卡顿、用户数据丢失等严重问题，影响了用户体验和平台声誉。测试策略与方法测试计划：制定完善的测试计划，明确测试目标、范围、方法和进度安排。根据项目特点选择合适的测试类型，如功能测试、性能测试、安全测试、兼容性测试等。团队与工具：建立专业的测试团队或培养测试人员，提高专业素质。采用自动化测试工具，提高测试效率和准确性，降低测试成本。过程管理：加强测试过程管理，精心设计和管理测试用例，及时记录和跟踪缺陷。在项目上线前，进行充分的用户验收测试（UAT），确保系统满足用户需求。（九）验收为目的的思想：确保项目交付成果符合要求以验收为导向的项目管理在项目开发过程中，始终牢记验收目标，确保每个任务的交付成果都能够满足验收标准。从项目计划制定到任务执行，都要考虑如何进行检查和验证。量化指标：例如在企业办公自动化系统开发中，对于界面设计要求“美观大方”，可细化为具体的界面布局规范、颜色搭配标准、操作流程便捷性等可量化的指标，便于检查和评估。验收标准的明确与沟通在项目启动阶段，与客户共同明确验收标准和流程，并将其纳入项目合同和文档中。在项目执行过程中，定期与客户沟通，确保双方对验收标准的理解一致。对于难以量化的验收要求，要通过案例演示、用户反馈等方式进行确认。在项目收尾阶段，严格按照验收标准进行检查和测试，确保项目顺利通过验收。二、总结与展望项目开发是一个复杂而系统的工程，涉及多个环节和众多因素。通过对项目开发过程中各个关键环节的深入剖析，我们分享了宝贵的经验和应对策略，希望能够帮助项目团队在实际工作中少走弯路，提高项目成功率。在未来的项目开发中，建议不断优化管理流程、加强团队协作、提升技术水平，以更加科学、高效的方式推动项目前进，为客户交付高质量、符合需求的项目成果。说明：本文所述项目管理流程与策略基于通用软件工程实践，适用于传统瀑布模型及敏捷混合模式。具体实施时，请结合团队规模、技术栈及业务场景灵活调整。
编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。前言许久未进行技术分享，现将曾经在老新浪论坛中使用过的架构技术进行改进和整理，总结成文。欢迎交流指正。1. 为什么分层？计算机领域的体系结构普遍采用分层设计。从整体结构来看，从最底层的硬件往高层依次有：操作系统驱动程序运行库系统程序应用程序从网络分层模型 OSI 来讲，由上至下为：应用层表示层会话层传输层网络层数据链路层物理层当然，实际应用的 TCP/IP 协议分层并没有 OSI 标准这么复杂。从 C 语言文件编写到生成可执行文件的过程来看：预处理：展开后的 C 语言代码。编译：成汇编语言（特定 CPU 体系结构的汇编语言源文件）。汇编：生成目标文件（CPU 可执行的二进制指令机器码）。链接：连接目标文件生成可执行文件（操作系统可以加载执行的二进制文件）。这虽不算是软件的分层结构，但可以理解为一种通过分层来简化复杂问题的思想。那么 PHP 语言可以认为是建立在 C 语言之上的层——其解释器 Zend 引擎是用 C 语言实现。毕竟用 PHP 这样的脚本语言编写动态网页要比用 C 语言方便得多。当然，还有我们最熟悉的 MVC 分层技术，后面会做详细介绍。分层的好处想必大家都已经比较熟悉，这是一种“分而治之，大而化小”的思想。说到分层就不得不提模块，其实分层和模块是从两种维度来进行“分而治之”的方式：模块：是从横向维度将一个整体分割成若干个独立的部分，每个部分行使自己的职责，当然它们之间也可能有依赖关系，这通过其对外提供的服务来实现。如果将整个系统比做中国版图，那么模块方式将中国分为省、自治区等。分层：则是从纵向维度将一个整体从高至低划分为若干个独立的层，一个完整的服务由底层至上层，层层传递最终产出。分层和模块可以同时运用。例如中国用模块方式分为省之后，每个省的行政机构利用分层方式来行使职责，从低到高有户、村、乡、区、市、省等等，每一层都向上一层汇报。分层和模块会提高系统复杂度并影响效率（户不能直接向省汇报，而需要一级一级向上汇报），但是这样有利于系统的扩展和维护。每一层只需要关注自己提供的服务接口以及它下一层所提供的服务接口。试想一下，如果省需要接受来自市、区、乡、村等所有下级层的汇报，那些省干部会很头疼的。2. HTTP 服务传统的三层架构 MVCHTTP 服务中最经典的分层架构非 MVC 莫属了，几乎任何一个 PHP 开发框架都是支持 MVC 分层模式。此模式历史也比较悠久，是在上个世纪八十年代为编程语言 Smalltalk 设计的软件模式，至今已经被广泛引用。这里引用了百度百科中的图片：关于 MVC 的优点我不做介绍，搜索引擎中能找到大量相关资料。本文的标题是《HTTP 服务七层架构技术探讨》，比 MVC 多出了四层，这样复杂的分层是否有必要呢？关于这个问题可谓仁者见仁，智者见智。本人认为 MVC 分层粒度不够精细，当然你也可以继续坚持传统的三层，那么后文你也没必要看下去了。那么为什么 MVC 分层不够精细呢？在我曾经使用开源框架的 MVC 模式的经验中发现，V 和 C 层功能职责一般都很清晰稳定，但是 M 层却常常显得臃肿笨拙。C 层（Controller）：主要是负责整体流程控制。一般规范的架构中，流程都可以用一张或几张流程图画出，表明流程一般都是固定的。V 层（View）：主要是负责页面呈现。可能使用 Smarty 模板引擎，也可能是自带的模板引擎，显示的页面可能是 HTML、XML 或者 JSON。这些种类再多也都是可以度量的，所以 V 层也可以说是固定的。M 层（Model）：却关系到系统的业务逻辑。随着系统不断迭代更新，M 层中的内容也会不断演变。而这一层中也有很多复杂的处理，如文件读取、数据库查询、缓存策略、逻辑运算、数据加工、数据打包等等。所以 MVC 三层模型中，M 层是还能再做细分的。当 M 层有一个更精细合理的分层方式之后，我们的业务逻辑演变过程会更加的得心应手。3. 七层架构由上面的介绍，我们对 MVC 中的 M 层再进行分层规划。我这里给出的是一种对 M 层分五层的方式，读者如果觉得五层太多或者太少，可以参考这个再进行规划。原来的 M 层被分为：A 层：Application（应用层）B 层：Business（业务层）C 层：Component（组件层）D 层：Datadriver（数据驱动层）S 层：Systemdriver（系统驱动层）那么整个七层架构则为：ControllerViewApplicationBusinessComponentDatadriverSystemdriver结构图还是参考经典 MVC，将其中 M 层换成新的五层即可。现在依次介绍这几个新的层：3.1 Application（应用层）应用层在最上面，其针对实际中的单个页面或者单个接口。Controller 通过 HTTP 请求地址中的参数找到对应的 Application，然后执行中指定的公共方法，比如 main()，然后应用就开始启动。应用层的职责包括接受 HTTP 参数（一般是间接接受，比如从 request 对象中获取），调用 Business 层的特定业务，保存业务执行结果。这些结果最终会由 View 显示出来，当然是通过 Controller 协调。应用层是 M 层分解成五层之后最高的层，Controller 会与此层直接通信。3.2 Business（业务层）业务层在应用层之下，通常一个应用实例对应一个业务实例，而一个业务有可能为多个应用服务。业务是一个执行流，它通过执行一系列的操作来完成应用的需求。这些操作来自下层的组件层 Component，可能一个业务需要一个或者多个组件来完成一个完整的需求。因为一个业务实例通常只对应一个功能，所以只有一个固定的方法会被上层的应用调用，比如 flow()。业务层的职责是帮应用层执行业务流并且有必要的时候返回数据给应用层，它会调用下层 Component 的方法。3.3 Component（组件层）从组件层开始和上面两层有一个本质的区别，组件层开始有了类库的概念。前面两层的实例通常只暴露一个特殊约定的公共的方法让上层调用，从这一层开始一个实例会提供多个方法给上层。组件层通常和系统中一个角色对应。例如在博客系统中，博文是一个角色，用户是一个角色，那么就会有博文组件 BlogComponent，用户组件 UserComponent。每个角色都有对应的操作，例如博文和用户都可以添加、删除、修改。需要注意组件层中不应该有任何数据读取的操作，数据读取是下层数据驱动层来做的。如果组件层从下层获取了数据，那么它的一个职责就是对数据进行加工。例如 BlogComponent 有一个方法是获取一个博文 getBlog($id)，那么 getBlog() 方法中，从数据驱动层中取得了对应 id 的博文数据之后，需要对博文数据进行一定的处理，比如将博文中的 HTML 特殊标签过滤等等。组件层不关心数据的读取方式，但是会关心数据的结果，比如数据不存在或者数据已经过期。3.4 Datadriver（数据驱动层）数据驱动层的职责是为组件层提供源数据，此层关心数据的存取介质、存取方式等等。数据可能被存储在 DB、MC、Filesystem 或者远程的 HTTP 服务器上。数据驱动层不关心数据的内容，只关心数据读取的操作结果。例如假设数据存在 DB 中，但是数据驱动层在执行数据库查询的时候出错了，那么需要在此层处理。假设数据存储在远程的 HTTP 服务器上，那么数据驱动层需要关心 HTTP 返回码是否为正确的 200 系列或者错误的 400、500 系列。哪怕 HTTP 请求返回了错误的数据实体，但是返回码为 200，那么数据驱动层也不关心，这种情况需要上层组件层来处理。3.5 Systemdriver（系统驱动层）系统驱动层是系统环境提供的数据访问实例。例如数据库服务的 Systemdriver 可能是一个 db handler，HTTP 服务的 Systemdriver 可能是一个 http handler，文件存储系统驱动层可能是一个 file handler。系统驱动层相对简单，这层可以和数据驱动层进行合并，其职责也较少，仅仅只是执行数据驱动层的数据访问指令。通常情况下这五个层中，上层的实例数量比下层的实例数量要多，整体类似一个倒置的梯形：在上图中一共有 6 个 Application，5 个 Business，4 个 Component，3 个 Datadriver，2 个 Systemdriver。每个 Application 都由一个 Business 为其服务。每个 Business 都服务一个或者多个 Application（B5 同时服务 A5、A6），都有一个或者多个 Component 为其服务。每个 Component 为一个或者多个 Business 服务，都有一个或者多个 Datadriver 为其服务。每个 Datadriver 为一个或者多个 Component 服务，都有一个或者多个 Systemdriver 为其服务。每个 Systemdriver 为一个或者多个 Datadriver 服务。4. 七层架构运用现在运用这样的架构来设计一个简单的博客系统，服务端用 PHP 语言实现。当然，架构是思想，不区分语言。整个系统包含以下功能：发布博文修改博文删除博文评论博文修改用户信息要求每个功能都记录操作日志。设计的数据存储包括：博文数据表用户数据表评论数据表日志（存文件系统）在表结构设计的时候我们加入了一些冗余字段信息。例如在博文表中有评论数量字段 comment_nums，博文每被评论一次其值加 1，每删除一个评论其值减 1。用户数据表中我们添加了用户发布的博文数量字段 blog_nums，用户每发布一篇博文其值加 1，每删除一篇博文其值减 1。下面设计分层：应用层：一共有 5 个应用PostBlogApplication：发布博文UpdateBlogApplication：修改博文DeleteBlogApplication：删除博文CommentBlogApplication：评论博文UpdateUserApplication：修改用户信息业务层：这 5 个应用分别有 5 个业务对其服务PostBlogBusiness：博文发布业务UpdateBlogBusiness：博文修改业务DeleteBlogBusiness：博文删除业务CommentBlogBusiness：博文评论业务UpdateUserBusiness：用户修改业务组件层：系统一共有 4 个角色对应 4 个组件BlogComponent（博文组件）postBlog()：发布博文deleteBlog()：删除博文updateBlog()：修改博文getBlog()：获取博文内容CommentComponent（评论组件）postComment()：发布评论deleteComment()：删除评论UserComponent（用户组件）updateUser()：修改用户信息LogComponent（日志组件）logMsg()：记录日志信息数据驱动层：和 4 个组件对应BlogDatadriver：DB 类型，提供 blog 的 select、insert、delete、updateCommentDatadriver：DB 类型，提供 comment 的 select、insert、delete、updateUserDatadriver：DB 类型，提供 user 的 select、insert、delete、updateLogDatadriver：FS 类型，提供 file 的 read、write系统驱动层：DB 类型和 FS 类型MySqlSystemdriver：DB 的 handlerFileSystemdriver：FS 的 handler现在以发布博文操作来介绍流程。假设接口地址为：http://www.xxxxx.com/postBlog：Controller 通过重写规则发现其对应的 Application 为 PostBlogApplication，于是 PostBlogApplication 被实例化，并且其中的特殊方法 main() 会被自动调用。PostBlogApplication 需要 PostBlogBusiness 业务来完成博文发布操作，PostBlogBusiness 被实例化，并且其中的特殊方法 flow() 会被调用。根据需求，发布博文的时候需要在博文表中插入一条博文，然后修改用户信息中的博文数量字段。那么 PostBlogBusiness 业务流就包括两个操作，这两个操作分别由 BlogComponent 中的 postBlog() 方法和 UserComponent 中的 updateUser() 方法来实现。其中前者往博文表中插入博文信息，后者将用户信息中的博文数量字段加 1。由于系统要求任何操作都需要记录日志，因此还有第三个操作就是记录日志，通过 LogComponent 的 logMsg() 方法实现。那么 PostBlogBusiness 业务流一共包括了三个操作，分别由三个组件来完成。下面就需要分别考虑上面三个组件的下层调用了：BlogComponent 的 postBlog() 调用 BlogDatadriver 的 insert 相关方法来插入博文数据，BlogDatadriver 是 DB 类型，因此通过 MySqlSystemdriver 来实现。UserComponent 的 updateUser() 调用 UserDatadriver 的 update 相关方法来实现博文数量更新，UserDatadriver 也是 DB 类型，因此也通过 MySqlSystemdriver 来实现。LogComponent 的 logMsg() 调用 LogDatadriver 的 write 相关方法，LogDatadriver 是 FS 类型，因此通过 FileSystemdriver 来实现。三个组件的操作都执行成功后，PostBlogBusiness 告诉 PostBlogApplication 博文发布成功，然后 PostBlogApplication 通过 Controller 来调用 View 相关的方法显示执行结果。其他几个操作流程读者可以举一反三，这里不再多介绍了。那么现在我们通过几个系统功能的演变用例来看看这个分层带来的益处。用例 1：为了方便对日志的管理，现在希望能够将日志存储在 DB 中而不是 FS 中。解决方法：这是对数据存储的改造，我们知道应该从数据驱动层入手。日志功能是由日志组件 LogComponent 实现的，其中 LogComponent 的 logMsg() 方法调用 LogDatadriver 来存日志。我们将 LogDatadriver 由 FS 类型改造成 DB 类型，接口方法保持不变，这样很快就完成了改造。用例 2：新增需求 - 用户 A 可以将一篇博文转移给另外一个用户 B。解决方法：首先这个新需求对应了一个新的应用，于是我们新增了一个 SendBlogApplication。需要有一个业务完成操作，新增业务为 SendBlogBusiness。考虑转移一篇博文涉及到的操作：将博文表中对应的用户 ID 字段由 A 的 ID 切换到 B 的 ID。A 用户的博文数量减 1。B 用户的博文数量加 1。这三个操作需要两个组件来完成，这两个组件我们系统中已经有了，BlogComponent 的 updateBlog() 完成操作 1，UserComponent 的 updateUser() 完成操作 2、3。从用例 2 可以看出，当新增了这么一个需求的时候，我们在应用层和业务层添加了实例，组件层以下都不变。这是因为现在的组件层已经能够满足新的业务的需求，当我们现有组件无法满足新的业务需求的时候，我们则需要对组件层做修改。通过这两个简单的用例我们发现，我们对系统的修改要么可以很明确的确定在哪些层，要么就是从上层组件往下层进行，操作起来很方便。5. 小结这种纵向的分层和横向的模块结合起来，能让整个系统的结构清晰流畅。在本人设计的一款框架里面，原生支持这样的分层架构和模块，使用者只需要按照同一个模式简单的操作，层之间的接口和协议已经由框架本身约定好。框架还不够完善，仅供内部使用，暂时不发布了。说明：本文涉及的技术架构探讨基于早期 PHP 开发环境（约 2012 年左右），文中提到的部分外链图片可能已失效，部分设计模式（如 MVC 变体）在当今微服务或云原生架构下可能有不同的演进形式，阅读时请结合当前技术栈酌情参考。
有经验的Java开发者和架构师容易犯的10个错误
有经验的 Java 开发者和架构师容易犯的 10 个错误即使是经验丰富的 Java 开发人员和架构师也会犯错。今天，我们讨论了一些如果您不密切注意，可能会犯的常见 Java 开发错误！我根据困扰程度将它们按降序排列——#1 最让我困扰，但根据其他因素，所有这 10 个错误都会给您带来同样的伤害 ;-)常见 Java 错误 #10：滥用或误解依赖注入依赖注入（Dependency Injection, DI）通常被认为是企业项目中的“最佳实践”。使用它似乎不会出错，但事实真的如此吗？DI 的基本思想之一是：您无需在对象内部查找其依赖关系，而是在以定义良好的方式（使用 DI 框架）创建对象之前初始化依赖关系。在实际创建对象时，只需将其传递给对象即可——可以通过构造函数中的预配置对象（构造函数注入）或使用 setter 方法（方法注入）。最重要的是，您传递的是对象所需的最终依赖，仅此而已。但是，即使在全新项目中，我仍然可以找到这样的代码：public class CustomerBill { // 由 DI 框架注入 private ServerContext serverContext; public CustomerBill(ServerContext serverContext) { this.serverContext = serverContext; } public void chargeCustomer(Customer customer) { CreditCardProcessor creditCardProcessor = serverContext.getServiceLocator().getCreditCardProcessor(); Discount discount = serverContext.getServiceLocator().getActiveDiscounts().findDiscountFor(customer); creditCardProcessor.bill(customer, discount); } }当然，这不是真正的 DI。该对象仍然自行进行依赖查找。存在链式调用代码（Train Wreck）serverContext.getServiceLocator().getCreditCardProcessor() 是这里出问题的另一种指示。正确的方法是像这样直接传递最终对象：public class CustomerBillCorrected { // 由 DI 框架注入 private ActiveDiscounts activeDiscounts; // 由 DI 框架注入 private CreditCardProcessor creditCardProcessor; public CustomerBillCorrected(ActiveDiscounts activeDiscounts, CreditCardProcessor creditCardProcessor) { this.activeDiscounts = activeDiscounts; this.creditCardProcessor = creditCardProcessor; } public void chargeCustomer(Customer customer) { Discount discount = activeDiscounts.findDiscountFor(customer); creditCardProcessor.bill(customer, discount); } }常见 Java 错误 #9：伪装 Java 更像 Perl与其他语言相比，Java 的优点之一是其类型安全方法。在您是唯一开发人员的小型项目中，几乎可以采用任何喜欢的编码样式。但是，在大型 Java 代码库和复杂的系统上，出现问题时需要早期警告。大多数编程错误应在编译时而不是在运行时捕获。Java 提供了许多便利来促进这些编译时警告。但是，当您像这样编写代码时，并没有什么能阻止您搬起石头砸自己的脚：public class AnimalFactory { public static Animal createAnimal(String type) { switch (type) { case "cat": return new Cat(); case "cow": return new Cow(); case "dog": return new Dog(); default: return new Cat(); } } }该代码非常危险，因为没有任何编译时检查可以保护您。开发人员可能会使用拼写错误的字符串来调用您的工厂，例如 createAnimal("dig") 期待狗（而不是猫）。该代码将正常编译，并且该错误将仅在以后的运行时出现。根据应用程序的不同，错误的出现可能会在应用程序正式投入生产后一个月！哎哟。请帮自己一个忙，并使用 Java 提供的所有功能来保证编译时的安全。这是一种更正确的方法（还有其他可能的解决方案），仅在正确时才进行编译：public class AnimalFactoryCorrected { public enum AnimalType { DOG, CAT, COW, ANY } public static Animal createAnimal(AnimalType type) { switch (type) { case CAT: return new Cat(); case COW: return new Cow(); case DOG: return new Dog(); case ANY: default: return new Cat(); } } }常见 Java 错误 #8：假装 Java 更像 C（即不了解 OOP）早在 C 时代，建议的编写代码的方法就是过程方法。您的数据存在于结构中)，并且通过函数对数据进行操作。数据很愚蠢，方法很聪明。但是，Java 是一种面向对象的语言，与这种方法相反。数据和函数绑定在一起（创建类），这些函数应该自己聪明。但是，许多 Java 开发人员要么不了解它们之间的区别，要么就不费心去编写 OOP 代码，即使他们内心深处知道他们的过程方法似乎不合适。Java 应用程序中过程代码的最佳指示之一是 instanceof 操作符的使用以及跟在其后的相应上下游代码段。instanceof 操作符当然有它的有效用途，但在平时的企业代码这是一个巨大的反模式。这是一个不涉及动物的示例：public void bill(Customer customer, Amount amount) { Discount discount = null; if (customer instanceof VipCustomer) { VipCustomer vip = (VipCustomer) customer; discount = vip.getVipDiscount(); } else if (customer instanceof BonusCustomer) { BonusCustomer vip = (BonusCustomer) customer; discount = vip.getBonusDiscount(); } else if (customer instanceof LoyalCustomer) { LoyalCustomer vip = (LoyalCustomer) customer; discount = vip.getLoyalDiscount(); } paymentGateway.charge(customer, amount); }可以按以下 OOP 方式重构 此代码：public void bill(Customer customer, Amount amount) { Discount discount = customer.getAppropriateDiscount(); paymentGateway.charge(customer, amount); }扩展 Customer（或实现 Customer 接口）的每个类都定义了一个折扣方法。这样做的好处是您可以添加新类型的客户，而无需接触客户管理系统。对于 instanceof 变体，添加新客户意味着您将必须搜索客户打印代码、客户账单代码、客户联系代码等，并为新类型添加新的 if 语句。您还应该查看有关 富域模型 和 贫域模型 的讨论。常见 Java 错误 #7：使用过多的延迟加载（即，不了解对象生命周期）我更经常发现以下代码：public class CreditCardProcessor { private PaymentGateway paymentGateway = null; public void bill(Customer customer, Amount amount) { // 给客户计费总是需要一个支付网关 getPaymentGateway().charge(customer.getCreditCard(), amount); } private PaymentGateway getPaymentGateway() { if (paymentGateway == null) { paymentGateway = new PaymentGateway(); paymentGateway.init(); // 此处有网络副作用 } return paymentGateway; } }延迟加载的最初想法是正确的：如果您有一个昂贵的对象，则仅在需要时才创建它。但是，在应用此技术之前，您必须真正确保：该对象确实是“昂贵的”（您如何定义它？）在某些情况下，不使用对象（因此不需要创建对象）如果对象中的结构不是真正的“沉重”或运行时始终创建的对象，我将越来越多地看到这种结构——那么有什么好处呢？过度使用此技术的主要问题是它隐藏了组件的生命周期。一个构建良好的应用程序具有其主要结构的明确生命周期。应该清楚何时创建、使用和销毁对象。几个 DI 框架可以帮助您实现对象生命周期。但是，当对象创建产生副作用时，这种技术真正可怕的用途就来自图片。这意味着您的应用程序的状态取决于对象创建的顺序（进入的请求类型的顺序）。由于涉及的案例太多，因此突然调试应用程序几乎是不可能的。复制生产中发生的问题是一项艰巨的任务，因为您必须知道 if 语句的运行顺序。无需使用此方法，只需定义应用程序启动期间所需的所有对象。这还有一个额外的优势，那就是您可以在应用程序部署期间发现任何致命问题。常见 Java 错误 #6：取决于“四人帮”（GoF）书作为您的圣经（又名 GoF 宗教）我真的很羡慕《设计模式》这本书的作者。单一出版物以一种其他书籍无法胜过的方式影响了整个 IT 行业。设计模式甚至已经进入了工作面试过程，而且有时候，如果您不阅读本书并记住几种设计模式的名称和公式，您有时将无法获得 IT 职位。希望那个时代正在慢慢消失。现在不要误会我的意思；这本书本身还不错。就像整个历史一样，问题出在人们如何使用和解释它。这是通常的情况：马克（Mark）是架构师，他把手放在 GoF 书上并阅读。他认为这太酷了！Mark 查看他正在研究的当前代码库。马克选择了他喜欢的设计模式，并将其应用到当时的代码中。马克将本书传递给高级开发人员，这些高级开发人员从步骤 1 开始相同的周期。结果代码一团糟。本书的简介中清楚地描述了正确使用该书的方式（适用于那些确实不愿阅读的人）。您遇到的问题是，您一遍又一遍地绊脚石，而本书为您提供了过去曾解决过类似问题的解决方案。注意事件的正确顺序。我有一个问题，我看书，然后找到解决问题的方法。不要陷入看书，寻找喜欢的解决方案，然后尝试将其应用到代码中的某个随机位置的陷阱中，尤其是因为书中提到的某些模式不再有效（请参阅 #5 下面）…。常见 Java 错误 #5：使用 Singletons（这是一种反模式！）我已经在前面提到了设计模式。但是，单例模式 需要一个观点。我请大家重复我 100 次 ;-)单例是反模式单例是反模式单例是反模式单例是反模式单例是反模式...单例在某个时间点使用它们。但是，借助现代的依赖注入框架，可以完全消除单例。 当您使用单例时，实际上是在代码中引入了一组新问题。为什么？因为：单例在类中创建隐藏的依赖项单例使代码不可测试（即使带有 Mock）单例将资源创建与资源获取混合在一起单例允许对全局状态产生副作用单例可能是并发问题的根源如果您仍然不相信我，那么有很多文档说明为什么单例现在是反模式。搜索，您将找到它。常见 Java 错误 #4：忽略方法可见性当我遇到经验丰富的 Java 开发人员时，我总是感到惊讶，他们认为 Java 只有三个保护修饰符。好吧，它有四个（！），并且还有 包私有（也称为 default）。不，我不会在这里解释它的作用。去查一下。我要解释的是，您应该注意 公开的 方法。应用程序中的公共方法是应用程序的可见 API。这应该尽可能的小巧紧凑，尤其是在编写可重用的库时（另请参见 SOLID 原理）。我真的很讨厌看到应该 私有的 公共方法。不仅因为它们公开了类的内部实现细节，而且还因为几乎根本不应该在此类之外使用它们。结果是，您始终对类的公共方法使用 单元测试。我见过所谓的架构师，他们相信将私有方法转换为公共方法以使他们可以对它们进行单元测试是可以接受的。测试私有方法是完全错误的。只需测试调用私有方法的公共方法。请记住：决不要因为单元测试而用更多的公共方法扩展 API。常见 Java 错误 #3：遭受项目特定的 StringUtils（即或更一般的 NIH 综合征）的折磨在过去，每个足够大的 Java 项目都包含 StringUtils、DateUtils、FileUtils 等文件。现在，当我在旧版代码中看到它时，我就明白了。相信我，我感到您的痛苦！我知道这些文件为使它们成熟和稳定所做的一切努力，因为太多的代码依赖于它们。但是，当我在全新的代码上看到像这样的文件而又没有任何依赖关系时，我的一部分就会陷入困境。架构师和高级开发人员的职责之一是跟踪经过测试的现有解决方案，这些解决方案更容易集成到您的应用程序中。如果您的职称中包含“架构师”一词，那么您就不会因为不了解 Apache Commons、Guava 库 或 Joda Date 库 而找借口。在不熟悉的区域编写代码之前，还应该进行一些研究。例如，在编写用于创建 PDF 文件的 REST 服务之前，请花一些时间来了解存在于 Java 的 REST 框架以及创建 PDF 文件的建议方法。（不建议运行 unix 命令行应用程序。Java 可以通过 iText 自己创建 PDF 文件）。常见 Java 错误 #2：依赖于环境的构建我已经与 Java 开发团队合作了 10 年，现在，我告诉您一个秘密：这只是构建企业应用程序的一种可接受的方法。 这是您的操作方式：我有一天会在您的软件公司中崭露头角。我将注意力集中在工作站上，并安装 Java 和我最喜欢的 IDE/工具。我从公司存储库（svn、git 或其他）中签出代码。我最多花 5 分钟来了解您所拥有的构建系统（例如 Maven、Gradle 甚至是 Ant）。我运行一个命令来构建应用程序，并且成功。这不仅是最佳方案，而且如果您已关注应用程序的构建方式，则这是唯一有效的方案。如果您的企业应用程序依赖于特定的 IDE、特定版本的 IDE 插件、PC 上的本地文件、额外的、未记录的环境设置、网络资源或任何非标准的东西，那么该是重新考虑构建系统的时候了。另外一个相关的事实是构建应该一步完成（请参阅 Joel 测试）。现在不要误会我 - 集成测试、自检版本或额外的部署/文档步骤可以具有额外的设置或使用外部数据库（应在公司 Wiki 中进行记录）。但是，获取可执行文件的简单编译方案最多应该花费一个小时的时间。我见过一些项目，新员工的第一次编译需要 2 天的时间（以设置环境）。常见 Java 错误 #1：使用反射/自省新闻快讯：如果您正在编写 ORM 框架、Java 代理、元编译器、IDE 或其他奇特的东西，则可以根据需要使用 Java 反射。但是，对于大多数企业应用程序而言，它们实际上只是数据存储上的美化的 CRUD 接口，Java 反射 实在是太过分了。我经常看到以性能、向后兼容甚至向前兼容的名义使用反射。几乎总是错误地使用它。反射总是基于一些假设（例如方法名称约定），这些假设在今天可能会成立，但明天可能会成立。反射问题很难理解，很难调试，很难解决。而且我甚至不打算研究 自我修改的代码。在企业 Java 应用程序中使用反射就像在建筑物的基础上插入定时炸弹。该建筑物现在可能已经稳定了，但是一旦计时器启动，一切都会崩溃。令我感到困惑的是，引入反射的架构师总是将反射视为“基本”，而实际上，您可以通过精心构建的对象层次结构和明确的插件体系结构/文档清晰地解决相同的问题。因此，让我为您在那里的所有“架构师”提供说明。它 IS 可以创建一个稳定、快速和易于维护的企业级 Java 应用程序 无 使用反射。如果您的香草型企业应用程序使用反射，那么您只是在自找麻烦。奖励部分：只有在了解实际瓶颈后，才尝试进行优化我见过在以下方面花费大量精力的架构师：微调日志记录语句在旧版代码中替换向量在循环中用 StringBuffers 替换 + 运算符为了“性能”而重构现有的、稳定的、成熟的且无错误的代码…和其他奇特的东西可悲的是，这样做并没有实际衡量这些努力可能产生的影响。即使应用程序运行速度更快，大多数情况下，还有其他更严重的问题（例如数据库锁定、内存泄漏、永远不会关闭/刷新的流）没有人关注。修复 SQL 查询将使速度提高 200％时，花费时间来修复日志记录并获得 3％的速度并不是很有帮助。因此，请务必记住，由于性能原因而发生的任何更改实际上都是一个四个步骤的过程：测量当前系统（有效的基准测试本身就是一项艰巨的任务）套用变更再次测量评估付出的努力和绩效获得的比率。记住优化的口头禅——测量，不要猜测。结论如果我看起来有些脾气暴躁，那是因为我除了清理经验丰富的编码员留下的混乱之外，还承担了其他许多工作。从某种程度上说，它比解决 n00bs 所犯的错误还要糟糕——高级开发人员应该更了解！;-)希望这份由高级开发人员提出的 10 个常见陷阱的清单可以帮助您发现应该在解决问题中使用您的专业知识的一些领域。即使是最优秀的编码人员，也应该记住，总有一些东西需要重新学习并付诸实践。寻找其他提高性能的方法吗？我们的电子书 Java 的“隐藏的生产力杀手” 概述了 Java 开发过程中最严重的生产力中断之一。是否想避免微服务应用程序中的性能瓶颈？请务必检查 XRebel。它可以帮助开发人员在开发过程中识别和修复潜在的性能问题。单击下面的按钮，在我们即将发布的演示中了解 XRebel 在实时编码环境中的工作方式。参见 XRebel 的实际应用说明适用版本：文中部分代码示例（如 switch 字符串）需要 Java 7 或更高版本。资源时效：文中部分外部链接（如 Google Code 上的 Guava 库）可能因服务关闭而失效，建议在实际开发中查阅最新的官方文档或 Maven 中央仓库。技术演进：Joda-Time 库的功能在 Java 8 及更高版本中已被 java.time 包原生支持，新项目建议优先考虑原生 API。
探索Java代码重构：优化之道与实践经验
在软件开发领域，代码如同建筑的基石，其质量直接影响软件的可维护性、扩展性和性能。随着项目的不断演进，代码可能会逐渐变得复杂、冗余，甚至难以理解和维护。此时，代码重构（Refactoring）成为了提升代码质量、保持软件健康发展的关键手段。本文将深入探讨 Java 代码重构的相关知识，包括其定义、重要性、常见方法、重构的心理阶段以及实际案例，旨在帮助读者更好地理解和应用代码重构技术，提升 Java 编程能力。代码重构的定义与重要性定义代码重构是指在不改变代码外部行为的前提下，对代码内部结构进行优化和改进的过程。它旨在提高代码的可读性、可维护性和可扩展性，使代码更易于理解、修改和扩展。通过重构，我们可以消除代码中的重复、优化算法、改善代码结构，从而提升软件的整体质量。重要性提高代码质量良好的代码结构和清晰的逻辑有助于减少错误的发生，提高软件的稳定性和可靠性。例如，通过合理的变量命名、函数拆分和代码组织，可以使代码更易于阅读和理解，降低出错的概率。增强可维护性当代码需要修改或扩展时，清晰的结构和简洁的逻辑能够让开发人员更快地定位问题和进行修改。例如，遵循设计模式和单一职责原则（Single Responsibility Principle）的代码，每个类和函数只负责一项明确的任务，修改时不会影响到其他无关部分。促进团队协作易于理解的代码便于团队成员之间的协作和交流。大家可以更快地理解彼此的代码，减少沟通成本，提高开发效率。例如，在团队项目中，统一的代码风格和规范有助于成员之间的协作。Java 代码重构的常见方法使用接口和泛型优化代码结构原始代码问题分析在 Java 开发中，经常会遇到代码冗余的问题。例如，在操作缓存的代码中，获取 Session 和 ObjectMap 实例的代码可能在多个方法中重复出现，违反了 DRY（Don't Repeat Yourself）原则。这不仅增加了代码量，还使得代码难以维护，一旦需要修改获取实例的方式，就必须在多个地方进行修改。重构步骤与示例代码创建接口：创建一个带有抽象方法的 Executable 接口，用于传递包含业务逻辑的实例。例如：@FunctionalInterface interface Executable<T> { T execute(ObjectMap map) throws ObjectGridException; }封装重复代码：创建 executeWithMap() 方法，封装获取 Session 和 ObjectMap 实例以及执行异常处理的重复代码。该方法接受一个 Executable 接口的实例，执行其中的业务逻辑并返回结果。例如：private <T> T executeWithMap(Executable<T> ex) { Session s; try { s = oGrid.getSession(); ObjectMap map = s.getMap(cacheName); // 执行业务逻辑 return ex.execute(map); } catch (ObjectGridException oge) { throw new RuntimeException("Error performing cache operation", oge); } finally { if (s != null) s.close(); } }替换原始代码：使用 Lambda 表达式或匿名内部类实现 Executable 接口，替换原始方法中的重复代码。例如：public V get(final K key) { return executeWithMap((ObjectMap map) -> (V) map.get(key)); } public void put(final K key, final V value) { executeWithMap((ObjectMap map) -> { map.upsert(key, value); return null; }); } public Map<K, V> getAll(Set<? extends K> keys) { final List<K> keyList = new ArrayList<K>(); keyList.addAll(keys); List<V> valueList = executeWithMap((ObjectMap map) -> map.getAll(keyList)); Map<K, V> map = new HashMap<K, V>(); for (int i = 0; i < keyList.size(); i++) { map.put(keyList.get(i), valueList.get(i)); } return map; }优化字符串连接操作字符串连接问题剖析在 Java 中，字符串连接是常见操作。在早期版本中，使用 + 进行字符串连接可能导致效率低下。因为每次使用 + 连接字符串时，都会创建一个新的 String 对象，在内存中生成大量的 String 和 StringBuffer 实例，增加了内存管理的开销。改进方案与代码示例使用 StringBuilder 或 StringBuffer：在 JDK 1.5 之后，推荐使用 StringBuilder（非线程安全但性能更高）或 StringBuffer（线程安全）进行字符串连接。例如：StringBuilder sb = new StringBuilder(); sb.append("Dear ").append(title).append(" ").append(name).append(" ").append(familyName).append(","); String msg = sb.toString();注意编译器优化：虽然编译器在某些情况下会自动优化使用 + 连接的字符串，但在复杂逻辑代码中，如循环内的字符串连接，仍需手动使用 StringBuilder 或 StringBuffer 以确保效率。例如：List<Student> students = studentBean.findStudents(); StringBuilder listedNames = new StringBuilder(); String separator = ""; for (Student student : students) { if (student.approved()) { if (listedNames.length() > 0) { separator = ", "; } listedNames.append(separator).append(student.getName()); } } String msg = intro + listedNames.toString();其他常见重构方法方法提取（Extract Method）：将过长或功能复杂的方法拆分成多个小方法，每个方法只负责单一功能，提高代码的可读性和可维护性。例如，一个处理用户注册逻辑的方法可能包含验证用户名、密码、邮箱等多个步骤，可以将这些步骤分别提取成独立的方法。变量重命名（Rename Variable）：使用有意义的变量名，使代码更易于理解。避免使用单个字母或缩写作为变量名，除非其含义非常明确。例如，将变量 i 重命名为 index，str 重命名为 userName 等。去除重复代码（Remove Duplication）：通过提取公共方法或使用继承、接口等方式，消除代码中的重复部分。例如，多个方法中都包含相同的数据库查询逻辑，可以将其提取到一个公共方法中。代码重构的心理阶段重构不仅是技术活动，往往也伴随着开发者的心理变化。了解这些阶段有助于更好地管理重构过程。绝望阶段刚开始查看要重构的模块时，可能会觉得任务简单，只需修改几个类和函数。但随着重构的深入，会发现代码存在诸多问题，如注释不符、数据结构混乱、函数调用逻辑复杂等。此时意识到问题的严重性，感到绝望，意识到任务可能无法在预期时间内完成。找最简单的做阶段承认模块的复杂性后，开始做一些简单的改进，如重命名函数、移除代码阻碍、定义常量等。这些操作虽然不能彻底解决问题，但至少不会使代码变得更糟。再次绝望阶段发现简单的修改无法解决根本问题，意识到需要重写大部分代码，但又受时间限制。只能部分重写相对简单的部分，以提高代码的重用性，尽管结果可能并不完美。开始乐观阶段经过几天的重构，对代码的改进方向有了更清晰的认识，确定了需要修改的类。虽然时间超支且任务艰巨，但仍对重构充满希望，认为付出是值得的。快速了结阶段由于花费时间过多，为了尽快完成重构，开始采用捷径，如简化文档、采用简单粗暴的重写方法。然而，运行测试时发现大量失败，不得不快速修复代码以使测试通过，然后提交代码。修改大量 Bug 阶段重写后的代码虽然通过了测试，但测试覆盖范围有限，后续几周内会出现越来越多的 Bug。每次快速修复都会使代码结构变得更差，但总体上仍比重构前有所改善。觉悟阶段几个月后，重构的模块出现严重 Bug，发现新代码的设计存在问题，被重构掉的老代码可能有其合理性。此时意识到重构并非简单任务，需要更谨慎地对待。实际案例分析案例背景在一个电商项目中，负责订单处理模块的开发。随着业务的发展，订单处理逻辑变得越来越复杂，代码中出现了大量的重复代码和复杂的条件判断，导致代码可读性和可维护性急剧下降。例如，计算订单总价的方法中，根据不同的商品类型和促销活动，包含了多层嵌套的 if-else 语句，难以理解和扩展。重构过程方法提取：将计算订单总价的逻辑拆分成多个小方法，每个方法负责一种商品类型或促销活动的计算。这样，主方法中的逻辑变得清晰，每个小方法的功能也易于理解和测试。使用策略模式优化条件判断：定义一个策略接口，每个商品类型或促销活动对应一个实现类。在订单处理类中，根据商品类型或促销活动选择相应的策略实现类来计算总价。这样，代码结构更加灵活，易于扩展，当添加新的商品类型或促销活动时，只需添加新的策略实现类即可。重构效果提高了代码可读性：拆分后的方法和策略模式使代码逻辑清晰，其他开发人员能够更快地理解订单处理逻辑。增强了可维护性：新的代码结构便于修改和扩展，当业务规则发生变化时，能够快速定位并修改相应的代码。提升了代码质量：通过消除重复代码和优化条件判断，减少了潜在的 Bug，提高了代码的稳定性。总结重构的意义与价值代码重构是软件开发过程中不可或缺的一环，它有助于提升代码质量、增强软件的可维护性和扩展性，降低开发成本，提高团队协作效率。通过不断地重构，我们可以使代码保持简洁、清晰，适应不断变化的业务需求。实践中的注意事项备份代码：在进行重构之前，务必备份原始代码，以防重构过程中出现问题导致代码丢失或损坏。编写测试用例：完善的测试用例是重构成功的保障。在重构前后，运行测试用例确保代码的外部行为不变。小步重构：避免一次性进行大规模的重构，应将重构任务分解为多个小步骤，每次只进行少量的修改，确保每一步的正确性。理解代码意图：在重构之前，充分理解代码的功能和业务逻辑，避免在不了解代码意图的情况下盲目修改。持续学习与改进代码重构是一个持续的过程，随着技术的发展和业务需求的变化，我们需要不断学习新的重构方法和技巧，不断改进代码质量。同时，要善于从实践中总结经验教训，避免在重构过程中犯同样的错误。希望本文能为 Java 开发者在代码重构的道路上提供有益的参考和指导，让我们一起通过重构打造更优质的 Java 代码。说明：文中关于字符串连接优化的建议主要基于 JDK 1.5 及之后的版本特性。在现代 Java 版本（如 Java 9+）中，编译器对 + 连接的优化机制有所升级（如使用 invokedynamic），但在循环体内进行大量字符串拼接时，显式使用 StringBuilder 仍是推荐的最佳实践。文中代码示例涉及的 ObjectGrid、Session 等类名为特定场景下的示意代码，实际使用时请根据项目依赖进行调整。
DDD（领域驱动设计）全面介绍领域驱动设计（Domain-Driven Design，简称 DDD），是由埃里克·埃文斯（Eric Evans）在 2003 年出版的《领域驱动设计：软件核心复杂性应对之道》中提出的一种软件设计方法论。其核心思想是让软件设计与业务领域紧密对齐，通过聚焦业务核心逻辑、梳理领域概念，解决复杂业务系统的设计难题，避免代码与业务脱节及后期维护成本高昂的问题。一、DDD 的核心目标DDD 并非一套固定的“编码规范”，而是一种“设计思想”。其核心目标可概括为以下三点：降低复杂性：将复杂的业务系统拆解为更小、更易理解的“领域单元”。每个单元聚焦特定业务场景，避免整体系统陷入混沌。对齐业务与技术：让开发人员、产品经理、业务专家使用统一的“领域语言”沟通（避免技术侧说“接口”、业务侧说“流程”的认知偏差），确保软件实现真正满足业务需求。提升系统可维护性：业务逻辑集中在“领域层”，与技术框架（如数据库、API 层）解耦。后续业务变更时，只需修改领域层，无需大面积重构技术代码。二、DDD 的核心概念DDD 通过一系列抽象概念梳理业务逻辑，这些概念是理解 DDD 的基础，可分为“领域基础概念”和“设计模式概念”两类。1. 领域基础概念（1）领域（Domain）与子领域（Subdomain）领域：指软件要解决的“业务范围”，例如“电商领域”、“物流领域”、“金融支付领域”。子领域：将一个大领域拆解为更小的、聚焦单一业务方向的单元。例如“电商领域”可拆分为：核心子领域：订单管理（业务核心，直接决定业务价值）；支撑子领域：用户管理、商品管理（为核心服务，需自主开发）；通用子领域：日志、权限、通知（非业务专属，可复用现有组件或第三方服务）。（2）领域模型（Domain Model）领域模型是对业务领域中“概念、规则、流程”的抽象表达，不是数据库表的映射，而是业务逻辑的载体。例如“订单领域模型”会包含“订单状态流转规则（待支付→已支付→已发货）”、“订单金额计算逻辑（商品价 + 运费 - 优惠券）”等核心业务逻辑。（3）限界上下文（Bounded Context）DDD 中最关键的概念之一，指“一个边界，边界内的领域模型具有统一的语义（即领域语言），边界外的模型无需强关联”。作用：解决“同一概念在不同业务场景下含义不同”的问题。例如“用户”在“订单上下文”中只需“用户 ID、收货地址”，在“用户中心上下文”中则需要“手机号、密码、实名认证信息”——两个上下文的“用户模型”可独立设计，避免冗余。示例：电商系统的限界上下文可划分为“订单上下文”、“商品上下文”、“支付上下文”、“物流上下文”。2. 设计模式概念DDD 提供了具体的模式来落地领域模型，核心模式包括：概念定义与作用示例（电商订单场景）实体（Entity）具有唯一标识（如 ID）、状态可变化的业务对象，核心是“身份”而非属性。订单（Order）：有唯一订单 ID，状态会从“待支付”变为“已完成”。值对象（Value Object）无唯一标识、不可变（创建后属性不修改）的业务对象，核心是“属性组合的含义”。收货地址（Address）：由“省、市、街道、邮编”组成，无 ID；金额（Money）：由“数值 + 币种”组成，不可修改（如需改金额，需新建 Money 对象）。聚合（Aggregate）由“实体 + 值对象”组成的“业务单元”，确保单元内数据一致性，对外暴露唯一“聚合根”。订单聚合：聚合根是“订单（Entity）”，包含“订单项（Value Object，如商品 ID、数量）”、“收货地址（Value Object）”；外部只能通过“订单 ID"操作聚合内的对象，避免直接修改“订单项”导致数据不一致。聚合根（Aggregate Root）聚合的“入口”，是聚合内唯一允许被外部访问的实体，负责维护聚合内的业务规则。订单（Order）是聚合根，外部要修改订单项，必须通过“订单的修改订单项方法”（而非直接操作订单项），确保订单项数量不超过商品库存。领域服务（Domain Service）封装“跨实体/跨聚合”的业务逻辑，本身无状态，仅负责协调领域对象完成业务操作。订单支付服务（OrderPaymentService）：协调“订单（修改状态）”、“支付（创建支付记录）”、“库存（扣减库存）”三个聚合，完成“支付成功后更新订单状态”的逻辑。领域事件（Domain Event）记录领域中发生的“重要业务事件”，用于实现“跨上下文通信”或“异步业务流程”。订单支付成功事件（OrderPaidEvent）：订单支付完成后发布此事件，“物流上下文”监听事件后自动创建物流单，“积分上下文”监听事件后为用户增加积分。仓储（Repository）封装领域对象的“持久化逻辑”（如数据库 CRUD），对外提供“领域友好”的接口，隔离领域层与数据访问层。订单仓储（OrderRepository）：提供 getById(String orderId)、save(Order order) 方法，内部实现 MySQL 或 MongoDB 的操作，但领域层无需关心存储细节。三、DDD 的分层架构为了实现“业务与技术解耦”，DDD 推荐采用分层架构，将系统分为 4 层（从核心到外围）：领域层（Domain Layer）核心层，包含领域模型（实体、值对象、聚合）、领域服务、领域事件、仓储接口（仅定义接口，不实现）。职责：纯业务逻辑实现，不依赖任何技术框架（如 Spring、MyBatis），是系统的“业务内核”。应用层（Application Layer）位于领域层之上，是“业务流程的编排者”，不包含复杂业务逻辑，仅调用领域层的对象完成用户需求。职责：接收外部请求（如 API 接口、消息队列），协调领域服务、仓储接口，完成流程串联（例如“创建订单”接口：调用商品领域查库存→调用订单领域创建订单→调用仓储保存订单）。基础设施层（Infrastructure Layer）位于最底层，为其他层提供技术支持，包含仓储实现（如 OrderRepository 的 MySQL 实现）、工具类、第三方服务集成（如支付接口、物流接口）。职责：将领域层的“抽象需求”落地为技术实现（例如领域层定义 OrderRepository 接口，基础设施层实现 OrderRepositoryImpl，用 MyBatis 操作数据库）。表现层（Presentation Layer）最外层，负责与用户或外部系统交互，包含API 接口（如 RESTful）、前端页面、消息接收端等。职责：接收外部输入（如用户提交的订单信息），转换为应用层能处理的参数；将应用层的处理结果（如订单创建成功的 ID）转换为外部可理解的格式（如 JSON）返回。四、DDD 的适用场景与优势1. 适用场景DDD 并非“万能方法论”，更适合复杂业务系统，例如：电商系统（订单、支付、库存、物流的联动逻辑复杂）；金融系统（账户、交易、风控的业务规则严谨）；企业 ERP 系统（采购、销售、库存、财务的流程交叉）。对于简单业务系统（如个人博客、静态网站），DDD 会增加设计复杂度，反而不如"CRUD 快速开发”高效。2. 核心优势业务可读性强：代码结构与业务领域对齐（如“订单模块”下直接包含 Order 实体、OrderService、OrderRepository），新开发人员能快速理解业务逻辑。易维护、易扩展：业务逻辑集中在领域层，技术变更（如换数据库、换 API 框架）只需修改基础设施层或表现层，不影响核心业务；新增业务（如“订单拆分”）只需在领域层扩展聚合和服务，无需重构整体架构。团队协作高效：基于“限界上下文”拆分团队（如“订单团队”负责订单上下文，“支付团队”负责支付上下文），团队内部用统一领域语言沟通，减少跨团队协作成本。五、DDD 的落地步骤（简化版）领域探索与建模：与业务专家沟通，梳理业务流程、核心概念，识别“领域、子领域、限界上下文”；在每个上下文中，抽象出“实体、值对象、聚合、领域服务”，形成领域模型草图。架构分层实现：按“领域层→应用层→基础设施层→表现层”的顺序编码，先定义领域层的抽象（如实体、仓储接口），再落地基础设施层的实现。验证与迭代：通过测试（如单元测试、集成测试）验证领域模型是否符合业务规则；根据业务变更（如新增“优惠券抵扣”规则）迭代领域模型，确保模型始终与业务对齐。六、DDD 的常见误区将 DDD 等同于“复杂设计”：DDD 的核心是“简化复杂”，而非“制造复杂”——对于复杂业务，DDD 通过拆分和抽象降低理解成本；对于简单业务，无需强行套用 DDD 模式。将领域模型与数据库表绑定：领域模型是“业务逻辑的载体”，数据库表是“存储结构”，二者无需一一对应（例如一个聚合可对应多张数据库表，一个值对象可存储为表的多个字段）。忽略领域语言的统一：若开发人员与业务专家对“订单状态”、“支付成功”的定义不一致，即使套用 DDD 模式，也会导致软件与业务脱节——统一领域语言是 DDD 落地的前提。总之，DDD 是一种“以业务为中心”的设计思想，其价值不在于“使用了多少 DDD 术语”，而在于“是否让软件真正贴合业务、解决业务问题”。对于复杂系统而言，DDD 是应对业务复杂性、提升系统长期可维护性的重要工具。
并发编程的 15 条建议 (译)内核专家 Bryan Cantrill 和 Jeff Bonwick 在 2008 年 9 月的《ACM Queue》上发表了《Real-world Concurrency》一文，提出了 15 条并发编程的建议。以下是对该文建议的简单摘录，部分条目附带了陈硕先生的注释。Know your cold paths from your hot paths. 弄清楚代码里的热门执行路径和冷门执行路径。对冷门路径，用粗粒度的锁即可。对热门路径——也就是那些必须高度并发才能实现所期望的高吞吐量的代码，应该更加小心，加锁的策略必须简单明了且细粒度。Intuition is frequently wrong—be data intensive. 直觉常常是错的，要靠数据说话。【陈硕】比如线程切换到底有多大开销，普通 mutex 加锁到底有多大代价，系统调用的开销如何，gettimeofday() 在 x86-64 Linux 是不是真的系统调用等等，都要靠数据说话。Know when—and when not—to break up a lock. 知道什么时候把一个锁拆成多个，并知道什么时候不必这样做。除了把全局锁拆成多个锁，另外一种常用的避免线程争用 (contention) 的办法是减少加锁的范围。比方说从共享的数据结构里移除 (remove and delete) 元素，其实 delete 这一步可以放到锁外面。Be wary of readers/writer locks. 警惕读写锁。初学者常犯的一个错误是，见到某个数据结构频繁读而很少写，那么就把 mutex 替换为 rwlock。这不见得是正确的。【陈硕】这条深得我心，muduo thread lib 目前就没有提供读写锁的封装。另外，这一条也能鉴别 另一篇关于线程争用的文章 不靠谱。Consider per-CPU locking. 考虑用每个 CPU 用一个锁。Know when to broadcast—and when to signal. 知道什么时候用单个唤醒，什么时候用广播唤醒。notifyAll() 通常表示状态变更，而 notify() 通常表示资源变得可用。滥用 notifyAll() 会导致 惊群现象。【陈硕】muduo thread lib 的 ThreadPool 区分使用 notify() 和 notifyAll()，可作参考。Learn to debug postmortem. 学会验尸。【陈硕】在程序中只使用 Scoped locking 来加锁的话，很容易从 call stack 查出死锁。参考《多线程服务器的常用编程模型》第 6 节 线程间同步。Design your systems to be composable. 设计系统，使之能扩充。【陈硕】比方说，把对对象的修改操作都挪到同一个线程，这样就不必加锁。参考 muduo 的 EventLoop::runInLoop()。Don't use a semaphore where a mutex would suffice. 如果 Mutex 就能解决问题的话，不要使用信号量 semaphore。【陈硕】muduo thread lib 有意识地不提供信号量的封装。Consider memory retiring to implement per-chain hash-table locks. 考虑用内存“退休”法来实现哈希表的按桶加锁。Be aware of false sharing. 知道什么是伪共享。跟多 CPU 的 Cache 有关，值得了解。Consider using nonblocking synchronization routines to monitor contention. 考虑使用非阻塞的加锁来观察线程争用。When reacquiring locks, consider using generation counts to detect state change. 在重新加锁时，考虑使用版本号来检测状态变更。Use wait- and lock-free structures only if you absolutely must. 只在别无它法时才使用无锁数据结构。Prepare for the thrill of victory—and the agony of defeat. 准备接受成功的喜悦和失败的痛苦。更详细的解释请看原文。Bryan Cantrill 是 dtrace 的主要作者，Jeff Bonwick 是 ZFS 和 Slab allocator 的发明者。说明：本文源自 2008 年的文章，部分硬件架构与语言标准（如 C++11 内存模型）此后已有演进，建议结合现代技术栈参考阅读。
一堂如何提高代码质量的培训课编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。今天这堂培训课讲什么呢？我既不讲 Spring，也不讲 Hibernate，更不讲 Ext，我不讲任何一个具体的技术。我们抛开任何具体的技术，来谈谈如何提高代码质量。如何提高代码质量，相信不仅是在座所有人苦恼的事情，也是所有软件项目苦恼的事情。如何提高代码质量呢？我认为我们首先要理解什么是高质量的代码。高质量代码的三要素我们评价高质量代码有三要素：可读性、可维护性、可变更性。我们的代码要一个都不能少地达到这三要素的要求，才能算高质量的代码。1. 可读性 (Readability)一提到可读性似乎有一些老生常谈的味道，但令人沮丧的是，虽然大家一而再、再而三地强调可读性，但我们的代码在可读性方面依然做得非常糟糕。由于工作的需要，我常常需要去阅读他人的代码，维护他人设计的模块。每当我看到大段大段、密密麻麻的代码，而且还没有任何的注释时，常常感慨不已，深深体会到了这项工作的重要。由于分工的需要，我们写的代码难免需要别人去阅读和维护。而对于许多程序员来说，他们很少去阅读和维护别人的代码。正因为如此，他们很少关注代码的可读性，也对如何提高代码的可读性缺乏切身体会。有时即使为代码编写了注释，也常常是注释语言晦涩难懂、形同天书，令阅读者反复斟酌依然不明其意。针对以上问题，我给大家以下建议：1) 避免大段代码如果你有阅读他人代码的经验，当你看到别人写的大段大段的代码，而且还不怎么带注释，你是怎样的感觉？是不是“嗡”地一声头大。各种各样的功能纠缠在一个方法中，各种变量来回调用，相信任何人都不会认为它是高质量的代码，但却频繁地出现在我们编写的程序里。如果现在你再回顾自己写过的代码，你会发现，稍微编写一个复杂的功能，几百行的代码就出去了。一些比较好的办法就是分段。将大段的代码经过整理，分为功能相对独立的一段又一段，并且在每段的前端编写一段注释。这样的编写，比前面那些杂乱无章的大段代码确实进步了不少，但它们在功能独立性、可复用性、可维护性方面依然不尽人意。从另一个比较专业的评价标准来说，它没有实现低耦合、高内聚。我给大家的建议是，将这些相对独立的段落另外封装成一个又一个的函数。许多大师在自己的经典书籍中，都鼓励我们在编写代码的过程中应当养成不断重构的习惯。我们在编写代码的过程中常常要编写一些复杂的功能，起初是写在一个类的一个函数中。随着功能的逐渐展开，我们开始对复杂功能进行归纳整理，整理出了一个又一个的独立功能。这些独立功能有它与其它功能相互交流的输入输出数据。当我们分析到此处时，我们会非常自然地要将这些功能从原函数中分离出来，形成一个又一个独立的函数，供原函数调用。在编写这些函数时，我们应当仔细思考一下，为它们取一个释义名称，并为它们编写注释（后面还将详细讨论这个问题）。另一个需要思考的问题是，这些函数应当放到什么地方。这些函数可能放在原类中，也可能放到其它相应职责的类中，其遵循的原则应当是"职责驱动设计"（后面也将详细描述）。下面是我编写的一个从 XML 文件中读取数据，将其生成工厂的一个类。这个类最主要的一段程序就是初始化工厂，该功能归纳起来就是三部分功能：用各种方式尝试读取文件、以 DOM 的方式解析 XML 数据流、生成工厂。而这些功能被我归纳整理后封装在一个不同的函数中，并且为其取了释义名称和编写了注释：/** * 初始化工厂。根据路径读取 XML 文件，将 XML 文件中的数据装载到工厂中 * @param path XML 的路径 */ public void initFactory(String path){ if(findOnlyOneFileByClassPath(path)){return;} if(findResourcesByUrl(path)){return;} if(findResourcesByFile(path)){return;} this.paths = new String[]{path}; } /** * 初始化工厂。根据路径列表依次读取 XML 文件，将 XML 文件中的数据装载到工厂中 * @param paths 路径列表 */ public void initFactory(String[] paths){ for(int i=0; i<paths.length; i++){ initFactory(paths[i]); } this.paths = paths; } /** * 重新初始化工厂，初始化所需的参数，为上一次初始化工厂所用的参数。 */ public void reloadFactory(){ initFactory(this.paths); } /** * 采用 ClassLoader 的方式试图查找一个文件，并调用readXmlStream()进行解析 * @param path XML 文件的路径 * @return 是否成功 */ protected boolean findOnlyOneFileByClassPath(String path){ boolean success = false; try { Resource resource = new ClassPathResource(path, this.getClass()); resource.setFilter(this.getFilter()); InputStream is = resource.getInputStream(); if(is==null){return false;} readXmlStream(is); success = true; } catch (SAXException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } catch (IOException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findOnlyOneFileByClassPath:"+path,e); } return success; } /** * 采用 URL 的方式试图查找一个目录中的所有 XML 文件，并调用readXmlStream()进行解析 * @param path XML 文件的路径 * @return 是否成功 */ protected boolean findResourcesByUrl(String path){ boolean success = false; try { ResourcePath resourcePath = new PathMatchResource(path, this.getClass()); resourcePath.setFilter(this.getFilter()); Resource[] loaders = resourcePath.getResources(); for(int i=0; i<loaders.length; i++){ InputStream is = loaders[i].getInputStream(); if(is!=null){ readXmlStream(is); success = true; } } } catch (SAXException e) { log.debug("Error when findResourcesByUrl:"+path,e); } catch (IOException e) { log.debug("Error when findResourcesByUrl:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findResourcesByUrl:"+path,e); } return success; } /** * 用 File 的方式试图查找文件，并调用readXmlStream()解析 * @param path XML 文件的路径 * @return 是否成功 */ protected boolean findResourcesByFile(String path){ boolean success = false; FileResource loader = new FileResource(new File(path)); loader.setFilter(this.getFilter()); try { Resource[] loaders = loader.getResources(); if(loaders==null){return false;} for(int i=0; i<loaders.length; i++){ InputStream is = loaders[i].getInputStream(); if(is!=null){ readXmlStream(is); success = true; } } } catch (IOException e) { log.debug("Error when findResourcesByFile:"+path,e); } catch (SAXException e) { log.debug("Error when findResourcesByFile:"+path,e); } catch (ParserConfigurationException e) { log.debug("Error when findResourcesByFile:"+path,e); } return success; } /** * 读取并解析一个 XML 的文件输入流，以 Element 的形式获取 XML 的根， * 然后调用buildFactory(Element)构建工厂 * @param inputStream 文件输入流 * @throws SAXException * @throws IOException * @throws ParserConfigurationException */ protected void readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{ if(inputStream==null){ throw new ParserConfigurationException("Can't parse source because of InputStream is null!"); } DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(this.isValidating()); factory.setNamespaceAware(this.isNamespaceAware()); DocumentBuilder build = factory.newDocumentBuilder(); Document doc = build.parse(new InputSource(inputStream)); Element root = doc.getDocumentElement(); buildFactory(root); } /** * 用从一个 XML 的文件中读取的数据构建工厂 * @param root 从一个 XML 的文件中读取的数据的根 */ protected abstract void buildFactory(Element root);在编写代码的过程中，通常有两种不同的方式。一种是从下往上编写，也就是按照顺序，每分出去一个函数，都要将这个函数编写完，才回到主程序，继续往下编写。而一些更有经验的程序员会采用另外一种从上往下的编写方式。当他们在编写程序的时候，每个被分出去的程序，可以暂时只写一个空程序而不去具体实现功能。当主程序完成以后，再一个个实现它的所有子程序。采用这样的编写方式，可以使复杂程序有更好的规划，避免只见树木不见森林的弊病。有多少代码就算大段代码，每个人有自己的理解。我编写代码，每当达到 15~20 行的时候，我就开始考虑是否需要重构代码。同理，一个类也不应当有太多的函数，当函数达到一定程度的时候就应该考虑分为多个类了；一个包也不应当有太多的类······2) 释义名称与注释我们在命名变量、函数、属性、类以及包的时候，应当仔细想想，使名称更加符合相应的功能。我们常常在说，设计一个系统时应当有一个或多个系统分析师对整个系统的包、类以及相关的函数和属性进行规划，但在通常的项目中这都非常难于做到。对它们的命名更多的还是程序员来完成。但是，在一个项目开始的时候，应当对项目的命名出台一个规范。譬如，在我的项目中规定，新增记录用 new 或 add 开头，更新记录用 edit 或 mod 开头，删除用 del 开头，查询用 find 或 query 开头。使用最乱的就是 get，因此我规定，get 开头的函数仅仅用于获取类属性。注释是每个项目组都在不断强调的，可是依然有许多的代码没有任何的注释。为什么呢？因为每个项目在开发过程中往往时间都是非常紧的。在紧张的代码开发过程中，注释往往就渐渐地被忽略了。利用开发工具的代码编写模板也许可以解决这个问题。以我们常用的 MyEclipse 为例，在菜单 Window >> Preferences >> Java >> Code Style >> Code Templates >> Comments 中，可以简单地修改一下。"Files"代表的是我们每新建一个文件（可能是类也可能是接口）时编写的注释，我通常设定为：/* * created on ${date} */"Types"代表的是我们新建的接口或类前的注释，我通常设定为：/** * * @author ${user} */第一行为一个空行，是用于你写该类的注释。如果你采用“职责驱动设计”，这里首先应当描述的是该类的职责。如果需要，你可以写该类一些重要的方法及其用法、该类的属性及其中文含义等。${user} 代表的是你在 Windows 中登陆的用户名。如果这个用户名不是你的名称，你可以直接写死为你自己的名称。其它我通常都保持为默认值。通过以上设定，你在创建类或接口的时候，系统将自动为你编写好注释，然后你可以在这个基础上进行修改，大大提高注释编写的效率。同时，如果你在代码中新增了一个函数时，通过 Alt+Shift+J 快捷键，可以按照模板快速添加注释。在编写代码时如果你编写的是一个接口或抽象类，我还建议你在 @author 后面增加 @see 注释，将该接口或抽象类的所有实现类列出来，因为阅读者在阅读的时候，寻找接口或抽象类的实现类比较困难。/** * 抽象的单表数组查询实现类，仅用于单表查询 * @author 范钢 * @see com.htxx.support.query.DefaultArrayQuery * @see com.htxx.support.query.DwrQuery */ public abstract class ArrayQuery implements ISingleQuery { ...2. 可维护性 (Maintainability)软件的可维护性有几层意思，首先的意思就是能够适应软件在部署和使用中的各种情况。从这个角度上来说，它对我们的软件提出的要求就是不能将代码写死。1) 代码不能写死我曾经见我的同事将系统要读取的一个日志文件指定在 C 盘的一个固定目录下，如果系统部署时没有这个目录以及这个文件就会出错。如果他将这个决定路径下的目录改为相对路径，或者通过一个属性文件可以修改，代码岂不就写活了。一般来说，我在设计中需要使用日志文件、属性文件、配置文件，通常都是以下几个方式：将文件放到与类相同的目录，使用 ClassLoader.getResource() 来读取；将文件放到 classpath 目录下，用 File 的相对路径来读取；使用 web.xml 或另一个属性文件来制定读取路径。我也曾见另一家公司的软件要求，在部署的时候必须在 C:/bea 目录下，如果换成其它目录则不能正常运行。这样的设定常常为软件部署时带来许多的麻烦。如果服务器在该目录下已经没有多余空间，或者已经有其它软件，将是很挠头的事情。2) 预测可能发生的变化除此之外，在设计的时候，如果将一些关键参数放到配置文件中，可以为软件部署和使用带来更多的灵活性。要做到这一点，要求我们在软件设计时，应当有更多的意识，考虑到软件应用中可能发生的变化。比如，有一次我在设计财务软件的时候，考虑到一些单据在制作时的前置条件，在不同企业使用的时候，可能要求不一样，有些企业可能要求严格些而有些要求松散些。考虑到这种可能的变化，我将前置条件设计为可配置的，就可能方便部署人员在实际部署中进行灵活变化。然而这样的配置，必要的注释说明是非常必要的。软件可维护性的另一层意思就是软件的设计便于日后的变更。这一层意思与软件的可变更性是重合的。所有的软件设计理论的发展，都是从软件的可变更性这一要求逐渐展开的，它成为了软件设计理论的核心。3. 可变更性 (Changeability)前面我提到了，软件的变更性是所有软件理论的核心，那么什么是软件的可变更性呢？按照现在的软件理论，客户对软件的需求时时刻刻在发生着变化。当软件设计好以后，为应对客户需求的变更而进行的代码修改，其所需要付出的代价，就是软件设计的可变更性。由于软件合理的设计，修改所付出的代价越小，则软件的可变更性越好，即代码设计的质量越高。一种非常理想的状态是，无论客户需求怎样变化，软件只需进行适当的修改就能够适应。但这之所以称之为理想状态，因为客户需求变化是有大有小的。如果客户需求变化非常大，即使再好的设计也无法应付，甚至重新开发。然而，客户需求的适当变化，一个合理的设计可以使得变更代价最小化，延续我们设计的软件的生命力。1) 通过提高代码复用提高可维护性我曾经遇到过这样一件事，我要维护的一个系统因为应用范围的扩大，它对机关级次的计算方式需要改变一种策略。如果这个项目统一采用一段公用方法来计算机关级次，这样一个修改实在太简单了，就是修改这个公用方法即可。但是，事实却不一样，对机关级次计算的代码遍布整个项目，甚至有些还写入到了那些复杂的 SQL 语句中。在这样一种情况下，这样一个需求的修改无异于需要遍历这个项目代码。这样一个实例显示了一个项目代码复用的重要，然而不幸的是，代码无法很好复用的情况遍布我们所有的项目。代码复用的道理十分简单，但要具体运作起来非常复杂，它除了需要很好的代码规划，还需要持续地代码重构。对整个系统的整体分析与合理规划可以根本地保证代码复用。系统分析师通过用例模型、领域模型、分析模型的一步一步分析，最后通过正向工程，生成系统需要设计的各种类及其各自的属性和方法。采用这种方法，功能被合理地划分到这个类中，可以很好地保证代码复用。采用以上方法虽然好，但技术难度较高，需要有高深的系统分析师，并不是所有项目都能普遍采用的，特别是时间比较紧张的项目。通过开发人员在设计过程中的重构，也许更加实用。当某个开发人员在开发一段代码时，发现该功能与前面已经开发功能相同，或者部分相同。这时，这个开发人员可以对前面已经开发的功能进行重构，将可以通用的代码提取出来，进行相应的改造，使其具有一定的通用性，便于各个地方可以使用。一些比较成功的项目组会指定一个专门管理通用代码的人，负责收集和整理项目组中各个成员编写的、可以通用的代码。这个负责人同时也应当具有一定的代码编写功力，因为将专用代码提升为通用代码，或者以前使用该通用代码的某个功能，由于业务变更，而对这个通用代码的变更要求，都对这个负责人提出了很高的能力要求。虽然后一种方式非常实用，但是它有些亡羊补牢的味道，不能从整体上对项目代码进行有效规划。正因为两种方法各有利弊，因此在项目中应当配合使用。2) 利用设计模式提高可变更性对于初学者，软件设计理论常常感觉晦涩难懂。一个快速提高软件质量的捷径就是利用设计模式。这里说的设计模式，不仅仅指经典的 23 个模式，是一切前人总结的，我们可以利用的、更加广泛的设计模式。a. 消除 if...else...这个我也不知道叫什么名字，最早是哪位大师总结的，它出现在 Larman 的《UML 与模式应用》，也出现在 Martin 的《敏捷软件开发》。它是这样描述的：当你发现你必须要设计这样的代码：if...elseif...elseif...else... 时，你应当想到你的代码应当重构一下了。我们先看看这样的代码有怎样的特点。if(var.equals("A")){ doA(); } else if(var.equals("B")){ doB(); } else if(var.equals("C")){ doC(); } else{ doD(); }这样的代码很常见，也非常平常，我们大家都写过。但正是这样平常才隐藏着我们永远没有注意的问题。问题就在于，如果某一天这个选项不再仅仅是 A、B、C，而是增加了新的选项，会怎样呢？你也许会说，那没有关系，我把代码改改就行。然而事实上并非如此，在大型软件研发与维护中有一个原则，每次的变更尽量不要去修改原有的代码。如果我们重构一下，能保证不修改原有代码，仅仅增加新的代码就能应付选项的增加，这就增加了这段代码的可维护性和可变更性，提高了代码质量。那么，我们应当如何去做呢？经过深入分析你会发现，这里存在一个对应关系，即 A 对应 doA()，B 对应 doB()···如果将 doA()、doB()、doC()···与原有代码解耦，问题就解决了。如何解耦呢？设计一个接口 X 以及它的实现 A、B、C···每个类都包含一个方法 doX()，并且将 doA() 的代码放到 A.doX() 中，将 doB() 的代码放到 B.doX() 中···经过以上的重构，代码还是这些代码，效果却完全不一样了。我们只需要这样写：X x = factory.getBean(var); x.doX();这样就可以实现以上的功能了。我们看到这里有一个工厂，放着所有的 A、B、C···并且与它们的 key 对应起来，并且写在配置文件中。如果出现新的选项时，通过修改配置文件就可以无限制的增加下去。这个模式虽然有效提高了代码质量，但是不能滥用，并非只要出现 if...else... 就需要使用。由于它使用了工厂，一定程度上增加了代码复杂度，因此仅仅在选项较多，并且增加选项的可能性很大的情况下才可以使用。另外，要使用这个模式，继承我在附件中提供的抽象类 XmlBuildFactoryFacade 就可以快速建立一个工厂。如果你的项目放在 Spring 或其它可配置框架中，也可以快速建立工厂。设计一个 Map 静态属性并使其 V 为这些 A、B、C···这个工厂就建立起来了。b. 策略模式 (Strategy Pattern)也许你看过策略模式的相关资料但没有留下太多的印象。一个简单的例子可以让你快速理解它。如果一个员工系统中，员工被分为临时工和正式工并且在不同的地方相应的行为不一样。在设计它们的时候，你肯定设计一个抽象的员工类，并且设计两个继承类：临时工和正式工。这样，通过下溯类型，可以在不同的地方表现出临时工和正式工的各自行为。在另一个系统中，员工被分为了销售人员、技术人员、管理人员并且也在不同的地方相应的行为不一样。同样，我们在设计时也是设计一个抽象的员工类，并且设计数个继承类：销售人员、技术人员、管理人员。现在，我们要把这两个系统合并起来，也就是说，在新的系统中，员工既被分为临时工和正式工，又被分为了销售人员、技术人员、管理人员，这时候如何设计。如果我们还是使用以往的设计，我们将不得不设计很多继承类：销售临时工、销售正式工、技术临时工、技术正式工。。。如此的设计，在随着划分的类型，以及每种类型的选项的增多，呈笛卡尔增长。通过以上一个系统的设计，我们不得不发现，我们以往学习的关于继承的设计遇到了挑战。解决继承出现的问题，有一个最好的办法，就是采用策略模式。在这个应用中，员工之所以要分为临时工和正式工，无非是因为它们的一些行为不一样，比如，发工资时的计算方式不同。如果我们在设计时不将员工类分为临时工类和正式工类，而仅仅只有员工类，只是在类中增加“工资发放策略”。当我们创建员工对象时，根据员工的类型，将“工资发放策略”设定为“临时工策略”或“正式工策略”，在计算工资时，只需要调用策略类中的“计算工资”方法，其行为的表现，也设计临时工类和正式工类是一样的。同样的设计可以放到销售人员策略、技术人员策略、管理人员策略中。一个通常的设计是，我们将某一个影响更大的、或者选项更少的属性设计成继承类，而将其它属性设计成策略类，就可以很好的解决以上问题。使用策略模式，你同样把代码写活了，因为你可以无限制地增加策略。但是，使用策略模式你同样需要设计一个工厂——策略工厂。以上实例中，你需要设计一个发放工资策略工厂，并且在工厂中将“临时工”与“临时工策略”对应起来，将“正式工”与“正式工策略”对应起来。c. 适配器模式 (Adapter Pattern)我的笔记本是港货，它的插头与我们常用的插座不一样，所有我出差的时候我必须带一个适配器，才能使用不同地方的插座。这是一个对适配器模式最经典的描述。当我们设计的系统要与其它系统交互，或者我们设计的模块要与其它模块交互时，这种交互可能是调用一个接口，或者交换一段数据，接受方常常因发送方对协议的变更而频繁变更。这种变更，可能是接受方来源的变更，比如原来是 A 系统，现在变成 B 系统了；也可能是接受方自身的代码变更，如原来的接口现在增加了一个参数。由于发送方的变更常常导致接受方代码的不稳定，即频繁跟着修改，为接受方的维护带来困难。遇到这样的问题，一个有经验的程序员马上想到的就是采用适配器模式。在设计时，我方的接口按照某个协议编写，并且保持固定不变。然后，在与真正对方接口时，在前段设计一个适配器类，一旦对方协议发生变更，我可以换个适配器，将新协议转换成原协议，问题就解决了。适配器模式应当包含一个接口和它的实现类。接口应当包含一个本系统要调用的方法，而它的实现类分别是与 A 系统接口的适配器、与 B 系统接口的适配器···我曾经在一个项目中需要与另一个系统接口，起初那个系统通过一个数据集的方式为我提供数据，我写了一个接收数据集的适配器；后来改为用一个 XML 数据流的形式，我又写了一个接收 XML 的适配器。虽然为我提供数据的方式不同，但是经过适配器转换后，输出的数据是一样的。通过在 Spring 中的配置，我可以灵活地切换到底是使用哪个适配器。d. 模板模式 (Template Pattern)经典模式中的模板模式，对开发者的代码规划能力提出了更高的要求，它要求开发者对自己开发的所有代码有一个相互联系和从中抽象的能力，从各个不同的模块和各个不同的功能中，抽象出其过程比较一致的通用流程，最终形成模板。譬如说，读取 XML 并形成工厂，是许多模块常常要使用的功能。它们虽然有各自的不同，但是总体流程都是一样的：读取 XML 文件、解析 XML 数据流、形成工厂。正因为有这样的特征，它们可以使用共同的模板，那么，什么是模板模式呢？模板模式通常有一个抽象类。在这个抽象类中，通常有一个主函数，按照一定地顺序去调用其它函数。而其它函数往往是某这个连续过程中的各个步骤，如以上实例中的读取 XML 文件、解析 XML 数据流、形成工厂等步骤。由于这是一个抽象类，这些步骤函数可以是抽象函数。抽象类仅仅定义了整个过程的执行顺序，以及一些可以通用的步骤（如读取 XML 文件和解析 XML 数据流），而另一些比较个性的步骤，则由它的继承类自己去完成（如上例中的“形成工厂”，由于各个工厂各不一样，因此由各自的继承类自己去决定它的工厂是怎样形成的）。各个继承类可以根据自己的需要，通过重载重新定义各个步骤函数。但是，模板模式要求不能重载主函数，因此正规的模板模式其主函数应当是 final（虽然我们常常不这么写）。另外，模板模式还允许你定义的这个步骤中，有些步骤是可选步骤。对于可选步骤，我们通常称为“钩子（Hook）”。它在编写时，在抽象类中并不是一个抽象函数，但却是一个什么都不写的空函数。继承类在编写时，如果需要这个步骤则重载这个函数，否则就什么也不写，进而在执行的时候也如同什么都没有执行。通过以上对模板模式的描述可以发现，模板模式可以大大地提高我们的代码复用程度。以上一些常用设计模式，都能使我们快速提高代码质量。还是那句话，设计模式不是什么高深的东西，恰恰相反，它是初学者快速提高的捷径。然而，如果说提高代码复用是提高代码质量的初阶，使用设计模式也只能是提高代码质量的中阶。那么，什么是高阶呢？我认为是那些分析设计理论，更具体地说，就是职责驱动设计和领域驱动设计。3) 职责驱动设计和领域驱动设计前面我提到，当我们尝试写一些复杂功能的时候，我们把功能分解成一个个相对独立的函数。但是，应当将这些函数分配到哪个类中呢？也就是系统中的所有类都应当拥有哪些函数呢？或者说应当表现出哪些行为呢？答案就在这里：以职责为中心，根据职责分配行为。我们在分析系统时，首先是根据客户需求进行用例分析，然后根据用例绘制领域模式和分析模型，整个系统最主要的类就形成了。通过以上分析形成的类，往往和现实世界的对象是对应的。正因为如此，软件世界的这些类也具有了与现实世界的对象相对应的职责，以及在这些职责范围内的行为。职责驱动设计（Responsibility-Driven Design，RDD） 是 Craig Larman 在他的经典著作《UML 和模式应用》中提出的。职责驱动设计的核心思想，就是我们在对一个系统进行分析设计的时候，应当以职责为中心，根据职责分配行为。这种思想首先要求我们设计的所有软件世界的对象，应当与现实世界尽量保持一致，他称之为“低表示差异”。有了低表示差异，一方面提高了代码的可读性，另一方面，当业务发生变更的时候，也可以根据实际情况快速应对变更。Craig Larman 在提出职责驱动设计理论的同时，还提出了 GRASP 设计模式，来丰富这个理论。在 GRASP 设计模式中，我认为，低耦合、高内聚、信息专家模式最有用。继 Craig Larman 提出的职责驱动设计数年之后，另一位大师提出了领域驱动设计。领域驱动设计（Domain-Driven Design，DDD） 是 Eric Evans 在他的同名著作《领域驱动设计》中提出的。在之前的设计理论中，领域模型是从用例模型到分析模型之间的一种中间模型，也就是从需求分析到软件开发之间的一种中间模型。这么一个中间模型，既不是需求阶段的重要产物，在开发阶段也不以它作为标准进行开发，仅仅是作为参考，甚至给人感觉有一些多余。但是，Evans 在领域驱动设计中，将它放到了一个无比重要的位置。按照领域驱动设计的理论，在需求分析阶段，需求分析人员使用领域模型与客户进行沟通；在设计开发阶段，开发人员使用领域模型指导设计开发；在运行维护和二次开发阶段，维护和二次开发人员使用领域模型理解和熟悉系统，并指导他们进行维护和二次开发。总之，在整个软件开发的生命周期中，领域模型都成为了最核心的内容。领域驱动设计继承了职责驱动设计。在领域驱动设计中强调的，依然是低表示差异，以及职责的分配。但是，如何做到低表示差异呢？如何完成职责分配呢？领域驱动设计给了我们完美的答案，那就是建立领域模型。领域驱动设计改变了我们的设计方式。在需求分析阶段，用例模型已不再是这个阶段的核心，而是建立领域模型。在开发和二次开发阶段，开发人员也不再是一埋头地猛扎进程序堆里开始编程，而是首先细致地进行领域模型分析。领域驱动设计强调持续精化，使领域模型不再是一旦完成分析就扔在一边不再理会的图纸，而是在不断理解业务的基础上不断修改和精化领域模型，进而驱动我们代码的精化。领域驱动设计强调的不再是一次软件开发过程中我们要做的工作，它看得更加长远，它强调的是一套软件在相当长一段时间内持续升级的过程中我们应当做的工作。我认为，领域驱动设计是提高代码质量的最高等级。当时，使用领域驱动设计进行软件开发是一场相当巨大的改革，它颠覆了我们过去的所有开发模式，我们必须脚踏实地地一步一步去实践和改变。代码质量评价的关键指标：低耦合与高内聚随着软件业的不断发展，随着软件需求的不断扩大，软件所管理的范围也在不断拓宽。过去一个软件仅仅管理一台电脑的一个小小的功能，而现在被扩展到了一个企业、一个行业、一个产业链。过去我们开发一套软件，只有少量的二次开发，当它使用到一定时候我们就抛弃掉重新又开发一套。现在，随着用户对软件依赖程度的不断加大，我们很难说抛弃一套软件重新开发了，更多的是在一套软件中持续改进，使这套软件的生命周期持续数年以及数个版本。正是因为软件业面临着如此巨大的压力，我们的代码质量，我们开发的软件拥有的可变更性和持续改进的能力，成为软件制胜的关键因素，令我们不能不反思。耦合就是对某元素与其它元素之间的连接、感知和依赖的量度。耦合包括：元素 B 是元素 A 的属性，或者元素 A 引用了元素 B 的实例（这包括元素 A 调用的某个方法，其参数中包含元素 B）。元素 A 调用了元素 B 的方法。元素 A 直接或间接成为元素 B 的子类。元素 A 是接口 B 的实现。如果一个元素过于依赖其它元素，一旦它所依赖的元素不存在，或者发生变更，则该元素将不能再正常运行，或者不得不相应地进行变更。因此，耦合将大大影响代码的通用性和可变更性。内聚，更为专业的说法叫功能内聚，是对软件系统中元素职责相关性和集中度的度量。如果元素具有高度相关的职责，除了这些职责内的任务，没有其它过多的工作，那么该元素就具有高内聚性，反之则为低内聚性。内聚就像一个专横的管理者，它只做自己职责范围内的事，而将其它与它相关的事情，分配给别人去做。高质量的代码要求我们的代码保持低耦合、高内聚。但是，这个要求是如此的抽象与模糊，如何才能做到这些呢？软件大师们告诉我们了许多方法，其中之一就是 Craig Larman 的职责驱动设计。深入理解职责驱动设计要理解职责驱动设计，我们首先要理解“低表示差异”。低表示差异我们开发的应用软件实际上是对现实世界的模拟，因此，软件世界与现实世界存在着必然的联系。当我们在进行需求分析的时候，需求分析员实际上是从客户那里在了解现实世界事物的规则、工作的流程。如果我们在软件分析和设计的过程中，将软件世界与现实世界紧密地联系到一起，我们的软件将更加本色地还原事物最本质的规律。这样的设计，就称之为“低表示差异”。采用“低表示差异”进行软件设计，现实世界有什么事物，就映射为软件世界的各种对象（类）；现实世界的事物拥有什么样的职责，在软件世界里的对象就拥有什么样的职责；在现实世界中的事物，因为它的职责而产生的行为，在软件世界中就反映为对象所拥有的函数。低表示差异，使分析设计者对软件的分析和设计更加简单，思路更加清晰；使代码更加可读，阅读者更加易于理解；更重要的是，当需求发生变更，或者业务产生扩展时，设计者只需要遵循事物本来的面貌去思考和修改软件，使软件更加易于变更和扩展。角色、职责、协作理解了“低表示差异”，现在我们来看看我们应当如何运用职责驱动设计进行分析和设计。首先，我们通过与客户的沟通和对业务需求的了解，从中提取出现实世界中的关键事物以及相互之间的关系。这个过程我们通常通过建立领域模型来完成。领域模型建立起来以后，通过诸如 Rational Rose 这样的设计软件的正向工程，生成了我们在软件系统中最初始的软件类。这些软件类，由于每个都扮演着现实世界中的一个具体的角色，因而赋予了各自的职责。前面我已经提到，如果你的系统采用职责驱动设计的思想进行设计开发，作为一个好的习惯，你应当在每一个软件类的注释首行，清楚地描述该软件类的职责。当我们完成了系统中软件类的制订，分配好了各自的职责，我们就应该开始根据软件需求，编写各个软件类的功能。在前面我给大家提出了一个建议，就是不要在一个函数中编写大段的代码。编写大段的代码，通常会降低代码的内聚度，因为这些代码中将包含不是该软件类应当完成的工作。作为一个有经验的开发人员，在编写一个功能时，首先应当对功能进行分解。一段稍微复杂的功能，通常都可以被分解成一个个相对独立的步骤。步骤与步骤之间存在着交互，那就是数据的输入输出。通过以上的分解，每一个步骤将形成一个独立的函数，并且使用一个可以表明这个步骤意图的释义函数名。接下来，我们应当考虑的，就是应当将这些函数交给谁。它们有可能交给原软件类，也有可能交给其它软件类，其分配的原则是什么呢？答案是否清楚，那就是职责。每个软件类代表现实世界的一个事物，或者说一个角色。在现实世界中这个任务应当由谁来完成，那么在软件世界中，这个函数就应当分配给相应的那个软件类。通过以上步骤的分解，一个功能就分配给了多个软件类，相互协作地完成这个功能。这样的分析和设计，其代码一定是高内聚的和高可读性的。同时，当需求发生变更的时候，设计者通过对现实世界的理解，可以非常轻松地找到那个需要修改的软件类，而不会影响其它类，因而也就变得易维护、易变更和低耦合了。说了这么多，举一个实例也许更能帮助理解。拿一个员工工资系统来说吧。当人力资源在发放一个月工资的时候，以及离职的员工肯定不能再发放工资了。在系统设计的期初，开发人员商量好，在员工信息中设定一个“离职标志”字段。编写工资发放的开发人员通过查询，将“离职标志”为 false 的员工查询出来，并为他们计算和发放工资。但是，随着这个系统的不断使用，编写员工管理的开发人员发现，“离职标志”字段已经不能满足客户的需求，因而将“离职标志”字段废弃，并增加了一个“离职时间”字段来管理离职的员工。然而，编写工资发放的开发人员并不知道这样的变更，依然使用着“离职标志”字段。显然，这样的结果就是，软件系统开始对离职员工发放工资了。仔细分析这个问题的原因，我们不难发现，确认员工是否离职，并不是“发放工资”软件类应当完成的工作，而应当是“员工管理”软件类应当完成的。如果将“获取非离职员工”的任务交给“员工管理”软件类，而“发放工资”软件类仅仅只是去调用，那么离职功能由“离职标志”字段改为了“离职时间”字段，其实就与“发放工资”软件类毫无关系。而作为“员工管理”的开发人员，一旦发生这样的变更，他当然知道去修改自己相应的“获取非离职员工”函数，这样就不会发生以上问题。通过这样一个实例，也许你能够理解“职责驱动设计”的精要与作用了吧。职责分配与信息专家通过以上对职责驱动设计的讲述，我们不难发现，职责驱动设计的精要就是职责分配。但是，在纷繁复杂的软件设计中，如何进行职责分配常常令我们迷惑。幸运的是，Larman 大师清楚地认识到了这一点。在他的著作中，信息专家模式（又称为专家模式）为我们提供了帮助。信息专家模式告诉我们，在分析设计中，应当将职责分配给软件系统中的这样一个软件类，它拥有实现这个职责所必须的信息。我们称这个软件类，叫“信息专家”。用更加简短的话说，就是将职责分配给信息专家。为什么我们要将职责分配给信息专家呢？我们用上面的例子来说明吧。当“发放工资”软件类需要获取非离职员工时，“员工管理”软件类就是“获取非离职员工”任务的信息专家，因为它掌握着所有员工的信息。假设我们不将“获取非离职员工”的任务交给“员工管理”软件类，而是另一个软件类 X，那么，为了获取员工信息，软件类 X 不得不访问“员工管理”软件类，从而使“发放工资”与 X 耦合，X 又与“员工管理”耦合。这样的设计，不如直接将“获取非离职员工”的任务交给“员工管理”软件类，使得“发放工资”仅仅与“员工管理”耦合，从而有效地降低了系统的整体耦合度。总之，采用“职责驱动设计”的思路，为我们提高软件开发质量、可读性、可维护性，以及保持软件的持续发展，提供了一个广阔的空间。说明本文涉及的部分工具（如 MyEclipse）及设计实践可能随技术发展有所演变，请以当前主流开发环境与官方文档为准。文中部分外链图片源自历史博客，可能存在失效情况，不影响核心内容理解。文中提到的设计模式与理论（如 RDD、DDD）为核心软件工程思想，依然具有极高的参考价值。