在之前发表的文章中,作者报告了使用最简单的 Greeting Service 示例应用程序在 Winglang 编程语言中实现六边形端口和适配器模式的可能性。这次评估的主要结论如下:

  1. 云资源(如 API Gateway)扮演着驱动(in-)和被驱动(out-)端口的角色
  2. 事件处理函数扮演着适配器的角色,它通向一个纯粹的核心,可以用 Winglang、TypeScript 或任何编程语言实现,编译成 JavaScript 并在 Node.js 运行时引擎上运行。

最初,作者计划继续探索在 Winglang 中实现更通用的分阶段事件驱动架构(SEDA)的可能方法。然而,使用最简单的问候服务(GreetingService)作为示例,仍有一些非常重要的架构问题没有得到解答。因此,作者决定更深入地探讨实现一个典型的创建/检索/更新/删除(CRUD)服务所涉及的问题,该服务暴露于标准化的 REST API,并解决了典型的生产环境问题,如安全认证、可观察性、错误处理和报告。

为了防止特定领域的复杂性影响对重要架构考虑因素的关注,作者选择了最简单的 TODO 服务,包含四种操作:

  1. 检索所有任务(每个用户)
  2. 创建新任务
  3. 完全替换现有任务定义
  4. 删除现有任务

通过这个简单的例子,作者评估了许多重要的架构选项,并为 Winglang 编程语言提出了一个中间件库的初始原型,该库与主流编程语言的流行库兼容,并有可能超越它们,如 AWS Lambda 的 Node.js 中间件引擎 Middy 和 AWS Lambda 的 Power Tools。

与以前的文章不同,本文不会描述作者是如何一步步实现当前安排的。软件架构和设计过程很少是线性的,尤其是在初级教程之外。取而代之的是,作者将描述一个起点解决方案,虽然远非最终方案,但其代表性足以让人感觉到最终框架可能的发展方向。作者将概述其想要解决的需求、当前的架构决策,并强调未来的研究方向。

Winglang 中的简单 TODO

在 Winglang 中开发一个简单的、原型级的 TODO REST API 服务确实非常容易,使用 Winglang Playground 只需半小时即可完成:

为了简单起见,把所有内容都放在一个源中,当然也可以分成核心、端口和适配器。让我们来看看这个示例的主要部分。

为了简单起见,把所有内容都放在一个源代码中,当然也可以分成 Core、Ports 和 Adapters 三部分。让我们来看看这个示例的主要部分。

资源(端口)定义

首先,我们需要定义要使用的云资源(又称端口)。具体步骤如下:

bring ex;
bring cloud;

let tasks = new ex.Table(
name: "Tasks",
columns: {
"id" => ex.ColumnType.STRING,
"title" => ex.ColumnType.STRING
},
primaryKey: "id"
);
let counter = new cloud.Counter();
let api = new cloud.Api();
let path = "/tasks";

在这里,我们定义了一个 Winglang 表来保存 TODO 任务,该表只有两列:任务 ID 和标题。为了保持简单,我们使用 Winglang 计数器资源将任务 ID 实现为一个自动递增的数字。最后,我们使用 Winglang API 资源公开 TODO 服务 API。

API 请求处理程序(适配器)

现在,我们要为四个 REST API 请求分别定义一个处理函数。获取所有任务列表的方法如下:

api.get(
path,
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let rows = tasks.list();
let var result = MutArray<Json>[];
for row in rows {
result.push(row);
}
return cloud.ApiResponse{
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(result)
};
});

创建新任务记录的方法如下:

api.post(
path,
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = "{counter.inc()}";
if let task = Json.tryParse(request.body) {
let record = Json{
id: id,
title: task.get("title").asStr()
};
tasks.insert(id, record);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(record)
};
} else {
return cloud.ApiResponse {
status: 400,
headers: {
"Content-Type" => "text/plain"
},
body: "Bad Request"
};
}
});

更新现有任务的方法如下:

