写点什么

如何使用统一架构简化全栈开发

  • 2020-01-12
  • 本文字数:4850 字

    阅读完需:约 16 分钟

如何使用统一架构简化全栈开发

现代的全栈应用程序通常由六层组成:数据访问、后端模型、API 服务端、API 客户端、前端模型和用户界面。我们需要大量的胶水代码才能将它们全部连接起来,并且领域模型在整个栈中存在重复。因此,开发的敏捷性受到了极大的影响。本文如何使用统一架构来构建全栈应用程序,以及统一架构语言扩展 Liaison。



现代的全栈应用程序(例如,单页应用程序或移动应用程序)通常由六层组成:


  • 数据访问

  • 后端模型

  • API 服务端

  • API 客户端

  • 前端模型

  • 用户界面


通过这种架构方式,我们可以实现某些设计良好的应用程序特性,例如关注点分离(separation of concerns,SoC)耦合(loose coupling)


但它也并非没有缺点。它通常是以牺牲其他一些重要特性为代价的,比如简单性、内聚性或敏捷性。


似乎我们不可能拥有上述全部特性。我们必须妥协。


但问题在于,通常每一层都是作为一个完全不同的世界被单独构建的。


即使这些层都是使用相同的语言实现的,它们之间也不能很容易地通信和共享。


我们需要大量的胶水代码才能将它们全部连接起来,并且领域模型重复地存在于整个栈中。因此,开发的敏捷性受到了极大的影响。


例如,向模型中添加一个简单的字段通常需要修改栈中的所有层。难道您不觉得这有点可笑吗?


最近我一直在思考这个问题,我相信我已经找到了解决的办法。


诀窍在于:当然,应用程序的层必须是“物理上”的分割,但不需要是“逻辑上”的分割。

统一架构


在面向对象编程中,当我们使用继承时,我们可以得到一些类,并从两种角度来观察它们:物理和逻辑。这是什么意思呢?


假设我们有一个继承自 A 类的 B 类,那么,可以将 A 和 B 看作是两个物理类。但是在逻辑上,它们并不是分离的,B 可以被看作是一个逻辑类,它是由 A 的属性和其自身的属性组成的。


例如,当我们在类中调用某个方法时,我们不必担心这个方法是在这个类中实现的还是在它的父类中实现的。从调用方的角度来看,只需要担心一个类即可。父类和子类被统一成一个逻辑类了。


如何将相同的方式应用到应用程序的各个层中呢?例如,如果前端可以以某种方式从后端继承,这不是很好吗?


这样做,前端和后端将被统一到一个单一的逻辑层,这将消除所有通信和共享问题。实际上,可以从前端直接访问后端的类、属性和方法。


当然,我们通常不希望将整个后端都暴露给前端。但是类继承也是如此,并且它有一个优雅的解决方案叫做“私有属性”。类似地,后端也可以有选择地暴露一些属性和方法。


能够从一个统一的世界中掌握应用程序的所有层并不是一件小事。它完全改变了游戏规则。这就像是从三维世界降到二维世界。一切都变得容易多了。


继承并不邪恶。是的,它可能被误用了,并且在某些语言中,它可能非常僵化。但是,如果使用得当,它会是我们的工具箱中的一种宝贵机制。


不过,我们有个问题。据我所知,没有一种语言允许我们可以跨多个执行环境继承类。但我们是程序员,不是吗?我们可以构建我们所需的一切,并且我们可以扩展语言来提供新的功能。


但在我们开始之前,让我们先对技术栈进行下分解,看看每层应该如何适用于统一架构。

数据访问

对于大多数应用程序,可以使用某种 ORM 来对数据库进行抽象。因此,从开发人员的角度来看,无需担心数据访问层。


对于更复杂的应用程序,我们可能必须优化数据库模式和请求。但我们不想因为这些问题而使后端模型变得混乱,因此此处可能需要额外附加一层。


我们构建一个数据访问层来实现优化关注点,而这通常发生在开发周期的后期(如果真的会发生的话)。


不管怎样,如果我们需要这样一个层,我们可以稍后再构建它。通过跨层继承,我们可以在后端模型层上再添加一个数据访问层,而这几乎不需要对现有代码进行任何更改。

