用干净的架构组织应用程序的逻辑[有例子]。

261 阅读12分钟

本主题摘自 Solid Book - The Software Architecture & Design Handbook w/ TypeScript + Node.js。如果您喜欢本帖,请查看

你有多少次在开发一个应用程序时,在编写一些逻辑时,问自己。

"这是放这个逻辑的正确位置吗?"

要想知道某样东西属于哪里,有很多事情要做。

首先,你必须了解你所处的领域。这就是你如何弄清楚你的应用程序中的子域

然后,对于每个子域,你必须确定所有的用例,以及哪些角色被允许执行这些用例

这就是康威法则作用

我发现最具有挑战性的部分,特别是对于那些刚进入企业应用开发领域、已经超越了简单的CRUD MVC应用的开发者来说,是实现分层架构以分离应用开发的关注点。

挑战似乎是理解哪一层应该放置逻辑。

在这篇文章中,我们将了解到以下内容。

  • 清洁架构,以及为什么我们要把大型应用的关注点分成几层。
  • 6种最常见的应用逻辑类型以及它们属于哪一层。

清洁架构/分层架构

Robert Martin在他的同名书中写到了清洁架构。虽然读起来有点挑战性(我花了2个小时才真正读完),但它很了不起。它教你如何将代码组织和分组为组件,然后如何组织一个应用程序,将这些组件连接到诸如数据库、API、网络服务器和其他我们需要为应用程序提供动力的外部事物。

一个真正的简化版本的清洁架构可能看起来像什么。

Simplified Clean Architecture

领域层

在一层(领域),我们有所有重要的东西:实体、业务逻辑、规则和事件。这是我们软件中不可替代的东西,我们不能用另一个库或框架来代替。这也是那些不太可能需要改变的东西,因为它代表了我们的业务内容

例如,如果我们是一个卖书的应用程序,我们就卖书。如果我们是一个寻找停车位的应用,领域层包含了寻找停车位的核心逻辑

因为我们不太可能改变我们业务的核心本质,所以领域层是最稳定一层

域层假设了一种更高层次的政策,其他一切都依赖于此。

信息(其他一切)层

另一层(infra)包含了所有实际在领域层中旋转执行的代码。

清洁架构的扩展

我们一般可以把干净的架构表述为领域基础设施

与我们的业务有关的东西是领域,而适配器--那些与使我们能够运行网络应用的技术(数据库、网络服务器、控制器、缓存等)挂钩的东西是基础设施

但是,魔鬼就在细节中。

如果我们把眼镜擦掉,一个更详细的干净架构的视图将看起来像这样。


对于小规模的应用来说,这可能看起来是矫枉过正。但是,对于那些预期寿命很长、由更大的团队维护、为公司赚钱/省钱的应用程序来说,弄清楚如何分离关注点以及将逻辑放在哪里是至关重要的。

如果不能解决这种复杂性,不能用更好的架构来解决它,就会使一个项目很快变成遗留模式

闲话少说,这里有6种最常见的应用逻辑类型,它们都是进入大型应用的途径。

1.演示逻辑

涉及到我们如何向用户展示某种东西的逻辑。

消费者使用的大多数应用程序都有前端。这种类型的逻辑完全与我们如何向用户展示东西有关。

是我们编写的HTML、CSS和JavaScript将一个空白的页面变成了一个精心设计的、活生生的、会呼吸的前端应用程序。

.container
  background: white;
  color: blue;

傻瓜式的UI不应该包含业务规则

有一个原则叫做Dumb UI

这个想法是让UI逻辑与任何领域层的逻辑保持分离,因为领域层的逻辑对架构中的其他一切都有依赖作用。

前端是不稳定的。这意味着它不断需要改变。由于这一点,把对架构中其他组件很重要的逻辑(特别是领域逻辑)放在这里并不是一个好主意,因为这有可能会持续破坏应用程序。

稳定的依赖性原则:如果你对这种现象感兴趣,即什么使依赖性稳定,什么使依赖性不稳定,可以看看这个