api.put(
"{path}/:id",
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = request.vars.get("id");
if let task = Json.tryParse(request.body) {
let record = Json{
id: id,
title: task.get("title").asStr()
};
tasks.update(id, record);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "application/json"
},
body: Json.stringify(record)
};
} else {
return cloud.ApiResponse {
status: 400,
headers: {
"Content-Type" => "text/plain"
},
body: "Bad Request"
};
}
});

最后,删除现有任务的方法如下:

api.delete(
"{path}/:id",
inflight (request: cloud.ApiRequest): cloud.ApiResponse => {
let id = request.vars.get("id");
tasks.delete(id);
return cloud.ApiResponse {
status: 200,
headers: {
"Content-Type" => "text/plain"
},
body: ""
};
});

我们可以使用 Winglang 模拟器来试用这个 API:

我们可以编写一个或多个测试来自动验证应用程序接口:

bring http;
bring expect;
let url = "{api.url}{path}";
test "run simple crud scenario" {
let r1 = http.get(url);
expect.equal(r1.status, 200);
let r1_tasks = Json.parse(r1.body);
expect.nil(r1_tasks.tryGetAt(0));
let r2 = http.post(url, body: Json.stringify(Json{title: "First Task"}));
expect.equal(r2.status, 200);
let r2_task = Json.parse(r2.body);
expect.equal(r2_task.get("title").asStr(), "First Task");
let id = r2_task.get("id").asStr();
let r3 = http.put("{url}/{id}", body: Json.stringify(Json{title: "First Task Updated"}));
expect.equal(r3.status, 200);
let r3_task = Json.parse(r3.body);
expect.equal(r3_task.get("title").asStr(), "First Task Updated");
let r4 = http.delete("{url}/{id}");
expect.equal(r4.status, 200);
}

最后但并非最不重要的一点是,这项服务可以使用 Winglang CLI 部署到任何受支持的云平台上。TODO 服务的代码是完全云中立的,确保无需修改即可在不同平台上兼容。

这个例子清楚地表明,Winglang 编程环境是快速开发此类服务的一流工具。如果这就是您所需要的一切,那么您就无需继续阅读了。接下来的内容就像一个白兔洞,在我们开始认真讨论生产部署之前,需要解决多个非功能性问题。

请注意。即将发表的文章并非面向所有人,而是面向经验丰富的云软件架构师。

缩小生产差距

使用 REST API 快速构建 CRUD 服务与将其部署到生产环境之间存在巨大差距。生产环境需要考虑一系列非功能性问题。这些问题因业务环境而异。为小型团队内部部署服务与为政府或金融机构部署相同功能有很大不同。

专家们对非功能性需求清单的理想结构争论不休。作者更喜欢简明扼要的高层次概述,并根据需要进行深入分析。以下是列出的四大非功能性需求,同时也承认这份清单并非详尽无遗:

  1. 可用性:终端客户如何与服务沟通
  2. 安全性:如何保护服务,保护对象是谁
  3. 运行:我们将如何部署服务,以确保其稳健性和/或弹性,并控制其成本
  4. 规模:需要服务的并发客户数量以及著名的数据 V$³$:速度、数量和种类

这些领域不是孤立的,而是存在重叠。一个缓慢或失灵的系统是无法使用的。相反,在国防部层面确保杂货店库存系统的安全可能会让供应商满意,但却不会让银行家满意。虽然分类并不完善,但它为进一步讨论提供了一个框架。

可用性

上面介绍的 TODO 示例服务实现属于所谓的无头 REST API。这种方法侧重于核心功能,将用户体验设计留给独立的层。其实现方式通常是客户端渲染(Client-Side Rendering)或服务器端渲染(Server Side Rendering),中间有一个前端后台层(Backend for Frontend tier),或者使用多个作为 GraphQL 解析器(GraphQL Resolvers)运行的、范围较窄的 REST API 服务。每种方法在特定情况下都有其优点。

