追梦App系列博客——后端架构篇

简介: 追梦App系列博客——后端架构篇

前言

聊完追梦App的设计思路后我们来聊聊后端架构。


一、架构选择

此时我们面临几种架构选择。


单体架构


单体架构是最简单的软件架构,常用于传统的应用软件开发以及传统Web应用。传统Web应用,一般是将所有功能模块都打包(jar,war)在一个Web容器(JBoss、Tomcat)中部署、运行。随着业务复杂度增加、技术团队规模扩大。在一个单体应用中维护代码,会降低开发效率。即使是处理一个小需求,也需要将所有机器上的应用全部部署一遍,增加了运维的复杂度。


服务化架构(SOA架构)


当某一天使用单体架构发现很难推进需求的开发、以及日积月累的技术债,很多企业会开始做单体服务的拆分。拆分的方式一般有水平拆分以及垂直拆分。垂直拆分把一个应用拆成松耦合的多个独立的应用,让应用可以独立部署,有独立的团队进行维护。水平拆分把一些通用的,会被很多上层服务调用的模块独立拆分出去,形成一个共享的基础服务,这样拆分可以对一些性能瓶颈的应用进行单独的优化和运维管理,也一定程度防止了垂直拆分的重复造轮子。

SOA也叫面向服务的架构,从单体服务到SOA的演进,需要结合水平拆分以及垂直拆分。SOA强调用统一的协议进行服务间的通信,服务间运行在彼此独立的硬件平台但是需通过统一的协议接口相互协作,也即将应用系统服务化。举个易懂的例子,单体服务如果相当于一个快餐店,所有的服务员都是一样的,又要负责收银结算,又要负责做汉堡,又要负责端盘子,又要负责打扫,服务员之间不需要有交流,一个用户来了一个服务员从前到后负责到底。SOA相当于让服务员有职责分工,收银员负责收银,厨师负责做汉堡,保洁阿姨负责打扫等等。所有服务员需要同一种语言交流,方便工作协调。


微服务架构


微服务可以在“自己的程序”中运行,并通过“轻量级设备与HTTP型API进行沟通”。关键在于该服务可以在自己的程序中运行。通过这一点我们就可以将服务公开与微服务架构(在现有系统中分布一个API)区分开来。在服务公开中,许多服务都可以被内部独立进程所限制。如果其中任何一个服务需要增加某种功能,那么就必须缩小进程范围。在微服务架构中,只需要在特定的某种服务中增加所需功能,而不影响整体进程的架构。


首先,我们要明确的一点是——脱离实际业务谈架构的都是耍流氓!


我们先回顾下设计思路,我们要完成哪些功能。


追梦体系、个人账户成长系统、任务系统、虚拟货币系统、社交系统、学习系统、学习工具…


可以看到我们如果要把这款软件做好的话,实际上体量还是有点大的,而且业务对服务器要求比较高,这样一看,分布式微服务确实是个不错的选择。


诚然,分布式微服务架构确实是个不错的选择,但我们也该考虑自己的实际情况:

1.这款App首先是去参加比赛的,所以必须快速出产品,最起码要把基本功能做完。

2.分布式微服务的成本太高,我们大学生暂时无力承担服务器费用。

3.初期用户量小,分布式微服务并不合适。

4.小组成员技术参差不齐,学过分布式微服务的就只有我一个。


综上,分布式微服务对于初期的开发来说并不合适,单体架构对于初期的我们来说会更合适些。但我们就不用分布式微服务去架构吗?


不,初期确实不适合用分布式微服务去开发,但是等到我们做出点成绩后,等到我们App用户量增大的时候,我们再去拆分服务,用分布式微服务重构项目。


这确实会花费些时间,但只要我们前期有拆分服务的思想,有意的去区分服务,这样后期拆分起来也会容易很多。


二、技术选型

前端:Flutter

后端框架:SpringBoot、SSM

数据库:mysql

数据库连接池:Druid

服务器:Tomcat

JDK:8

开发工具:IDEA


三、项目结构

之前架构选择中说到我们选择单体架构,但是又要有服务化的趋势,这样才能让服务拆分变得简单容易。


那么怎么设计呢?


初步想法是分业务大致拆分出来,有人可能马上想到用多模块,因为多模块结构清晰,可以很好的区分业务。


