并查集复杂度
盯着微服务系统互连通道的迷宫,我立即意识到了问题所在。
我正和一位新客户坐在一起,对他们的系统进行审查。 这是他们第一次向我展示被描述为“非常有趣”的代码,“绝对是我研究过的最复杂的代码之一!” 兴奋地。
我关了一下。
我想到了带有艾伯特·爱因斯坦(Albert Einstein)图片的讽刺性错误报价的T恤。
任何聪明的傻瓜都可以使事情变得更大,更复杂……这需要一些天才,并且需要勇气朝相反的方向发展。 — EF舒马赫。
复杂性从来不是一个好兆头。
首先,我想了解业务,尤其是代码中的代表位置。
这是大多数人问的地方:“名词是什么?”
我的第一个问题:“发生了什么事?”
我们世界上每天发生的事件数量惊人。 该名男子越过马路,汽车炸毁了那盏转瞬就变成红色的灯。
数量如此之多,以致于不可能发生的可能性很大,因为数十亿个因素在不断变化并影响其他参与者和实体。 人们大喊“巧合!” 我认为,由于规模巨大,统计上可能有巧合。
我们为了回应事件而消耗了世界。 我们读到有关响应另一个演员的行为而发生的事情的新闻。 我们逃避威胁。 我们对自己的饥饿感感到满足。 等等。
除了消费发生在我们周围的事件之外,我们还影响并影响世界以及其他方面。 您拥有自己的专长-发生的事情,对于其他事情,您要求他人获得想要的结果。
通过我们自己的行动或命令的效果,我们在某种意义上正在改变世界的状态。
如果您知道世界上发生的一切,则可以重建世界的当前状态。
在构建自动解决世界问题的软件时,我们需要对世界的有用子集进行建模。 我们创建了一组抽象,可用于解决手头的任务。 您总是会听到名词的重要性,但是我想强调的是,名词,命令和事件在我们的上下文中发生都是非常重要且明确的部分。
在为世界的一小部分建模时,我们应始终努力使概念明确定义并易于理解。
专家提示:这也是重构的有用技巧。 如果您发现某个概念在被讨论和暗示,但未在代码中明确定义,则表明存在问题。 有“代码气味”。 使隐式,显式。
名词由事件及其在时间上的位置联系在一起,这是我们现实中唯一不变的结构。
世界是一个很大的地方。 我们无法为整个世界建模。
与计算机科学中的任何复杂问题领域一样,为了使其更易于解决,我们可以引入约束 。 用建模术语,我们必须将建模对象的上下文或焦点缩小到仅涉及的实体。 实际上,甚至更窄了,将那些实体的属性限制为仅对解决特定问题有用的实体。
有时,通过对话我们会很容易地并且很明显地知道域的一些重要实体是什么,并且我们可以开始将它们分解为较小的子系统,有时称为“ 有界上下文 ”。
例如,如果您正在构建一个电子商务平台,则可能需要一个产品模型来描述产品的属性,因为该产品与在各种介质(例如互联网或印刷目录)上显示有关,即库存模型用于跟踪可用性,用于跟踪和管理客户订单的Order Ledger,电子商务商店,或者可能是一些营销渠道。
每个都是自己的上下文。
在这些上下文中,由于被称为“ 耦合 ”和“ 内聚 ”的概念,从建模角度来看还有更多的微妙之处。
凝聚力是概念之间紧密联系的度量。 高内聚性意味着实体紧密耦合。 这意味着它们可能需要始终参照完整-在单个事务中或需要即时一致性时进行更新。
当事物非常无关时,它们就会松散耦合。
回到电子商务商店的示例–在库存系统中,有必要知道哪些商品可用。 但是,每种产品的详细信息对于满足清单上下文/系统的要求不是必需的。
哪些事件与库存相关联? 以下是一些: inventory.product.listed
, inventory.product.quantity.decreased
, inventory.product.outOfStock
。
您会注意到的第一件事是语言的清晰度。 在讨论业务时使用的是同一语言。
专家提示:小心不要意外地将这种语言强加给他人-作为工程师,我们经常不得不命名。 错误的名称可能会产生深远的影响。 关于此的更多信息。
通常,每个事件都会伴随有有效载荷,有关事件的元信息,例如上述示例中的产品ID。 它回答了一些问题,例如哪种产品,还提出了一些问题:产品如何上市,库存如何减少?
我们可以将域中发生的事件的这些不变事实追溯到执行命令的参与者,再追溯到发出命令的位置和位置。
我们还可以朝相反的方向进行思考和搜索-当产品缺货时,这会影响什么系统? 产品说明? 不。 这意味着在这种情况下内聚力很低,需要松散耦合。
我们能够从所了解的知识中向外定义它,以了解事件如何影响实体以及这些事件如何发生。
一个事件在很小的空间中传达了很多有关域的信息。 它有助于定义域的范围,上下文的大小以及内聚和耦合的级别。
遵循事件和命令的踪迹将引导我们跨越边界来定义思维模型,并查看遵循的模式和违反的规则。 我们可以识别耦合不正确或内聚力不适当的片段,并开始评估其复杂性。
返回代码审查…
我希望在这次审查中看到的是显式定义的命令和事件。
就个人而言,我喜欢每个事件处理程序或命令处理程序的文件。 它无需查看代码即可强烈地传达每个服务的功能。
当您可以浏览代码库并通过搜索发生了什么或应该以什么命名的文件来查找功能的入口点时,由于概念的组织,定义和传达清晰,因此可以非常有效地进行导航。
不幸的是,这不是我发现的。 公平地说,通常不是。
可以说,这更像是CRUD之类的服务在彼此之间大喊大叫,就像90年代纽约证券交易所的电影描写一样。
复杂性一发不可收拾。
我问:“您听说过一种称为CQRS和事件源的模式吗?”
客户回答“是”。 “我们很早就谈到了这一点,但决定保持简单,而仅使用REST。”
……“那算如何?”
提示:不好。
为了避免复杂性,比通过模式和纪律解决问题要复杂得多。
我看到这确实是一个陷阱,真正的优秀工程师总是会陷入其中,我认为这部分是语言的失败,部分是复杂性的谬误。
首先-语言的失败-该项目被描述为MVP。
经过一个大型团队的密集开发,这是8个月。
绝不是MVP。
MVP是您可以用来证明假设的最小的事情。 它向开发人员传达“Swift而肮脏”的信息。
几年前,当我创办第一家公司时,我确实犯了这个错误。 我花了几个月的时间来制作具有自定义CSS动画和实时更新的史诗般的MVP,可以将其推送到我们在酒店房间中连接的平板电脑上的任意数量,以为城市中的客人带来凉爽的体验。 事实证明,尽管人们可能想要它,但这并不重要,因为酒店没有。
现在,在精益启动机器,精益启动会议以及由精益分析作者Ben Yoskovitz领导的令人敬畏的加速器与Anheuser Busch InBev的合作之后,我是一家机器学习启动公司的首席技术官,此举引起了广泛关注。合并到ABI的电子商务部门后,我可以自信地告诉您这不是事实。
例如,在一个精益启动机器实验中,我构建了一个单页应用程序,该应用程序的表单收集了电子邮件地址,以查看人们是否有兴趣共享出租车(在超级游泳池之前),因为上东区被该死了! 我花了大约30分钟时间完成制作。 那是最有价值球员。 一个复杂的甚至!
说服一个陌生人和我一起坐出租车就足够了。 它采用了一个特定的假设并加以证明或反证。
评委们告诉我们,“ CabPool”并不是一个好主意,因为我们只赚了7美元,而且大多数人对与陌生人共享游乐设施感到陌生。 回想起来,即使那是真的,但事实并非如此,由于微生态系统的存在,一小部分未爬出来的人可能是一个足够大的市场! 这完全取决于您的目标。
无论如何,即使一封电子邮件也可能是MVP。 它只需要证明一个假设。
这使我明白: 语言非常重要。
没有适当的语言,您就没有适当的模型。
这导致了我在代码中发现的第二个主要问题-语言并不是那么重要。
听。
型号需要到被清楚地定义为。
这意味着不要混入REST API。
但是,当您建立MVP时会怎么做? 最好将其扔到REST API中!
API的目的是成为接口,而不是模型。 另外,每个上下文最多应有一个API服务。
如果您的每一项服务都是REST服务,您最好打开计算机并在里面倒一些意大利面,因为在8个月内,看来您将很难拥有一台损坏的PC。
前端世界中的人们知道前端很复杂。 想一想您知道并问“前端复杂设备”的任何前端开发人员,他们都会对通量,单向数据流和功能组件感到厌恶。
当涉及到后端时,流程和单向工作流就会消失。
专家提示:事件来源基本上是Redux。
前端人员 :您知道这些东西。 您知道通量之前的生活。 您想要后端吗? 你呢?
和后端人员:即使您的前端开发人员也知道这些知识! 拜托!
现在,如果我所做的一切都很夸张,而没有给出一个简单的例子的话,它将是什么样的文章!
让我们开始为库存建模,因为它的核心是一个非常简单的系统,但是足够复杂,以至于它可以成为真实项目的一部分。
首先-POJO-代表“普通的旧Java(script)对象”:
export default class Inventory {
constructor () {
this .products = []
}
init(id) {
this .id = id
}
catalogProduct(product) {
this .products.push(product)
}
}
到目前为止和我在一起,对吗?
现在,让我们添加一些魔术酱! ♂️
import { SourcedEntity } from 'sourced'
export default class Inventory extends SourcedEntity {
constructor (snapshot, events) {
super ()
this .products = []
this .rehydrate(snapshot, events)
}
init(id) {
this .id = id
this .digest( 'init' , id)
}
catalogProduct(product) {
this .products.push(product)
this .digest( 'catalogProduct' , product) // same as command name
}
}
而且您是活动采购!
让我们分解一下。
首先,我们从SourcedEntity扩展我们的类,因此需要调用super()
。 能够通过以前的事件为我们的实例补充水分非常重要。 这意味着通过重播事件来重新创建最新状态。 快照是一种优化,因为它用作起点,因此您不必总是完全回到零。
最后,执行命令后,我们将调用一个称为digest
的函数。 命令进入模型并被摘要。
这是我见过的人们在精神上绊倒的地方。 几乎看起来好像我们是“命令源”,但我希望您这样想:命令已被消化。 每个命令都被消化。
这实际上意味着实体的名为newEvents
的属性具有一个新对象,该对象包含刚摘要的命令的名称以及与之关联的数据。
在我们继续坚持之前,还有另外一个重要的方面。 消化命令后,我们可以选择发出有关发生的事件的信息,以便我们服务的其他部分可以决定对这些信息进行处理。
import { SourcedEntity } from 'sourced'
export default class Inventory extends SourcedEntity {
constructor (snapshot, events) {
super ()
this .products = []
this .rehydrate(snapshot, events)
}
init(id) {
this .id = id
this .digest( 'init' , id)
this .emit( 'inventory.initialized' )
}
catalogProduct(product) {
this .products.push(product)
this .digest( 'catalogProduct' , product) // same as command name
this .emit( 'inventory.product.cataloged' )
}
}
但是,这有点棘手,所以让我们放慢速度一秒钟。
如果在事件完全提交并保存到数据库之前发出事件,会发生什么情况?
我们的系统就像Rube Goldberg机器一样开始:
Dog Rube Goldberg机器( Giphy )
如您所料,我们不希望在准备之前发生这种情况。
这是一个简单的修复,我们将使用特殊版本的称为“ enqueue ”的emit。
让我们看一下更新的功能。
init(id) {this .id = id
this .digest( 'init' , id)
this .enqueue( 'inventory.initialized' )
}
catalogProduct(product) {
this .products.push(product)
this .digest( 'catalogProduct' , product) // same as command name
this .enqueue( 'inventory.product.cataloged' )
}
现在,在事件成功提交到我们的存储库之前,将不会发出该事件!
“什么存储库?” 您可能想知道。
很高兴你问。
如果您已经注意到,我们的模型既好又简单,一切……但是……它不需要持久性吗?
是。 是的,它确实。
这就是我们将使用“ 存储库模式 ”的目的。 通过将持久性问题移出模型,存储库模式将使我们能够保持模型的美观和简单。
Sourced是在考虑这一点的基础上构建的,并为此提供了MongoDB实现。
但是,在创建存储库之前,让我们使用Jest测试模型,因为测试很重要!
__tests __ / models / Inventory.mjs
import Inventory from 'models/Inventory.mjs'
describe( 'Inventory' , () => {
it( 'has a quick test for this article' , () => {
let inventory = new Inventory()
let id = 'test-store-1'
inventory.init(id)
expect(inventory.id).toEqual(id)
let product = { id : 1 , quantity : 100 }
inventory.catalogProduct(product)
expect(inventory.products[ 0 ]).toEqual(product)
})
})
对于我们的模型,这应该接近100%的覆盖率。 再次告诉我要获得100%的覆盖率是多么困难,我将告诉您编写更简单的代码!
好了,让我们创建该存储库! 让我们创建一个新文件:
仓库/InventoryRepository.mjs
import SourcedRepoMongo from 'sourced-repo-mongo'
import Inventory from '../models/Inventory.mjs'
const { Repository } = SourcedRepoMongo
export const inventoryRepository = new Repository(Inventory)
在继续之前,可以使用此模式解决一些其他复杂问题。
例如,假设我们的服务批量接收事件,并且我们希望尽快处理它们,但是某个实例的所有事件都需要按顺序处理,否则结果将不正确。
另外,我想使用async / await
代替存储库回调样式实现,因此让我们在使用Bluebird的promisifyAll
时就可以做到这一点。
import SourcedRepoMongo from 'sourced-repo-mongo'
import Queued from 'sourced-queued-repo'
import Bluebird from 'bluebird'
import Inventory from '../models/Inventory.mjs'
const { Repository } = SourcedRepoMongo
const repository = new Repository(Inventory)
const queuedRepository = Queued(repository)
export const inventoryRepository = Bluebird.promisifyAll(queuedRepository)
现在,事件仍将尽快处理,但是,存储库将在从数据库中检索具有给定id
的资源时将其锁定,并在更改完成并提交后将其删除,从而确保事件得到处理为了。
让我们看一下使用清单的方法-有很多方法可以使事件进入流程,并且本文已经很长了,因此让我们仅关注收到命令后的处理方式,而不是如何接收命令。 我们将实现listen
功能,该功能将在收到命令时调用。
命令/inventory.initialize.mjs
import Inventory from '../models/Inventory'
import { inventoryRepository } from '../repos/inventoryRepository.mjs'
// This would be a good place for validation
export const listen = async function ( data ) {
const { id } = data
let inventory = new Inventory()
inventory.initialize(id)
try {
await inventoryRepository.commitAsync(inventory)
} catch (error) {
throw new Error ( `Error while committing repository - ${error.message} ` )
}
}
提交成功后,所有排队的事件将立即触发。
这次,让我们检索和修改现有库存。
命令/inventory.catalogProduct.mjs
import Inventory from '../models/Inventory'
import { inventoryRepository } from '../repos/inventoryRepository.mjs'
export const listen = async function ( { inventoryId, product } ) {
let inventory
try {
inventory = await inventoryRepository.getAsync(id)
} catch (error) {
throw new Error ( 'Error while getting data from repository' )
}
if (!inventory) {
throw new Error ( 'Inventory does not exist - cannot add product. Initialize inventory first, or ensure you are using the correct id' ))
}
inventory.catalogProduct(product)
try {
await inventoryRepository.commitAsync(inventory)
} catch (error) {
throw new Error ( `Error while committing repository - ${error.message} ` )
}
}
并做了!
结论
“复杂模式”有时享有比应有的声望更高的声誉。 在这种情况下,避免使用它们会导致更复杂的解决方案,因为“ MVP”已超出其原始范围。
我希望您已经学会了如何将普通的旧JavaScript对象与两种设计模式,事件源和存储库模式一起使用,以大大简化代码库中最重要的部分—业务逻辑,以及为什么使用那些有时被称为“复杂”的毕竟可能并不复杂。
上面的代码为微服务奠定了坚实的基础,您只需要弄清楚在服务之间发布消息的方法就可以通过各种方式完成,并可能添加一些验证。 我个人建议使用servicebus作为通讯位。
有关更完整的源示例,请从sourced-repo-mongo
查看此测试套件 。
有关使用带源代码的servicebus的示例,请在此处查看我的示例存储库: https : //github.com/patrickleet/servicebus-microservice
如果您想了解更多信息,我将开设一门名为“微服务驱动”的微服务课程。 注册以在此处可用时得到通知!
当人们想学习微服务时,我总是建议学习一些DevOps和容器化,以及简化开发和生产过程的先决条件。 在这里查看我的DevOps旅程 ! 我个人认为,未来将包括在大多数企业方案中在Kubernetes中运行的容器和无服务器-以及无服务器工作负载。
与往常一样,如果您发现这有帮助,最好的帮助方法是在Twitter上关注我,并与他人分享! ✌️
翻译自: https://hackernoon.com/complicated-patterns-arent-always-that-complicated-usually-it-s-the-simple-ones-that-bite-you-caf870f2bf03
并查集复杂度