主张支持 HTTP 内容协商(HTTP Content Negotiation),并为通过浏览器直接进行 API 交互提供最小用户界面。虽然 Postman 或 Swagger 等工具可以促进 API 交互,但作为最终用户体验 API 可以提供宝贵的见解。这种基本的用户界面,或者称之为 “工程用户界面”,通常就足够了。

更简单的解决方案是使用基本的 HTML 模板,并利用 HTMX 的功能和 CSS 框架(如 Bootstrap)进行增强。目前,Winglang 本身并不支持 HTML 模板,但对于基本用例,可以使用 TypeScript 轻松管理。例如,渲染单个任务行的实现方式如下:

import { TaskData } from "core/task";

export function formatTask(path: string, task: TaskData): string {
return `
<li >
<form hx-put="${path}/${task.taskID}" hx-headers='{"Accept": "text/plain"}' >
<span >${task.title}</span>
<input
type="text"
name="title"

style="display: none;"
value="${task.title}">
</form>
<div >
<button
hx-delete="${path}/${task.taskID}"
hx-target="closest li"
hx-swap="outerHTML"
hx-headers='{"Accept": "text/plain"}'>✕</button>
<button >✎</button>
</div>
</li>
`
}

这样就会出现以下用户界面画面:

虽然不是超级华丽,但对于演示目的来说已经足够好了。

即使是纯粹的无头 REST 应用程序接口(API),也需要考虑很强的可用性。API 调用应遵循 HTTP 方法、URL 格式和有效载荷的 REST 约定。正确记录 HTTP 方法和潜在错误处理至关重要。需要记录客户端和服务器错误,将其转换为适当的 HTTP 状态代码,并在响应正文中提供清晰的解释信息。

由于需要使用 HTTP 请求中的 Content-Type 和 Accept 标头,根据内容协商处理多个请求解析器和响应格式器,因此采用了以下设计方法:

遵循依赖反转原则可确保系统核心与端口和适配器完全隔离。虽然可能有人倾向于将核心封装在由 ResourceData 类型定义的通用 CRUD 框架中,但建议大家谨慎行事。这一建议源于几个方面的考虑:

  1. 在实践中,即使是 CRUD 请求处理也往往会带来超出基本操作的复杂性。
  2. 核心不应该依赖于任何特定的框架,以保持其独立性和适应性。
  3. 要创建这样一个框架,就必须支持通用编程,而 Winglang 目前还不支持这一功能。

另一种选择是放弃核心数据类型定义,完全依赖无类型的 JSON 接口,类似于 Lisp 编程风格。不过,考虑到 Winglang 的强类型性,决定不采用这种方法。

总的来说,TodoServiceHandler 非常简单易懂:

bring "./data.w" as data;
bring "./parser.w" as parser;
bring "./formatter.w" as formatter;

pub class TodoHandler {
_path: str;
_parser: parser.TodoParser;
_tasks: data.ITaskDataRepository;
_formatter: formatter.ITodoFormatter;

new(
path: str,
tasks_: data.ITaskDataRepository,
parser: parser.TodoParser,
formatter: formatter.ITodoFormatter,
) {
this._path = path;
this._tasks = tasks_;
this._parser = parser;
this._formatter = formatter;
}

pub inflight getHomePage(user: Json, outFormat: str): str {
let userData = this._parser.parseUserData(user);

return this._formatter.formatHomePage(outFormat, this._path, userData);
}

pub inflight getAllTasks(user: Json, query: Map<str>, outFormat: str): str {
let userData = this._parser.parseUserData(user);
let tasks = this._tasks.getTasks(userData.userID);

return this._formatter.formatTasks(outFormat, this._path, tasks);
}

pub inflight createTask(
user: Json,
body: str,
inFormat: str,
outFormat: str
): str {
let taskData = this._parser.parsePartialTaskData(user, body);
this._tasks.addTask(taskData);

return this._formatter.formatTasks(outFormat, this._path, [taskData]);
}

pub inflight replaceTask(
user: Json,
id: str,
body: str,
inFormat: str,
outFormat: str
): str {
let taskData = this._parser.parseFullTaskData(user, id, body);
this._tasks.replaceTask(taskData);

return taskData.title;
}

pub inflight deleteTask(user: Json, id: str): str {
let userData = this._parser.parseUserData(user);
this._tasks.deleteTask(userData.userID, num.fromStr(id));
return "";
}
}