之前我也曾想过单体架构多模块项目的结构,但我感觉其实没有必要,既然不是分布式微服务架构,就没必要分模块,因为这要会增加模块间交互的成本,得不偿失。


多包完全可以达到之前想要的效果。


所以我计划如下图这样来构建项目:



q1.png

通过业务来区分包名:

common:存放实体类以及一些公共的接口文件

blog:发布博客、动态的业务

dream:追梦体系

social:社交业务

study:学习系统业务

user:个人管理中心,用来管理账户信息

(开发可能不完全按照这个,因为我总感觉这样弄怪怪的)


四、数据库表设计

此部分日后再补,需要和组员讨论一下。


五、前后端交互规范

以下摘自前后端接口规范


具体需要定义哪些接口, 可以按照下面的思路来整理


资源接口: 系统涉及到哪些资源, 按照 RESTful 方式定义的细粒度接口

操作接口: 页面涉及到哪些操作, 例如修改购物车中商品的数量, 更换优惠券等等, 也可以使用 RESTful 方式来定义

页面接口: 页面涉及到太多接口, 如果是一个个地调用, 会需要很多次请求, 有可以影响到前端的性能和用户感知(特别是首屏的体验), 因此可能需要将这些接口的数据合并到一起, 作成一个聚合型接口提供给前端来使用

接口协商要点

接口必须返回统一的数据结构, 参考后端接口通用规范中接口返回的数据结构

接口查询不到数据时, 即空数据的情况下返回给前端怎样的数据 * 建议返回非 null 的对应数据类型初始值, 例如对象类型的返回空对象({}), 数组类型的返回空数组([]),

其他原始数据类型(string/number/boolean…)也使用对应的默认值 *

这样可以减少前端很多琐碎的非空判断, 直接使用接口中的数据 * 例如: result.fieldName * 如果

result 为 null, 可想而知会报错 Uncaught TypeError: Cannot read property 'fieldName' of null

调用接口业务失败的常用错误码, 例如未授权时调用需要授权的接口返回 "status": 1

接口需要登录时如何处理, 特别是同时涉及到 Web 端/微信端/App 端, 需要前端针对运行环境判断如何跳转到登录页面

返回数据中图片 URL 是完整的还是部分的 * http://a.res.com/path/to/img.png 这就是完整的, 前端直接使用这个 URL * /path/to/img.png 这就是部分的, 一般省略域名部分, 前端需要自己拼接后才能使用

'http://a.res.com' + '/path/to/img.png'

返回数据中页面跳转的 URL 是给完整的还是部分的 * 内部页面返回部分的, 或者只给ID, 由前端自己拼接, 例如只给出商品ID, 让前端自己拼接商品详情页的 URL * 外部页面返回完整的, 例如广告位要跳转去谷歌

返回数据中日期的格式, 是使用时间戳还是格式化好的文字 * 对于需要前端再次处理的日期值(例如根据日期计算倒计时), 可以使用时间戳(简单暴力), 例如: 1458885313711, 或者参考

Date.prototype.toJSON

提供 ISO 标准格式(例如需要考虑时区时) * 对于纯展示用的日期值, 推荐返回为格式化好的文字, 例如: 2017年1月1日

对于大数字(例如 Java 的 long 类型), 返回给前端时需要设置为字符串类型, 否则 JavaScript 会发生溢出, 造成得到的数值错误 * 例如: 返回 JSON 数据 {"id": 362909601374617692} 前端拿到的值却是:

362909601374617660

分页参数和分页信息 * 如何限制只返回 N 条数据(limit 参数) * 如何控制每页的数据条数(pageSize 参数) * 如何加载某一页的数据(page 参数)

第一页是从 0 开始还是从 1 开始 * 如何避免无限滚动加载可能出现的重复数据(采用 lastId 分页方式, 来避免传统分页方式的弊端)

假设数据是按照新增时间倒序排列的

首先加载 2 页的数据

等了很久

期间新增了很多数据

再获取第 3 页数据

此时就可能出现重复数据的情况, 因为新增的数据都排在最前面, 后面会接着已经加载过数据 * 分页信息包含什么(total, page, pageSize) * 分页信息何时表明已经是最后一页了

请求某页数据时返回的数据条数 < pageSize

