Porto - 先进的软件架构模式

Porto 是我在查看 apiato 的文档时接触到的,看到的第一眼就感觉“这是我找了很久的东西”。一直在思考复杂的项目应该怎么架构,这个模式提供了很好的思路,从现实中的远洋运输船里得到启发,应用到软件开发的思想里,很有指导意义,翻译了一些主要部分, 时间仓促,肯定很多地方不到位,只是希望对大家有一些帮助。

Github 地址 https://github.com/Mahmoudz/Porto

Porto (软件架构模式)

文章目录

简介

Porto 是一个现代化的软件架构模式,旨在帮助开发人员以高度可维护的方式组织其代码。其主要目标是以可重用的方式组织业务逻辑。

Porto 是标准MVC的替代品,适用于大型和长期项目,因为它们往往随着时间的推移具有更高的复杂性。

Porto 继承了MVC,DDD,模块化和分层架构的概念。它坚持一系列方便的设计原则,如SOLID,OOP,LIFT,DRY,CoC,GRASP,泛化,高内聚和低耦合。

Porto 开始作为一个实验架构,试图解决在建造中大型网络项目时,开发者面临的常见问题。尽管所有模块化架构都侧重于框架通用组件的可重用性,但 Porto 专注于业务逻辑的可重用性。

优势特性

  • 可跨多个类似的项目重复使用业务逻辑(容器)。
  • 任何开发人员易于理解(没什么神秘的)。
  • 易于维护(易于适应变化)。
  • 易于测试(测试驱动)。
  • 零技术债务(开发者之间的沟通不畅)。
  • 解耦代码(编辑 X 不会中断 Y )。
  • 灵活的UI(从Web应用程序开始,随后构建一个API,或者相反)
  • 非常有组织的代码库。
  • 可扩展代码(易于修改和实现功能)。
  • 轻松的框架升级(应用程序和框架之间完全分离)。
  • 容易找到任何功能。
  • 内部组件之间有组织的联系。
  • 简洁的类库(遵循单一责任原则)。
  • 简洁和清晰的开发工作流程。

层级

Porto 包含2层(ContainersShip)和一套具有预定职责的“组件”。

这些图层(文件夹)可以在您选择的框架内的任何地方创建。

(例如:在Laravel PHP中,您可以将它们放在 app / 目录下,或在根目录下创建一个 src / 目录)

顶层代码(业务逻辑)应该写在 Container 层中。 而底层代码将存放在 Ship 层。

Container(顶层代码)间接依赖 Ship (底层代码)功能,不是相反。

层级图

更形象一点:

file

Ship 层:

Ship 层包含 Ship 的引擎,该引擎会自动加载容器的所有组件。
它还包含所有 Container 组件可以使用的代码。

Ship 层在将应用程序代码与框架代码分离时起着重要作用。
因此,它有助于升级 “框架代码” 而不影响 “应用代码”。

PortoShip 层非常简洁,它不包含常见的可重复使用的功能,如身份验证或授权,所有这些功能都在自己分开的 Container 中。

Ship 文件结构

Ship 层有以下文件夹:

  • Engine: 用在自动加载容器中的组件。
  • Parents:包含了容器中可能会用到的组件的父类。
  • 其他文件夹:提供了一些经常会用到的功能供 Container 调用。比如,全局异常,应用中间件,全局设置等等...

注意:所有 Container 的组件 必须 继承自 Ship 层(在 Parents 文件夹)。

Container 层:

Containers 层用来放业务逻辑。

我们建议每一个 Container 只包含一个 Model,但有时你可能会需要更多。只是需要牢记两个 Model 就需要两个仓库,两个转换器,等等...
除非你的两个 Model 一定要同时使用,否则最好把他们分开放进2个 Container

Containers 文件结构:

Container 1
    ├── Actions
    ├── Tasks
    ├── Models
    └── UI
        ├── WEB
        │   ├── Routes
        │   ├── Controllers
        │   └── Views
        ├── API
        │   ├── Routes
        │   ├── Controllers
        │   └── Transformers
        └── CLI
            ├── Routes
            └── Commands

Container 2
    ├── Actions
    ├── Tasks
    ├── Models
    └── UI
        ├── WEB
        │   ├── Routes
        │   ├── Controllers
        │   └── Views
        ├── API
        │   ├── Routes
        │   ├── Controllers
        │   └── Transformers
        └── CLI
            ├── Routes
            └── Commands

Containers 之间的交互

  • 一个 Container 可以依赖其他 Container
  • 一个控制器可以执行其他 Container 里的任务。
  • 一个 Container 里的模型可以和另一 Container 里的模型有关联关系.

组件类别

一个 Container 可以包含很多的组件,组件一般被分为两类:核心组件可选组件

核心组件:

几乎所有的 Web 应用里你都会用到以下组件:
路由 - 控制器 - 请求 - 动作 - 任务 - 模型 - 视图 - 转换器.

视图: 应用里有 html 页面时会用到。

转换器: App 提供 json 或 xml 数据时会用到。

核心控件交互图

file

请求生命周期

这是一个典型的 API 请求调用

  1. User 通过 Route 文件调用一个 Endpoint
  2. Endpoint 调用 Middleware 来处理身份认证。
  3. Endpoint 调用 Controller 方法.
  4. Request 注入 Controller 并自动执行表单验证和权限认证。
  5. Controller 调用 Action 并把请求的数据传过去。
  6. Action 调用多个 Tasks 处理业务逻辑, {或者自己全部搞定}
  7. Tasks 执行业务逻辑 (每个 Task 只干一件事)。
  8. ActionTasks 返回的数据再返回给 Controller
  9. ControllerView 或者 Transformer 组织响应并返回给 User

可选组件:

以下组件您可以按需选用,不过我们强烈你使用它们。

仓库 - 异常 - Criterias - 策略 - 测试 - 中间件 - 服务提供者 - 事件 - 命令 - 数据迁移 - 数据 - 数据工厂 - Contracts - Traits - 任务...

核心组件的使用方法

路由(Routes)

路由是 HTTP 请求的第一个接收者。

路由的责任是把请求转到集体的控制器方法去。

路由文件应该包含 Endpoints(一种分辨 HTTP 请求的 URL 模式)。

当 HTTP 请求到达应用时,Endpoints会匹配 url,并调用对应的控制器方法。

原则:

  • 一共有三种理由:API 路由,Web 路由,命令行路由。
  • Web 路由和 API 路由应该分开,放在各自的文件夹里。
  • Web 路由应该只包含 Web Endpoints (供浏览器请求),API 路由文件夹里只包含 API Endpoint。
  • 每个 Container 应该有自己的路由。
  • 每个路由应该只有一个 Endpoint。
  • Endpoint 的功能只是调用控制器里相应的方法,而不应该做其他任何事。

控制器(Controller)

控制器的责任是接受请求并提供正确的返回响应。

控制器的概念和 MVC(里面的 C)是一样的。但是有一些约束和约定。

原则:

  • 控制器不应该包含任何业务逻辑。
  • 控制器只做以下工作:
    1. 读取请求数据
    2. 调用 Action
    3. 返回响应数据
  • 控制器不能调用 Task,只能调用 Action。
  • 控制器只能被路由调用。
  • 各个 Container 的 UI 文件夹都有各自的控制器。

请求(Requests)

Requests 是用来处理用户的输入,经常用来处理表单验证和身份验证。

Requests 是处理验证的最佳地点,表单验证的规则可以被应用到每个请求。

Request 还可以用来做权限认证,比如判断一个用户是否有做某个操作的权限。

原则:

  • Requests 可以放表单验证/身份认证 规则。
  • Requests 只能在控制器里被注入使用,在注入时自动运行验证规则,如果不通过就立即抛出异常。
  • Requests 还可以被用来做权限认证,检查用户是否有权限来执行某个请求。

动作(Actions)

Actions 的责任是执行应用里的具体任务。

Actions 包含了业务逻辑。Actions 会通过调用 Task 来完成具体业务。

Actions 接受用户输入,按业务逻辑来处理,最终输出处理结果。

Actions 不应该关心数据是怎么来的,也不关心输出的结果会用到哪里。

查看 Container 里的 Actions 文件夹,就基本知道这个 Container 的功能了。看了所有的 Actions,你就能知道整个应用的功能了。

原则:

  • 每个 Action 只处理一个用例。
  • 一个 Action 可以从一个 Task 里取到结果并传入另一个 Task。
  • 一个 Action 可以调用多个 Task。
  • Action 的数据返回给控制器。
  • Actions不能直接返回请求响应,这是控制器的工作。
  • 一个 Action 可以调用另外一个 Action,但是不建议这么做。
  • Action 主要供控制器调用,但是也可以用于 Event,Command 或者其他类。但是不应该被 Task 调用。
  • 每个 Action 只有一个名为 run 方法。

任务(Task)

Task 是用来存放 Action 之间共享的业务逻辑片段。

每个 Task 实现一小部分业务逻辑。

Task 不是必须的,但很多时候你会发现你需要它们。

比如说:我们在 Action 1 里需要通过 ID 找到一条记录,然后触发事件。
在 Action 2 里需要通过 ID 找到一条记录,然后把它传给一个 API。
在两个 Action 里,我们都需要“通过 ID 找到一条记录”功能,我们就可以把这个做成一个 Task。

你发现你想要重用 Action 里的代码时,就应该考虑是不是可以用 Task。