您可能会注意到,代码结构与前面的设计图略有不同。这些细微的调整在软件设计中很正常;在整个过程中会出现新的见解,因此有必要进行调整。最显著的区别是每个函数都定义了 user.Json 参数:Json 参数。我们将在下一节讨论该参数的用途。

安全

在没有采取安全措施的情况下,将 TODO 服务暴露在互联网上会带来灾难。黑客、无聊的青少年和专业攻击者会迅速锁定其公共 IP 地址。规则非常简单:

  • 任何公共接口都必须受到保护,除非在很短的测试时间内暴露出来。安全问题不容商量。

反之,在服务中加入各种可以想象得到的安全措施会导致过高的运营成本。正如在以前的文章中所论述的,让建筑师对其设计的成本负责可能会大大改变他们的设计方法:

  • 如果云解决方案架构师要对其系统产生的成本负责,就会从根本上改变他们的设计理念。

我们需要的是对服务应用程序接口的合理保护,不能少也不能多。既然想尝试全栈服务端渲染用户界面,自然会选择在一开始就强制用户登录,生成一个有合理有效期(比如一小时)的 JWT 令牌,然后用它对所有即将到来的 HTTP 请求进行身份验证。

考虑到服务端渲染的具体情况,使用 HTTP Cookie 来传递会话令牌是一个自然的选择(老实说是 ChatGPT 建议的)。对于客户端渲染选项,可能需要使用通过 HTTP 请求头授权字段传递的承载令牌。

有了包含用户信息的会话令牌,就可以按用户汇总 TODO 任务。虽然有许多方法可以将会话数据(包括用户详细信息)整合到域中,但选择在本研究中重点关注 userID 和 fullName 属性。

在用户身份验证方面,有多种选择,尤其是在 AWS 生态系统内:

  1. AWS Cognito,利用其用户池或与外部身份供应商(如 Google 或 Facebook)的集成。
  2. 第三方身份验证服务,如 Auth0。
  3. 完全在 Winglang 中开发的自定义身份验证服务。
  4. AWS 身份中心
  5. ……

作为一名独立的软件技术研究人员,作者倾向于使用组件最少的最简单解决方案,这样也能满足日常操作需求。由于现有的多账户/多用户设置,利用 AWS 身份中心(详见另一篇文章)是顺理成章的一步。

集成后,AWS Identity Center 主屏幕如下所示:

这意味着,在作者的系统中,用户、自己或客人可以使用相同的 AWS 凭据进行开发、管理以及示例或内务管理应用程序。

为了与 AWS Identity Center 集成,需要注册应用程序,并提供一个实现所谓 “断言消费者服务 URL(ACS URL)”的新端点。本文与 SAML 标准无关。只需指出,在 ChatGPT 和 Google 搜索的帮助下,就可以完成这项工作。在这里可以找到一些有用的信息。非常方便的是 TypeScript samlify 库,它封装了 SAML 登录响应验证过程的全部繁重工作。

作者最感兴趣的是,这个可变点如何影响整个系统的设计。让我们尝试用半正式的数据流符号将其可视化:

这种表示法看似不寻常,但却高保真地反映了数据是如何在系统中流动的。我们在这里看到的是著名的管道过滤器架构模式的一个特殊实例。

在这里,数据流经一个管道,每个过滤器执行一个定义明确的任务,实际上遵循了单一责任原则。这样的安排允许更换过滤器,如果想改用简单的 Basic HTTP 身份验证、使用 HTTP 授权头或使用不同的秘密管理策略来构建和验证 JWT 令牌的话。