智能和哑巴组件

现代前端JavaScript框架,如React和Angular,已经普及了智能(容器)组件哑巴(功能)组件

这两者之间可能有很好的关注点分离,智能组件持有状态和操作状态的方法,让哑巴组件简单地投射视图,但前端仍然主要是表现逻辑,应该很少或不包含其他类型的逻辑(除了一些验证逻辑)。

对后端来说,前端是一个基础设施,我们通过RESTful API等提供数据访问/适配器的关注。

2.数据访问/适配器逻辑

逻辑关注的是我们如何实现对基础设施层关注的访问,如缓存、数据库、前端等。

仅仅用一个普通的JavaScript/TypeScript对象来代表我们的领域层是走不远的。

我们需要把这个东西连接到互联网上,并启用一个前端来连接它我们想用哪种网络服务器?Express.js?还是Hapi?

我们还需要弄清楚我们将如何持久化我们的域对象。想使用SQL数据库?还是NoSQL?

缓存如何?

哦,还有如何利用很酷的外部服务,比如Stripe的账单或Pusher的实时聊天?我们需要为这些服务编写适配器,以便我们的内部层可以使用它们。

这就对了。这一层是关于定义外部世界的适配器的。通过创建一个Repository类来封装将一个聚合体持久化到数据库的复杂性,从而简化内部层的使用。

下面是在这一层要做的几件常见的事情。

  • RESTful APIs。用Express.js定义一个RESTful API,并创建控制器来接受请求。
  • 生产中间件:编写Express.js控制器中间件,以保护你的API免受诸如DDos和暴力登录尝试的影响。
  • 数据库访问。创建包含在数据库上执行CRUD的方法的存储库。使用像Sequelize和TypeORM这样的ORM或原始查询来完成。
  • 计费集成。创建一个与Stripe或Paypal等支付处理器的适配器,使其能够被内层使用。

3.应用逻辑/用例

定义我们应用的实际功能的逻辑

用例是我们应用程序的功能。

一旦我们的应用程序的所有用例被确定,然后被开发,我们就客观上完成了。

完成了。"完成 "是一个我不常说的词。它对每个人都有不同的含义。然而,如果我们能弄清楚用例,并弄清楚谁或什么应该能够执行这些用例(演员或代理--因为机器人/服务器也是演员),我们就能更接近于对完成的共同理解。一旦这样做了,我就更有信心说我们已经完成了

CQS / CQRS

如果我们遵循命令-查询分离的原则,用例要么是COMMANDS ,要么是QUERIES

用例是特定的应用

你的公司内部可能有几个应用程序。

以谷歌为例。Google有GoogleDriveGoogle DocsGoogle Maps等。

这些应用中的每一个都有自己的一套用例,像这样。

Google Drive

  • shareFolder(folderId: FolderId, users: UserCollection):与谷歌企业中的其他用户共享一个文件夹。
  • createFolder(parentFolderId: FolderId, name: string):创建一个文件夹。
  • createDocument(parentFolderId: FolderId):创建一个Google Docs文档。

谷歌文档

  • shareDocument(users: UserCollection, visibility: VisibilityType):与多个用户共享一个文档。
  • deleteDocument(documentId: DocumentId):删除一个文档。

谷歌地图

  • getTripRoutes (start: Location, end: Location, time?: Date):获取一个行程的所有路线。
  • startTrip (start: Location, end: Location):现在开始一次旅行。

一个更常见和简单的例子是部署一个管理面板。你可能需要一个独立于你的主应用程序的仪表板,以便做一些管理方面的事情。

我通常围绕用例来计划和构建软件,因为这使得代码更容易推理,并提高你完成项目的速度。

我有一本免费的电子书,叫做 "名称、构造和结构",在那里你可以学到更多关于这种设计可读代码库的方法。

如果你想了解如何使用用例驱动开发来学习编码的具体细节,请先阅读这篇关于用例的文章(这是我最喜欢的文章)。

如果你想了解更多,可以看看我写的关于软件设计和架构的书,在书中我将带领你了解用例驱动开发的过程。