请求某页数据时返回的数据条数 = 0

如果碰巧最后一页有 pageSize 条数据, 前端无法通过数据条数来判断已经处于最后一页了

接口定义

所有的接口定义在项目前端静态文件目录的 _mockserver.json 文件中, 启动 puer-mock 服务,

即可使用这些接口获得符合规范的假数据, 也可以查看接口文档.


具体 puer-mock 的详细使用手册和 _mockserver.json 如何配置接口请参考 puer-mock

项目, 或者参考项目中已经配置好的其他接口.


接口协作

由于接口规范的定义和接口的实际实现是分开的两个部分, 而且涉及到多人协作, 因此在开发过程中可能出现接口规范与实现不同步,

最终造成实际的接口不符合规范的定义, 接口规范就会慢慢失去存在的意义.


为了尽量避免这种问题, 后端在实现接口的过程中应该确保与接口规范保持一致, 一旦出现分歧, 必须同步修改接口规范, 尽可能保持沟通。


后端接口通用规范

接口地址和请求方式

接口根路径 - Root

Endpoint 推荐为:

http://api.yourdomain.com 或者 http://yourdomain.com/api


接口地址即接口的 URL, 定义时使用相对路径(即不用带上域名信息), 建议分模块来定义, 推荐 REST 风格, 例如


GET /user/:id 表示获取用户信息

POST /user 表示新增用户

接口参数

向接口传递参数时, 如果是少量参数可以作为

URL query string 追加到接口的 URL 中, 或者作为 Content-Type: application/x-www-form-urlencoded 放在请求体(body)中(即表单提交的方式)


对于复杂的接口参数(例如嵌套了多层的数据结构), 推荐在 HTTP 请求体(body)中包含一个 JSON 字符串作为接口的参数,

并设置 Content-Type: application/json; charset=utf-8.


例如