如果我们放大 Parse 和 Format 过滤器,就会看到分别使用 Content-Type 和 Accept HTTP 标头的典型调度逻辑:

许多工程师将设计和架构模式与具体实现混为一谈。这就忽略了模式的本质。

模式就是要找出一种合适的方法,以最少的干预来平衡相互冲突的力量。在构建基于云的软件系统时,安全性至关重要,但成本或复杂性不应过高,因此这种理解至关重要。管道过滤器设计模式有助于有效解决此类设计难题。它允许对处理步骤进行模块化和灵活配置,在本例中,这些步骤与身份验证机制有关。

例如,虽然 SAML 身份验证等强大的安全措施对于生产环境是必要的,但在自动端到端测试等场景中,它们可能会带来不必要的复杂性和开销。在这种情况下,基本 HTTP 身份验证等更简单的方法可能就足够了,既能提供快速、经济的解决方案,又不会损害系统的整体完整性。我们的目标是保持系统的核心功能和代码库的统一性,同时根据环境或特定要求改变身份验证策略。

Winglang 独特的预检(Preflight)编译功能允许在构建阶段调整配置,从而消除了运行时的开销。与其他中间件库(如 Middy 和 AWS Power Tools for Lambda)相比,基于 Winglang 的解决方案的这一功能提供了管理身份验证管道的更高效、更灵活的方法,因而具有显著优势。

因此,实施基本 HTTP 身份验证只需修改身份验证管道中的一个过滤器,系统的其余部分则保持不变:

由于一些技术上的限制,目前还无法在 Winglang 中直接实现 Pipe-and-Filters 功能,但可以通过结合 Decorator 和 Factory 设计模式来轻松模拟。具体如何实现,我们很快就会看到。现在,让我们进入下一个主题。

运行

在本刊物中,作者不会涉及生产运营的所有方面。这个主题很大,值得单独出版。以下是作者认为最基本的内容:

要运行一项服务,我们需要知道它发生了什么,尤其是在出错时。这可以通过结构化日志机制来实现。目前,Winglang 只提供了一个基本的 log(str) 函数。在调查中,需要更多的功能,并实现了一个可怜的结构化日志类:

// A poor man implementation of configurable Logger
// Similar to that of Python and TypeScript
bring cloud;
bring "./dateTime.w" as dateTime;

pub enum logging {
TRACE,
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
}

//This is just enough configuration
//A serious review including compliance
//with OpenTelemetry and privacy regulations
//Is required. The main insight:
//Serverless Cloud logging is substantially
//different
pub interface ILoggingStrategy {
inflight timestamp(): str;
inflight print(message: Json): void;
}

pub class DefaultLoggerStrategy impl ILoggingStrategy {
pub inflight timestamp(): str {
return dateTime.DateTime.toUtcString(std.Datetime.utcNow());
}
pub inflight print(message: Json): void {
log("{message}");
}
}

//TBD: probably should go into a separate module
bring expect;
bring ex;

pub class MockLoggerStrategy impl ILoggingStrategy {
_name: str;
_counter: cloud.Counter;
_messages: ex.Table;

new(name: str?) {
this._name = name ?? "MockLogger";
this._counter = new cloud.Counter();
this._messages = new ex.Table(
name: "{this._name}Messages",
columns: Map<ex.ColumnType>{
"id" => ex.ColumnType.STRING,
"message" => ex.ColumnType.STRING
},
primaryKey: "id"
);
}
pub inflight timestamp(): str {
return "{this._counter.inc(1, this._name)}";
}
pub inflight expect(messages: Array<Json>): void {
for message in messages {
this._messages.insert(
message.get("timestamp").asStr(),
Json{ message: "{message}"}
);
}
}
pub inflight print(message: Json): void {
let expected = this._messages.get(
message.get("timestamp").asStr()
).get("message").asStr();
expect.equal("{message}", expected);
}
}

