让你的后端层越薄越好

2020-11-16 05:20:03

你需要在你的后台(在云中运行)和你的前端(在用户的设备上运行,无论是以移动应用还是web应用的形式)之间提供一个API。

我们尝试通过识别系统中的域对象以及将对其执行的操作来定义这样的API,让后端和前端团队就这些名词和动词达成一致,让后端团队实现API,并由前端团队使用它们。

在三层前端-后端-数据库体系结构中,尝试使用尽可能薄的中间层。或者甚至通过直接将数据库暴露给前端来完全消除后端层。

例如,如果API返回Person对象,只需返回底层表1:1的字段。执行SELECT*,迭代所有列,并将它们复制到将发送回前端的JSON。向数据库添加列时,这样的后端层不需要更改。相反,只选择SQL查询中的某些列。或者只将SQL响应中的某些列复制到JSON中,如下所示:

无论何时添加新的数据库列,都需要更改此代码。当--而不是如果--你忘记了,它会导致不必要的混淆和调试:

前台工程师:为什么我没有看到前台的电话号码栏?您没有将该列添加到数据库中吗?后台工程师:是的,我是这么做的。前台工程师:你没填上吗?后台工程师:是的,我是这么做的。前台工程师:那是怎么回事?后端工程师:您提出的请求是正确的吗?前台工程师:是的,看看这段代码…。后台工程师:看起来是正确的,但可能在运行时参数有误。你为什么不给我看看真正发出的请求呢?前台工程师:在这里。后台工程师:嗯,…。这个请求是正确的。我们如何确定后端不会返回电话号码。也许是这样的,但是在前端代码…的某一层中,它被遗漏了。前台工程师:我不这么认为,因为…。后端工程师:可以肯定的是,最好使用Postman进行测试。前台工程师:*去测试*看,这是API请求和响应。后台工程师:嗯,让我看看,也许几天后,我现在正忙着做别的事情。(两天后)哦,后端API实现需要更新才能添加新的字段。

通过使后端成为直通通道,您可以绕过整个效率低下的问题。这让你少了一件担心的事情,这是任何工程师都会欢迎的。

在API文档中,不要逐个列出要返回的字段及其类型。取而代之的是“返回与数据库相同的字段”。同样,出于同样的原因:它可能--不,将--过时,并且误导性的文档对您的公司有负面价值。相反,让前端工程师直接参考数据库模式,它不会过时,因为它是真相的来源。

这是一层不会增加价值的翻译。这使得工程师更难获得系统的端到端视图,比如他们是否好奇姓名是以独立的名字和姓氏字段表示,还是以单个全名字段表示。不仅如此,这样的翻译层还会有需要修复的错误。

您可以更进一步,直接将数据库公开给客户端应用程序,例如让客户端应用程序只将SQL查询发送到服务器执行。如果走到这一步,您甚至不会拥有后端层--而不是三层前端后端数据库体系结构,而是两层前端数据库体系结构。

现在,这可能并不总是可能的。比如说安全性--你不想让一个受到威胁的客户获得其他客户数据的访问权,或者让一个客户在没有付费的情况下将自己标记为高级计划中的一员。或者,可能某些代码需要访问客户端上不可用的资源。或者要求太高而不能在客户机上运行。或者需要在特定时间运行,而不管客户端设备当时是否在线。

所以,在没有后端的情况下,一个瘦的后端可能是你的正确选择。也许您可以授予客户端对数据库的只读访问权限,但不能授予其写入权限。或者,您可以只授予客户端对其数据的读写访问权限。问问你自己,你怎么会懒惰,把尽可能多的工作推给数据库。现代后端框架使用的数据库能力可能只有5%。这是不幸的。

作为仅直接公开数据库中部分数据的示例,假设您正在构建一个社交网络,其中的朋友信息是公开的。假设您将其建模为一个Friends表,该表由两列组成,其中包含两个好友的用户ID。现在,传统的方法是让你为前台定义一个后端API来交朋友。相反,您可以定义一个API,该API从客户端获取SQL查询,运行它,然后返回结果。此查询可以在仅具有读取访问权限的数据库用户帐户下运行,该数据库用户帐户也仅具有对FRIENDS表的访问权限。这可以适应不可预见的用途,比如获取好友数量,而不是实际的列表。或者,如果有一个搜索框,用户可以在其中过滤他的朋友,你可以进行Like查询。或者,您可以将返回的好友数量限制为在用户界面中显示的数量。或者只返回一些字段,即您将在前端使用的字段。或者只过滤有电话号码的朋友,比如你是否正在构建某种拨号器用户界面。所有这些灵活性现在都是可用的,而您不必预见、规划和构建它。不要让后端层成为数据库和前端之间的紧身衣,就像你不会强迫与会者通过摩尔斯电码进行交流一样。