查询 VIP 用户的接口
charset=utf-8
{
    "name": "hanmeimei",
    "isVip": true } ```
### 接口返回的数据结构
返回的响应体类型推荐为 `Content-Type: application/json; charset=utf-8`, 返回的数据包含在
HTTP 响应体中, 是一个 JSON Object. 该 Object 可能包含 3 个字段 `data`, `status`,
`statusInfo`
```HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8
{
    "data": {},
    "status": 0,
    "statusInfo": {
        "message": "给用户的提示信息",
        "detail": "用于排查错误的详细错误信息"
    } } ```
**字段名** |**字段说明** :----------|:----------- data       | **业务数据**<br>必须是任意 JSON 数据类型(number/string/boolean/object/array).<br>推荐始终返回一个 object
(即再包一层)以便于扩展字段.<br>例如: 用户数据应该返回 `{"user":{"name":"test"}}`, 而不是直接为
`{"name":"test"}` status     | **状态码**<br>必须是 `>= 0` 的 JSON Number
整数.<ul><li>`0` 表示请求处理成功, 此时可以省略 `status` 字段, 省略时和为 `0`
时表示同一含义.</li><li>`非 0`
表示发生错误时的[错误码](http://open.weibo.com/wiki/Error_code "错误码格式可以参考微博API的
Error code"), 此时可以省略 `data` 字段, 并视情况输出 `statusInfo` 字段作为补充信息</li></ul>
statusInfo | **状态信息**<br>必须是任意 JSON 数据类型.<br>推荐始终返回一个 object 包含
`message` 和 `detail` 字段<ul><li>`message` 字段作为接口处理失败时,
**给予用户的友好的提示信息**, 即所有给用户的提示信息都统一由后端来处理.</li><li>`detail` 字段用来放置接口处理失败时的详细错误信息. 只是为了方便排查错误, 前端无需使用.</li></ul>
例如
* 接口处理成功时接口返回的数据
  ```{
      "data": "api result"
      "status": 0   }   ```
* 接口处理失败时接口返回的数据
  ```{
      "status": 1,
      "statusInfo": {
          "message": "服务器正忙",
          "detail": {
              "exception": "java.util.List"
          }
      }   }   ```
这样我们就可以非常容易地通过判断 status 来处理数据了 ```javascript if (!response.status) {
    // status 为 0 或者没有 status 字段时表示接口成功返回了数据
    console.log(response.data); } else {
    // 失败
    console.error(response.status, response.statusInfo);
    // 统一由服务端返回给用户的提示信息
    alert(response.statusInfo.message); } ```
## 错误码规范: `status` 字段该如何取值
采用前后端分离开发模式的项目越来越多, 前端负责调用后端的接口来展现界面, 如果有界面显示异常,
需要有快速方便的手段来排查线上错误和定位出职责范围
综合了经验总结和行业实践, 最简单有效的手段是制定出一套统一的错误码规范, 协助多方人员来排查出接口的错误
例如
* 用户发现错误, 可以截错误码的图, 就能够提供有效的信息帮助开发人员排查错误
* 测试人员发现错误, 可以通过错误码, 快速定位是前端的问题还是后端接口的问题
因此我们确定提示信息规范为: 当后端接口调用出错时, 接口提供一个用户可以理解的错误提示, 前端展示给用户错误提示和错误码, 给予用户反馈
对于错误码的规范, 参考行业实践, 大致有两种方案
* 做显性的类型区分, 快速定位错误的类别, 例如通过字母划分类型: `A101`, `B131`   * [Standard ISO Response
Codes](http://www.nexion.co.za/docs/merchant-access/user-manual/17.%20Standard%20ISO%20Response%20codes.pdf)
* 固定位数, 设定区间(例如手机号码, 身份证号码)来划分不同的错误类型   * [HTTP Status Code Definitions](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
"Most API services follow the HTTP error code system RFC2616 with
ranges of error codes for different types of error")   * [System Error
Codes](https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes)
具体实践如下
* **错误码固定长度**, 以区间来划分错误类型(例如 HTTP 的状态码)
  例如: 10404 表示 HTTP 请求 404 错误, 20000 表示 API 调用失败, 30000 代表业务错误, 31000
表示业务A错误, 32000 表示业务B错误
* **错误码可不固定长度**, 以首字母来划分错误类型, 可扩展性更好, 但实际运作还是需要划分区间
  例如: H404 表示 HTTP 请求 404 错误, A100 表示 API 调用失败, B100 表示业务A错误, B200
表示业务B错误
关于错误分类的原则, 我们可以根据发送请求的最终状态来划分
- 发送失败(即请求根本就没有发送出去)
- 发送成功
  - HTTP 异常状态(例如 404/500...)
  - HTTP 正常状态(例如 200)
    - 接口调用成功
    - 接口调用失败(业务错误, 即接口规范中 status 非 0 的情况)
### 最终规范
错误码可不固定长度, 整体格式为: `字母+数字`, `字母`作为错误类型, 可扩展性更好, `数字`建议划分区间来细分错误
例如:
- `A` for **API**: API 调用失败(请求发送失败)的错误, 例如 `A100` 表示 URL 非法
- `H` for **HTTP**, HTTP 异常状态的错误, 例如 `H404` 表示 HTTP 请求404错误
- `B` for **backend or business**, 接口调用失败的错误, 例如 `B100` 业务A错误, `B200` 业务B错误
- `C` for **Client**: 客户端错误, 例如 `C100` 表示解析 JSON 失败


发送 HTTP 请求
                                             ┌───────────┴───────────┐
                                          发送成功¹               发送失败²
                                             │                       │
                                  ┌──────────┴──────────┐            A 例如: A100
                            获得 HTTP 响应       无法获得 HTTP 响应³
                                  │                     │
                             HTTP status                A 例如: A200
                       ┌──────────┴──────────┐
                   HTTP 成功(200-300)     HTTP 异常
                       │                     |
           {data, status, statusInfo}        H${HTTP status} 例如: H404
           ┌───────────┴───────────┐
      接口调用成功(status:0)   接口调用失败
  ┌────────┴────────┐              | 客户端处理出错      客户端处理正常       B${status}${statusInfo.message} 例如: B100
  |
  C 例如: C100



发送成功¹: 服务端收到了 HTTP 请求并返回了 HTTP 响应

发送失败²: HTTP 请求没有发送出去(例如由于跨域被浏览器拦截不允许发送), 未到达服务端(即服务端没有收到这个 HTTP 请求)

无法获得 HTTP 响应³: 服务端收到了请求并返回了响应, 但客户端由于某些原因无法获得 HTTP 响应, 例如请求的超时处理机制 ```

统一错误提示