pub class Logger {
_labels: Array<str>;
_levels: Array<logging>;
_level: num;
_service: str;
_strategy: ILoggingStrategy;

new (level: logging, service: str, strategy: ILoggingStrategy?) {
this._labels = [
"TRACE",
"DEBUG",
"INFO",
"WARNING",
"ERROR",
"FATAL"
];
this._levels = Array<logging>[
logging.TRACE,
logging.DEBUG,
logging.INFO,
logging.WARNING,
logging.ERROR,
logging.FATAL
];
this._level = this._levels.indexOf(level);
this._service = service;
this._strategy = strategy ?? new DefaultLoggerStrategy();
}
pub inflight log(level_: logging, func: str, message: Json): void {
let level = this._levels.indexOf(level_);
let label = this._labels.at(level);
if this._level <= level {
this._strategy.print(Json {
timestamp: this._strategy.timestamp(),
level: label,
service: this._service,
function: func,
message: message
});
}
}
pub inflight trace(func: str, message: Json): void {
this.log(logging.TRACE, func,message);
}
pub inflight debug(func: str, message: Json): void {
this.log(logging.DEBUG, func, message);
}
pub inflight info(func: str, message: Json): void {
this.log(logging.INFO, func, message);
}
pub inflight warning(func: str, message: Json): void {
this.log(logging.WARNING, func, message);
}
pub inflight error(func: str, message: Json): void {
this.log(logging.ERROR, func, message);
}
pub inflight fatal(func: str, message: Json): void {
this.log(logging.FATAL, func, message);
}
}

正如作者在评论中写道的那样,基于云的日志系统需要认真修改。不过,对于目前的调查来说,这已经足够了。完全相信,日志记录是任何服务规范不可分割的一部分,必须像测试核心功能一样严格。为此,开发了一种简单的机制来模拟日志,并根据预期进行检查。

对于 REST API CRUD 服务,我们至少需要记录三类日志:

  1. HTTP 请求
  2. 发生错误时的原始错误信息
  3. HTTP 响应

此外,根据需要,可能需要将原始错误信息转换为标准信息,例如,为了不给攻击者提供可乘之机。

记录多少细节取决于多种因素:部署目标、请求类型、特定用户、错误类型、统计采样等。在开发和测试模式下,我们通常会选择记录几乎所有内容,并将原始错误信息直接返回到客户端屏幕,以方便调试。在生产模式下,我们可能会选择删除一些敏感数据(因为有法规要求),返回 “Bad Request”(错误请求)等一般错误信息,而不提供任何详细信息,并只对特定类型的请求进行统计抽样记录,以节省成本。

通过在每个请求处理管道中注入四个额外的过滤器,实现了灵活的日志配置:

  1. HTTP 请求记录过滤器
  2. Try/Catch 装饰器,用于将任何异常转换为 HTTP 状态代码,并记录原始错误信息(这可以提取为一个单独的过滤器,但作者决定保持简单)
  3. 错误信息翻译器,根据需要将原始错误信息转换为标准错误信息
  4. HTTP 响应记录过滤器

这种结构虽然不是终极结构,但它提供了足够的灵活性,可根据服务及其部署目标的具体情况,实施多种日志记录和错误处理策略。

与日志一样,Winglang 目前只提供了一个基本的 throw <str> 操作符,因此决定实现穷人版结构化异常:

// A poor man structured exceptions
pub inflight class Exception {
pub tag: str;
pub message: str?;

new(tag: str, message: str?) {
this.tag = tag;
this.message = message;
}
pub raise() {
let err = Json.stringify(this);
throw err;
}
pub static fromJson(err: str): Exception {
let je = Json.parse(err);

return new Exception(
je.get("tag").asStr(),
je.tryGet("message")?.tryAsStr()
);
}
pub toJson(): Json { //for logging
return Json{tag: this.tag, message: this.message};
}
}