4.领域服务逻辑

核心业务逻辑并不完全适合在单一实体的范围内。

现在我们进入了领域层领域驱动设计是创建丰富领域模型的最好方法。

在DDD中,我们总是试图将领域逻辑定位在最接近它所涉及的实体的地方。

在某些情况下,逻辑会蔓延到两个或更多的实体中,而将逻辑放在其中一个实体中似乎并不太合理。

我们使用领域服务来确保我们不会在一个特定的应用程序的用例中丢失业务规则,而是将其保留在领域层中,这样它就可以被每个依赖它的应用程序所使用。

5.验证逻辑

规定领域对象有效的含义的逻辑。

验证逻辑是另一个领域层的问题,而不是基础设施的问题。

比方说,我们想创建一个User 实体。而User 包含一个叫做email:string 的属性。

interface UserProps {
  userEmail: string;
}
class User extends Entity<UserProps> {
  private constructor (props: UserProps, id?: UniqueEntityId) {
    super(props, id)
  }

  public static create (props: UserProps, id?: UniqueEntityId): Result<User> {
    const propsResult = Guard.againstNullOrUndefined(props.userEmail);
    if (!propsResult.succeeded) {
      return Result.fail<User>(propsResult.error);
    }

    return Result.ok<User>(new User(props, id))
  }
}

有什么办法可以阻止别人用一个无效的userEmail:string 来创建一个User

const userOrError: Result<User> = User.create({ userEmail: 'diddle' });
userOrError.isSuccess // true

这就是我们使用Value Objects的原因。我们可以用userEmailValue Object来封装验证规则。

如果我们把userEmail 的类型改为严格类型,而不是像这样的字符串类型。

interface UserProps {
  userEmail: UserEmail;
}

然后创建一个UserEmail 值对象...

import { TextUtil } from '../utils'
import { Result, Guard } from '../../core'

interface UserEmailProps {
  email: string;
}

export class UserEmail extends ValueObject<UserEmailProps> {

  // Private constructor. No one can say "new UserEmail('diddle')"
  private constructor (props: UserEmailProps) {
    super(props);
  }

  // Factory method, can do UserEmail.create() 
  public static create (props: UserEmailProps): Result<UserEmail> {
    if (Guard.againstNullOrUndefined(props.email) || 
      !TextUtil.isValidEmail(props.email)) {
        return Result.fail<UserEmail>("Email not provided or not valid.");
    }  else {
      return Result.ok<UserEmail>(new UserEmail(props));
    }
  }
}

现在,没有办法创建一个无效的User

6.核心业务逻辑/实体逻辑

属于一个实体的逻辑。

最重要的是应用程序的家族珠宝所在:实体。

而如果该实体有对其他相关实体的引用,则是聚合根

住在这里的核心业务逻辑是。

  • 初始/默认值
  • 保护类的不变性(什么变化是允许的,以及什么时候)。
  • 为变化、创建、删除和其他与业务相关的事情创建域事件。正是通过领域事件,复杂的业务逻辑可以被链起来。

一些需要遵循的原则

不要过度设计

你应该知道什么时候你需要使用分层架构。一般来说,当你的应用程序有很多业务规则时,它是最有意义的。在这种情况下,实现分层架构是个好主意,以便将持久化逻辑(例如)的关注点与验证逻辑和领域层的核心业务规则分开。


结论

在这篇文章中,我们介绍了大型应用开发中的6种主要逻辑类型。

下面是它们的摘要。

  • 表现逻辑。涉及到我们如何向用户展示某种东西的逻辑。
  • 数据访问/适配器逻辑。与我们如何实现对基础设施层的访问有关的逻辑,如缓存、数据库、前端等。
  • 应用逻辑/用例。定义我们应用程序的实际功能的逻辑。
  • 领域服务逻辑。核心业务,不完全适合于单一实体的范围。
  • 验证逻辑。规定领域对象有效的含义的逻辑。
  • 核心业务逻辑/实体逻辑。属于单一实体的逻辑。