q1.png

q2.png

错误日志 * 接口调用出错(${错误码}) ${HTTP 方法} ${HTTP URL} ${请求参数} ${请求选项} ${请求返回结果} * 例如: 接口调用出错(H404) GET https://domain.com {foo: bar} {option1: 'test'} {status: 404}


给用户的提示消息(参考自 QQ 的错误提示消息) * 提示消息(错误码: xxx)




提示消息和错误码之间用换行隔开 * 错误码整块内容建议弱化使用灰色字 * 例如

规范实现:

weapp-backend-api


接口实现建议

接口实现的大方向建议遵循 RESTful 风格

HTTP 动词: 获取数据用 GET, 新增/修改/发送数据用 POST * 例如: 获取用户数据的接口用 GET, 修改用户数据的接口用 POST

对于资源的操作类型, 使用 HTTP 动词来指定, 减少接口 URL 的数量 * 例如: GET /contact 获取联系人, POST /contact 新增/修改联系人

对外的 ID 字段使用字符串类型 * 特别核心数据的 ID 字段, 不要使用自增的数字类型, 建议使用无规则的字符串类型(例如UUID), 避免核心数据被轻易抓取 * 避免使用大数字类型(Long),

因为前端可能承载不了这个精度而溢出得到另外一个数值 * 例如: Java 中的 Long 类型的数值:

362909601374617692, 作为 JSON 数据返回给前端, 前端拿到的值变成了 362909601374617660

接口字段建议同时给出 ID 字段和用于显示字段, 前端提交数据时只提交 ID 字段 * 例如: {"gender": 1, "genderText": "男"}

图片的 URL 建议返回完整的 URL * 例如: {"pic": "https://domain.com/a.png"}

时间字段建议同时返回时间戳的原始值(或 ISO 标准格式)和用于统一显示的格式化文本 * 由后端接口集中控制各端的显示, 提供的原始值兼顾前端的自定义显示或者计算(例如倒计时)的需求 * 避免每个端(例如H5/APP/小程序)都需要对时间做统一的格式化实现,

一旦需要调整, 需要各个端都调整一遍 * 例如: {"createTime": 1543195480357, "createTimeText": "2018年11月26日"}

统一分页的数据格式 * 分页请求的参数和分页结果的数据结构

针对此,我们后端统一了一个简单的数据响应格式:


package com.dreamchaser.dream.common.utils;
import java.io.Serializable;
import lombok.Data;
/**
 * 所有服务统一响应数据格式
 *
 * @author ye17186
 * @version 2019/2/15 14:40
 */
@Data
public class RestResponse<T> implements Serializable {
    /**
     * 业务数据
     */
    private Object data;
    /**
     * 状态码
     */
    private int status;
    /**
     * 状态信息
     */
    private StatusInfo statusInfo;
}
@Data
class StatusInfo{
    /**
     * 字段作为接口处理失败时, 给予用户的友好的提示信息, 即所有给用户的提示信息都统一由后端来处理
     */
    private String message;
    /**
     * 字段用来放置接口处理失败时的详细错误信息. 只是为了方便排查错误, 前端无需使用.
     */
    private String detail;
}



接口部分需和大家协商制定。这部分以后再补。