// Standard exceptions, similar to those of Python
pub inflight class KeyError extends Exception {
new(message: str?) {
super("KeyError", message);
}
}
pub inflight class ValueError extends Exception {
new(message: str?) {
super("ValueError", message);
}
}
pub inflight class InternalError extends Exception {
new(message: str?) {
super("InternalError", message);
}
}
pub inflight class NotImplementedError extends Exception {
new(message: str?) {
super("NotImplementedError", message);
}
}
//Two more HTTP-specific, yet useful
pub inflight class AuthenticationError extends Exception {
//aka HTTP 401 Unauthorized
new(message: str?) {
super("AuthenticationError", message);
}
}
pub inflight class AuthorizationError extends Exception {
//aka HTTP 403 Forbidden
new(message: str?) {
super("AuthorizationError", message);
}
}

这些经验凸显了开发人员社区如何通过临时变通办法来弥补新语言的不足。尽管 Winglang 仍在不断发展,但它的创新功能可以被利用来取得进步。

现在,是时候简单了解一下清单上的最后一个生产主题了:

规模

扩展是云计算开发的一个重要方面,但却经常被误解。有些人完全忽视了这一点,导致系统增长时出现问题。另一些人则过度工程化,希望从第一天起就成为一个 “FANG “系统。我们在 Kubernetes 上运行一切 “是技术圈子里的一句流行语,不管它是否适合手头的项目。

忽视和过度设计这两种极端情况都不理想。与安全性一样,不应该忽视扩展性,但也不应该过分强调它。

在一定程度上,云平台提供了具有成本效益的扩展机制。不同选项之间的选择往往归结为个人偏好或惰性,而非显著的技术优势。

谨慎的做法是从小规模、低成本开始,根据实际使用情况和性能数据而不是假设进行扩展。这种方法要求系统在设计上易于更改配置以适应扩展,Winglang本身并不支持这种方法,但通过进一步的开发和研究,这种方法肯定是可行的。举例来说,让我们考虑一下 AWS 生态系统内的扩展:

  1. 起初,一个经济高效的快速部署可能涉及使用 S3 Bucket 进行存储的单个 Lambda 函数 URL,用于带有服务器端渲染功能的全栈 CRUD API。这种设置可以实现对早期开发阶段至关重要的快速反馈。就个人而言,更倾向于 “用户体验优先 “的方法,而不是 “应用程序接口优先 “的方法。你可能会惊讶于使用这种基本的技术堆栈能走多远。虽然 Winglang 目前还不支持 Lambda 函数 URL,但相信通过过滤器组合和系统调整可以实现。在这个层面上,遵循 Marc Van Neerven 的建议,使用标准的 Web 组件而不是厚重的框架,可能会有所裨益。这是一个有待今后探索的课题。
  2. 当需要外部 API 暴露或 Web Sockets 等高级功能时,就需要过渡到 API 网关或 GraphQL 网关。如果初始数据存储解决方案成为瓶颈,可能就需要考虑切换到 Dynamo DB 这样更强大、可扩展的解决方案了。此时,为每个 API 请求部署单独的 Lambda 函数可能会简化实施过程,但这并不总是最具成本效益的策略。
  3. 向容器化解决方案的转变应该以数据为导向,只有当有明确证据表明,基于功能的架构要么成本过高,要么因冷启动而存在延迟问题时,才会考虑采用容器化解决方案。对容器的初步尝试可能包括使用 ECS Fargate,因为它简单且具有成本效益,而将 EKS 保留给需要其功能的特定运营需求场景。理想情况下,应通过配置调整和自动过滤器生成来管理这种演进,并利用 Winglang 的独特功能来支持动态扩展策略。

从本质上讲,Winglang 的方法强调 “飞行前”(Preflight)和 “飞行中”(Inflight)阶段,有望促进这些扩展策略,尽管它可能仍处于充分实现这一潜力的早期阶段。对云软件开发中可扩展性的探索强调从小处入手,根据实际数据做出决策,并保持灵活性以适应不断变化的需求。