后端模型

通常,后端模型层具有如下职责:


  • 塑造领域模型。

  • 实现业务逻辑。

  • 处理授权机制。


对于大多数后端,最好在一个单一层中实现它的全部职责。但是,如果我们希望单独处理一些关注点,例如,如果我们希望将授权与业务逻辑分开,那么我们可以在两个相互继承的层中实现它们。

API 层

为了连接前端和后端,我们通常会构建一个 Web API(REST、GraphQL 等),这会使一切变得复杂。


Web API 必须在两侧都实现:前端是 API 客户端,后端是 API 服务端。这就是需要担心的两个额外层,并且它通常会导致需要复制整个领域模型的后果。


Web API 无非就是胶水代码,并且构建起来非常麻烦。所以,如果我们能避免,这将是一个巨大的进步。


幸运的是,我们可以再次利用跨层继承。在统一架构中,不需要构建 Web API。我们所要做的就是让前端模型从后端模型中继承,这样就完成了。


然而,仍然存在一些需要构建 Web API 的很好用例。这时,我们需要向某些第三方开发人员公开后端,或者需要与某些遗留的旧系统进行集成。


但是说实话,大多数应用程序都没有这样的需求。而当它们需要这样做时,事后处理也很容易。我们可以简单地将 Web API 实现到继承自后端模型层的新层中。


关于这个主题的更多信息可以在这篇文章中找到。

前端模型

因为后端是事实来源,所以它应该实现所有的业务逻辑,而前端不应该实现任何业务逻辑。因此,前端模型只是简单地继承自后端模型而已,几乎没有添加任何内容。

用户界面

我们通常是在两个独立的层中实现前端模型和 UI。但是,正如我在这篇文章中所展示的,它不是强制性的。


当前端模型由类构成时,可以将视图封装为简单的方法。如果您现在不明白我的意思,请不用担心,在后面的示例中我会给出更清楚解释。


由于前端模型基本上是空的(请参见上文),所以可以直接在其中实现 UI,因此技术栈本身就没有用户界面层了。


当我们想要支持多个平台(例如,Web 应用程序和移动应用程序)时,仍然需要在单独的层中实现 UI。但是,由于这只是继承一个层的问题,所以可以在开发路线图的后期进行。

将一切组装起来

统一架构使我们能够将 6 个物理层统一为 1 个逻辑层:


  • 在最小的实现中,数据访问被封装到了后端模型中,UI 也被封装到了前端模型中。

  • 前端模型继承自后端模型。

  • 不再需要 API 层。


结果如下图所示:



真是太壮观了,您不觉得吗?

Liaison

为了实现一个统一的架构,我们所需的只是跨层继承,而我是通过构建Liaison来实现这一点的。


如果您愿意的话,可以把 Liaison 看作一个框架,但是我更喜欢把它描述成一个语言扩展,因为它的所有特性都位于尽可能低的级别上:编程语言级别。


所以,Liaison 并不会把您锁定在一个预定义的框架中,而是可以在其之上创建一个完整的宇宙。您可以在这篇文章中阅读到更多关于此主题的信息。


在后台,Liaison 依赖于RPC机制。因此,从表面上看,它可以被看作是CORBAJava RMI.NET CWF之类的东西。


但是 Liaison 是完全不同的:


  • 它不是一个分布式对象系统。实际上,Liaison 的后端是无状态的,因此没有跨层的共享对象。

  • 它是在语言级别实现的(见上文)

  • 它的设计简单明了,并且公开了最少的 API。

  • 它不涉及任何样板代码、生成的代码、配置文件或工件。

  • 它使用了一个简单但功能强大的序列化协议(Deepr),该协议支持一些独特的特性,比如链式调用、自动化批处理或部分执行。


Liaison 始于 JavaScript,但是它所解决的问题是通用的,并且可以将它移植到任何面向对象的语言中而不会带来太多的麻烦。

Hello 计数器

让我们通过将经典的“计数器”示例实现成一个单页应用程序来说明 Liaison 是如何工作的吧。


首先,我们需要在前端和后端之间共享一些代码:


// shared.js
import {Model, field} from '@liaison/liaison';
export class Counter extends Model { // 共享类定义一个字段来跟踪计数器的值 @field('number') value = 0;}
复制代码


然后,构建后端以实现业务逻辑:


// backend.js
import {Layer, expose} from '@liaison/liaison';
import {Counter as BaseCounter} from './shared';
class Counter extends BaseCounter { // 我们将“value”字段暴露给前端 @expose({get: true, set: true}) value;
// 我同样将increment() 方法暴露给前端 @expose({call: true}) increment() { this.value++; }}
// 我们将后端类注册到导出层中export const backendLayer = new Layer({Counter});
复制代码


最后,让我们来构建前端:


// frontend.js
import {Layer} from '@liaison/liaison';
import {Counter as BaseCounter} from './shared';import {backendLayer} from './backend';
class Counter extends BaseCounter { // 目前,前端类只是继承共享类}
// 我们将前端类注册到一个继承自后端层的层中const frontendLayer = new Layer({Counter}, {parent: backendLayer});
// 最后,我们实例化一个计数器const counter = new frontendLayer.Counter();
// 运行计数器await counter.increment();console.log(counter.value); // => 1
复制代码


这是怎么回事呢?通过调用 counter.increment(),我们可以使计数器的值递增。请注意,increment()方法既没有在前端类中实现,也没有在共享类中实现。它只存在于后端。


那么,我们为什么能从前端调用它呢?这是因为前端类注册在从后端层继承的层中。因此,当前端类中缺少某个方法,而后端类中公开了具有相同名称的方法时,则会自动调用该方法。


从前端的角度来看,该操作是透明的。它不需要知道哪个方法被远程调用了。它只是调用。


实例的当前状态(即,counter 的属性)会被自动地来回传输。当方法在后端执行时,将发送在前端修改的属性。相反,当某些属性在后端发生变化时,它们也会反映到前端。


注意,在这个简单的示例中,后端并不是完全远程的。前端和后端都在同一个 JavaScript 运行时中运行。为了使后端真正处于远程状态,我们可以通过 HTTP 轻松地公开它。请看此处的示例。


如何向(从)远程调用的方法传递(返回)值呢?可以传递(返回)任何可序列化的内容,包括类实例。只要在前端和后端使用相同的名称注册一个类,就可以自动传输它的实例。


如何跨前端和后端重写一个方法呢?这与常规的 JavaScript 没有什么不同,我们可以使用 super。例如,我们可以重写 increment()方法以在前端的上下文中运行额外的代码:


// frontend.js
class Counter extends BaseCounter { async increment() { await super.increment(); // 后端的`increment()` 方法被调用 console.log(this.value); // 在前端添加额外的运行代码 }}
复制代码


现在,让我们使用React和前面所示的封装方法构建一个用户界面:


// frontend.js
import React from 'react';import {view} from '@liaison/react-integration';
class Counter extends BaseCounter { // 我们使用`@view()`装饰器来观察模型,并在需要时重新渲染视图 @view() View() { return ( <div> {this.value} <button onClick={() => this.increment()}>+</button> </div> ); }}
复制代码


最后,为了显示计数器,我们需要的是:


<counter.View />
复制代码


瞧!我们构建了一个具有两个统一层和一个封装 UI 的单页应用程序。

概念验证

为了试验统一架构,我使用 Liaison 构建了一个 RealWorld 示例应用程序


我可能有些自夸了,但结果看起来真的非常惊人:实现简单,代码高内聚,100% DRY(Don’t repeat yourself),没有胶水代码。


就代码量而言,我的实现比我使用过的其他任何实现都要轻得多。点击这里查看结果。


当然,RealWorld 示例是一个小型应用程序,但是由于它涵盖了所有应用程序都共有的最重要的概念,因此我相信统一架构可以扩展到更复杂的应用程序中。

结论

关注点分离、松散耦合、简单性、内聚性和敏捷性。


似乎这一切都实现了。


如果您是一位经验丰富的开发人员,那么我想您对此会有所怀疑,这也很正常。我们很难把多年的习惯抛诸脑后。


如果您不喜欢面向对象编程,那么就不要使用 Liaison 了,这也是完全没有问题的。


但是,如果您对 OOP 感兴趣,请在脑海中打开一扇小窗,下一次您必须构建一个全栈应用程序时,请试试看它是如何适用于统一架构的。


Liaison仍处于早期阶段,但我正在积极研究中,我希望在 2020 年初发布第一个测试版本。


如果您有兴趣的话,请为代码库加注 star 标,并通过关注博客或订阅newsletter的方式来保持更新。


原文链接:


https://www.freecodecamp.org/news/full-stack-unified-architecture/


2020-01-12 23:002880
用户头像
刘燕 InfoQ高级技术编辑

发布了 1112 篇内容, 共 535.1 次阅读, 收获喜欢 1977 次。

关注

评论

发布
暂无评论
发现更多内容

限流系列文章——漏斗限流

李子捌

redis 限流 签约计划第二季

Prometheus Exporter (十三)Elasticsearch Exporter

耳东@Erdong

elasticsearch Prometheus exporter 11月日更

2021年大数据开发发展趋势

五分钟学大数据

11月日更

在线文本交集计算工具

入门小站

工具

Skip List(跳跃列表)它到底好在哪?今天我们不仅只聊为什么,还手写一个玩玩

李子捌

redis skiplist 签约计划第二季

限流系列文章——令牌桶限流

李子捌

redis 限流 签约计划第二季

【高并发】如何使用Java7提供的Fork/Join框架实现高并发程序?

冰河

Java 并发编程 多线程 高并发 异步编程

云原生训练营作业--部署k8s集群

好吃不贵

转型中的学习型组织 ——阅读《第五项修炼》有感

研发管理Jojo

系统性思考 企业转型

音视频理论(1)- 音频格式之 Monkeys Audio(APE)

liuzhen007

签约计划第二季

签到功能怎么做?Bitmaps助你一臂之力

李子捌

redis bitmaps 签约计划第二季

李子捌 Redis精通系列文章 研究分享| 内容合集

李子捌

redis 内容合集 签约计划第二季 技术专题合集

SAP Cloud for Customer Price 计价简介

汪子熙

Cloud SAP C4C 11月日更 pricing

k8s statefulset controller源码分析

良凯尔

源码 Kubernetes 源码分析 #Kubernetes#

为什么我的 C4C Service Request 没办法 Release 到 ERP?

汪子熙

Cloud SAP abap C4C 11月日更

跟小师妹一起学JVM-系列文章

程序那些事

Java JVM JIT 内容合集 签约计划第二季

都在用MQ,Redis的Pub/Sub也可以试着了解下

李子捌

redis MQ 签约计划第二季

HyperLogLog这里面水很深,但是你必须趟一趟

李子捌

redis 签约计划第二季

CSS之盒模型

Augus

CSS 11月日更

URL URI傻傻分不清楚,dart告诉你该怎么用

程序那些事

flutter dart 程序那些事 11月日更

[Pulsar] 消息从Producer到Broker的历程

Zike Yang

Apache Pulsar 11月日更

Flutter 中的手势【Flutter 专题10】

坚果

flutter 签约计划第二季

听说你的服务经常被打崩?试试布隆过滤器(Bloom Filter)

李子捌

redis 布隆过滤器 签约计划第二季

Linux 调优之:调整 bond hash 策略提升网络吞吐能力

卫智雄

数据分析从零开始实战,Pandas读写Excel/XML数据

老表

Python 数据分析 Excel pandas 11月日更

JSON 数据格式

大数据技术指南

11月日更

新成就!OceanBase 入选 Forrester 首份分布式数据库报告

OceanBase 数据库

数据库 开源 新闻 oceanbase 荣誉

Redis之Geospatial,助你轻松实现附近的xx功能

李子捌

redis geospatial 签约计划第二季

数据库不能没有事务,今天他来了——Redis事务详述

李子捌

redis 事务 签约计划第二季

Redis高可用的绝对的利器——持久化(RDB和AOF)

李子捌

redis redis持久化 签约计划第二季

限流系列文章——滑动窗口限流

李子捌

redis 限流 签约计划第二季

如何使用统一架构简化全栈开发_AI&大模型_Manuel Vila_InfoQ精选文章