相关文章
|
19天前
|
API 数据库 开发者
构建高效可靠的微服务架构:后端开发的新范式
【4月更文挑战第8天】 随着现代软件开发的复杂性日益增加,传统的单体应用架构面临着可扩展性、维护性和敏捷性的挑战。为了解决这些问题,微服务架构应运而生,并迅速成为后端开发领域的一股清流。本文将深入探讨微服务架构的设计原则、实施策略及其带来的优势与挑战,为后端开发者提供一种全新视角,以实现更加灵活、高效和稳定的系统构建。
23 0
|
1月前
|
负载均衡 测试技术 持续交付
高效后端开发实践:构建可扩展的微服务架构
在当今快速发展的互联网时代,后端开发扮演着至关重要的角色。本文将重点探讨如何构建可扩展的微服务架构,以及在后端开发中提高效率的一些实践方法。通过合理的架构设计和技术选型,我们可以更好地应对日益复杂的业务需求,实现高效可靠的后端系统。
|
1月前
|
监控 Kubernetes 持续交付
构建高效可扩展的微服务架构:后端开发实践指南
在数字化转型的浪潮中,企业对软件系统的要求日益提高,追求快速响应市场变化、持续交付价值成为核心竞争力。微服务架构以其灵活性、模块化和独立部署的特点,成为解决复杂系统问题的有效途径。本文将深入探讨如何构建一个高效且可扩展的微服务架构,涵盖关键设计原则、技术选型及实践案例,为后端开发者提供一条清晰的指导路线,帮助其在不断变化的技术环境中保持竞争力。
133 3
|
7天前
|
消息中间件 监控 持续交付
构建高效微服务架构:后端开发的进阶之路
【4月更文挑战第20天】 随着现代软件开发的复杂性日益增加,传统的单体应用已难以满足快速迭代和灵活部署的需求。微服务架构作为一种新兴的分布式系统设计方式,以其独立部署、易于扩展和维护的特点,成为解决这一问题的关键。本文将深入探讨微服务的核心概念、设计原则以及在后端开发实践中如何构建一个高效的微服务架构。我们将从服务划分、通信机制、数据一致性、服务发现与注册等方面入手,提供一系列实用的策略和建议,帮助开发者优化后端系统的性能和可维护性。
|
26天前
|
存储 缓存 监控
构建高效可扩展的后端服务架构
在当今互联网时代,构建高效可扩展的后端服务架构对于企业的业务发展至关重要。本文将探讨如何通过合理设计和优化后端服务架构,实现系统的高性能、高可用性和易扩展性,从而满足不断增长的业务需求和用户规模。
18 0
|
28天前
|
监控 Java 开发者
构建高效微服务架构:后端开发的新范式
在数字化转型的浪潮中,微服务架构以其灵活性、可扩展性和容错性成为企业技术战略的关键组成部分。本文深入探讨了微服务的核心概念,包括其设计原则、技术栈选择以及与容器化和编排技术的融合。通过实际案例分析,展示了如何利用微服务架构提升系统性能,实现快速迭代部署,并通过服务的解耦来提高整体系统的可靠性。
|
1月前
|
监控 数据管理 API
构建高效微服务架构:后端开发的新趋势
在现代软件开发领域,随着业务需求的不断复杂化以及敏捷迭代的加速,传统的单体应用架构逐渐暴露出其局限性。微服务架构作为一种新的解决方案,以其高度模块化、独立部署和可扩展性,正成为后端开发领域的新趋势。本文将探讨微服务架构的核心概念,分析其优势与面临的挑战,并提供实施高效微服务的策略和最佳实践,帮助读者理解如何利用这一架构模式提升系统的可靠性、灵活性和可维护性。
137 5
|
1月前
|
人工智能 运维 监控
构建高性能微服务架构:现代后端开发的挑战与策略构建高效自动化运维系统的关键策略
【2月更文挑战第30天】 随着企业应用的复杂性增加,传统的单体应用架构已经难以满足快速迭代和高可用性的需求。微服务架构作为解决方案,以其服务的细粒度、独立性和弹性而受到青睐。本文将深入探讨如何构建一个高性能的微服务系统,包括关键的设计原则、常用的技术栈选择以及性能优化的最佳实践。我们将分析微服务在处理分布式事务、数据一致性以及服务发现等方面的挑战,并提出相应的解决策略。通过实例分析和案例研究,我们的目标是为后端开发人员提供一套实用的指南,帮助他们构建出既能快速响应市场变化,又能保持高效率和稳定性的微服务系统。 【2月更文挑战第30天】随着信息技术的飞速发展,企业对于信息系统的稳定性和效率要求
|
1天前
|
JSON API 数据库
后端架构设计与优化:打造高性能应用后端
后端架构设计与优化:打造高性能应用后端
10 2
|
1天前
|
消息中间件 负载均衡 持续交付
构建高效微服务架构:后端开发者的终极指南
【4月更文挑战第25天】在当今软件工程领域,微服务架构已经成为实现可扩展、灵活且容错的系统的首选模式。本文将探讨如何从零开始构建一个高效的微服务系统,涵盖关键组件的选择、通信机制、数据管理以及持续集成和部署策略。通过深入分析与案例研究,我们旨在为后端开发者提供一个全面的微服务实践指南,帮助他们在构建现代化应用时做出明智的架构决策。