结束语

20 世纪 90 年代中期,作者从 Jim Coplien 那里学到了共性可变性分析法。从那时起,这种方法与 Edsger W. Dijkstra 的分层架构一起,成为作者软件工程实践的基石。共性变异性分析会问”在我们的系统中,哪些部分永远不变,哪些部分可能需要改变?”开放-封闭原则 “规定,可变部分应可替换,而无需修改核心系统。

决定何时最终确定系统的稳定部分需要在灵活性和效率之间权衡利弊,从代码生成到运行时的几个阶段都提供了固定的机会。动态语言的支持者可能会将这些决定推迟到运行时,以获得最大的灵活性,而静态编译语言的支持者通常会尽早确保关键系统组件的安全。

Winglang 凭借其独特的预检编译阶段脱颖而出,允许在开发过程的早期修复云资源。在本文中,作者探讨了 Winglang 如何通过灵活的过滤器管道解决云服务的非功能性问题,尽管这种粒度带来了自身的复杂性。现在的挑战是在不影响系统效率或灵活性的前提下管理这种复杂性。

虽然最终的解决方案还在制定过程中,但作者可以勾勒出一个能平衡这些力量的高层次设计方案:

这种设计结合了多种软件设计模式,以达到理想的平衡。这一过程包括:

  1. Pipeline Builder组件负责准备最终的预检组件集。
  2. Pipeline Builder会读取一个配置,该配置可能会被组织成一个复合配置(团队或组织范围内的配置)。
  3. 配置指定了对资源(如记录仪)的能力要求。
  4. 每个资源都有多个规范,每个规范都定义了需要调用工厂生成所需过滤器的条件。我们设想了三种过滤器类型:
    – 行 HTTP 请求/响应过滤器
    – 扩展 HTTP 请求/响应过滤器,在令牌验证后提取会话信息
    – 转发给核心的通用 CRUD 请求过滤器

这种方法将复杂性转向实现 Pipeline Builder机制和配置规范。经验告诉我们可以实施这种机制(例如本文中描述的)。这通常需要一些通用编程和动态导入功能。提出一个好的配置数据模型更具挑战性。

基于生成式人工智能的协同驾驶仪的最新进展提出了如何实现最具成本效益的结果的新问题。为了理解这个问题,让我们重温一下传统的编译和配置堆栈:

这种一般情况可能不适用于每个生态系统。以下是典型层的细分:

  1. 核心语言设计为小型语言(”C “和 Lisp 传统)。它可能支持反射,也可能不支持反射。
  2. 尽可能多的扩展功能由标准库、第三方库和框架提供。
  3. 通用元编程:对 C++ 模板或 Lisp 宏等特性的支持会在早期(C++、Rust)或晚期(Java、C#)引入。泛型一直是争论的焦点:
    – 框架开发人员认为它们的表现力不够。
    – 应用程序开发人员则为其复杂性而苦恼。
    – Scala 就是泛型过于复杂的潜在弊端的一个例证。
  4. 尽管饱受批评,宏(如 C 预处理器)仍是自动生成代码的工具,通常可以弥补泛型的局限性。
  5. 第三方供应商(通常是开源的)提供的解决方案通常使用外部配置文件(YAML、JSON 等)来增强或弥补标准库。
  6. 专业生成器通常使用外部蓝图或模板。

这种复杂的结构有其局限性。泛型会掩盖核心语言,宏是不安全的,配置文件是伪装得很差的脚本,代码生成器依赖于不灵活的静态模板。这些局限性正是作者认为当前内部开发平台趋势发展潜力有限的原因。

当我们期待生成式人工智能在简化这些过程中发挥作用时,问题就来了:在软件工程中,基于生成式人工智能的协同驾驶不仅能简化流程,还能增强我们平衡共性与变异性的能力吗?

原文链接:Implementing Production-grade CRUD REST API in Winglang

作者:Asher Sterkin