SQL强大、简单、声明性强,大多数工程师都知道这一点--没有其他任何东西能带来这些好处。当然不是您的定制API。Greenspun的第十条编程规则说,任何足够复杂的C或Fortran程序都包含一个临时的、非正式指定的、充满错误的、缓慢实现的Common Lisp的一半。类似地,您的自定义API包含半个SQL的临时的、非正式指定的、充满错误的、缓慢的实现。至多一半。不要定义如此贫乏的API。

这是一种新的思维方式:在有事情要做的时候构建一个后端API,而不是作为对数据库的传递。这与您不会定义一个对象来包装另一个对象并转发对该对象的所有方法调用和返回值,而不做任何其他操作的原因是相同的。

无论如何,您都会在设计数据库模式时投入大量心思。如果不是,就应该这么做,因为这比后端或前端代码更难更改。但是,一旦这样做了,为什么还要设计另一个模式,即后端返回的响应呢?

现在,您可能不同意上面的建议,因为您反对理想的API不应该在后端数据库更改的情况下更改。理想的API应该是抽象层。但是,您不需要在第一天就进行所有可能的抽象层。抽象层的设计和实现需要时间,而且很难重构功能并将其转移到另一个层。

抽象层甚至可能需要雇佣一个单独的后端团队,这反过来又会增加团队之间的协调成本,这种成本永远不会消失。让每个额外的人都加入进来并充分发挥效率需要一年的时间。让团队编写一个规范来就它们之间的接口达成一致,实现这个规范,处理由于后端没有实现规范或前端假定规范不能保证的东西而导致的错误,在需要更改时更新这一层等等,这些都是更大的开销。

取而代之的是,让你的前端(包括移动)工程师负责API。后端工程师应该在那里提供建议、协助、实现和指出前端工程师忽略的事情,而不是定义API。让前端团队负责API,而不是让前端团队和后端团队建立平等的合作伙伴关系。

例如,假设您的前端工程师提议,不是先用API来吸引人,再用第二个API来获得订单,再用第三个API来获得其他东西,他们将用一个API来一次性获得渲染主页所需的所有数据。让他们为所欲为吧。事实上,这使前端工程师不必进行多次调用来获取他们想要呈现页面并将其组合在一起的数据。这进而导致了更复杂的问题,比如如何处理部分失败,其中一个调用成功,另一个调用失败。此外,如果像幼稚的实现那样一个接一个地进行调用,则会增加延迟。最好是把它们放在一起。除非第二个API依赖于第一个API。当您定义能够满足您的即时需求的API(如获取主页数据)时,所有这些复杂性就不复存在了。如果你想要一辆车,你不会想买一个发动机、方向盘、刹车和其他部件,然后自己把它们组装成一辆车。

每当主页UX发生更改,并且需要不同的信息时,让API更改。好的API不应该改变,但当您控制API的两端时,这并不重要[1]。内部API不应与公共API遵循相同的标准。如果你试图这样做,你就过度设计了它。走捷径。你是在一个背景下进行设计,而不是在真空中,所以不利用那个背景是愚蠢的。

事实上,即使使用同一术语API来指代两个完全不同的事物-内部API和公共API-也是有问题的,因为其中一个的含义并不适用于另一个。这就像用车辆这个词来指代自行车和核潜艇,并试图将潜艇设计中的一些原则应用到自行车上。

如果你把拥有一个尽可能小的后端的想法推向它的逻辑结论,你最终会得到一个Baas,就像Firebase一样。这个架构背后蕴含着很多智慧。如果我要开始一种新产品,Baas会是我的第一选择。只有在我确定巴斯不会为我工作的情况下,我才会使用其他人。

如果Baas对您不起作用,可以尝试PostGraphile或Hasura等接受数据库并自动生成API服务器的工具,通过GraphQL公开数据库内容。或者PostgREST,它类似,但是会自动生成一个REST服务器,如果您不需要GraphQL的话。

最终,您可能会发现这种架构已经无法满足您的需求,需要一个更重的后端。现在是建造一个的时候,而不是在此之前。这适用于很多事情--通常情况下,您不需要进行自动化测试。或微服务。或者正式的人力资源政策。或者很多事情。不要效仿谷歌或亚马逊。考虑到你所处的位置,做对你合适的事。