原则:

  • 每个 Task 只完成一个动作。
  • Task 不可以调用其他 Task,这样会引发服务架构的混乱。
  • Task 不能调用 Action。
  • Task 应该只被 Action 调用(不一定是同一个 Container)
  • Task 一般只包含一个名为run 的方法,不过你要是愿意也可以有多个。
  • Task 不能被控制器调用。

模型(Model)

Models 提供数据。 (MVC 里的 M).

Model 负责处理数据,保证数据能被正确的存取。

原则:

  • Model 里不能有业务逻辑。
  • 一个控制器可以有多个模型。
  • Model 可以定义和其它 Model 之间的关系。

视图(Views)

View 里包含了应用里会用到的 HTML 部分。

View 最重要的功能是将数据和表现分离(MVC 里的 V)。

原则:

  • Views应该只能用于 Web 控制器。
  • 根据实际情况,Views 可以被分开存放在多个文件或文件夹。
  • 一个控制器可以有多个 Views 文件。

转换器(Transformers)

Transformers(是响应转换器的缩写)。

有点像是用于 JSON 响应的 Views,Views 是用来渲染 HTML,Transformers是用来格式化 JSON 数据。

Transformers 是把 Model转换成数组的类。

Transformers 把一个 Model 或者一组 Model 序列化成一个数组。

原则:

  • 每个 API 请求都必须用 Transformer 来格式化响应数据。
  • 每个 Model 都应该有一个对应的 Transformer。
  • 一个 Container 可以有多个 Transformer。

子动作(Sub-Actions)

Sub-Action 是用来减少 Action 里的重复代码。不要疑惑,Sub-Action 和 Task 不一样。

Task 是 Action 之间共享的功能,SubActions 则是 Action 之间共享的一系列 Task。

Example: If an Action is calling Task1, Task2 and Task3. And another Action is calling Task2, Task3 and Task4. (And both Actions are throwing the same Exception when Task2 returns Foo). Would be very useful to extract that code into a SubAction, and make it reusable.

原则:

  • Sub-Action 必须调用 Task,如果 Sub-Action 不需要 Task 就完成了所有的业务逻辑,那它就不算是一个 Sub-Action。
  • Sub-Action 可以从一个 Task 里获取数据,再传到另一个 Task。
  • 一个 Sub-Action 应该调用多个 Task。
  • Sub-Action 应该返回数据给 Action。
  • Sub-Action 不能直接返回请求响应。(那是控制器的活)
  • Sub-Action 不能调用其他的 Sub-Action。
  • Sub-Action 一般用于 Action,但是也可以用于 Event,Commands或者其他类。但是不能用于 Controller 和 Task。
  • 每个 Sub-Action 应该只有一个名为 run的方法。
  • 如果你能看到这一句,说明你真的认真看了,也不枉我打了那么多字,谢谢,第一个看到这里的@我,给你发红包。

典型的 Conainer 示例:

Container
    ├── Actions
    ├── Tasks
    ├── Models
    ├── Events
    ├── Policies    
    ├── Exceptions
    ├── Contracts
    ├── Traits
    ├── Jobs
    ├── Notifications
    ├── Providers
    ├── Configs
    ├── Data
    │   ├── Migrations
    │   ├── Seeders
    │   ├── Factories
    │   ├── Criterias
    │   └── Repositories
    ├── Tests
    │   ├── Unit
    │   └── Traits
    └── UI
        ├── API
        │   ├── Routes
        │   ├── Controllers
        │   ├── Requests
        │   ├── Transformers
        │   └── Tests
        │       └── Functional
        ├── WEB
        │   ├── Routes
        │   ├── Controllers
        │   ├── Requests
        │   ├── Views
        │   └── Tests
        │       └── Acceptance
        └── CLI
            ├── Routes
            ├── Commands
            └── Tests
                └── Functional
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 4年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 6

我们的项目已经使用apiato开始架构,请问下您在使用apiato的过程中是否发现有什么问题可以分享一下吗?

4年前 评论

@lxalano 我们还没有将这个正式用在产品上, 只是个人在研究, 欢迎讨论

4年前 评论

@dinghua 我们在这个架构中遇到的小坑小纠结都解决了,最大的问题卡在了接口规范上。apiato这个架构完全遵循了RESTful API的规范,在国内很多前端不认可这个规范调试起来也比较费力。

4年前 评论

@lxalano 准备采用,请问一下有什么样的小坑小纠结呢?

4年前 评论

在使用apiato框架的时候我发现请求数据很慢,一个接口大概需要3-5秒左右,我用的是laradock容器,不知道是什么原因!大家有没有遇到过?

3年前 评论

点赞
看完之后,觉得在自己的项目里面应该加入一个task层,来辅助一下service。
谢谢啦。

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!