称呼:
cargo run
从存储库根开始,您就可以开始了。
还要尝试探索API文档以了解一切的生活:
cargo doc --open
该存储库包含在线商店的样本Rust应用程序。目标是探索一些利用锈语构建可扩展和可维护应用程序的设计模式。
这是一个有不同想法的操场,其中一些可能不会在实践中淘汰。如果您在这里有任何反馈,请随时开放一个问题!
很难在真空中设计软件。当您没有真正的领域来驱动重要的事情时,设计决策就会感到任意。我已经努力记录决策及其背后的原因,但是诸如我们是否应该从订单中分配订单项目之类的问题?还是订单中的查询应该能够访问产品的数据库表?从纯粹的技术角度来看,无法真正回答。他们也需要对项目目标的看法。对于阅读此代码的任何人,我鼓励您根据这些任意的设计决策进行审查,考虑一下您在自己的环境中面临的约束,以及这些在Rust构建应用程序时如何告知您自己的决定。
这与特定的锈迹框架或图书馆无关,也不是解决在线购物应用程序固有的问题。
以下各节描述了应用程序的一部分,并解释了为什么它们按照它们的方式进行。
项目布局集中在隐私上。通过限制某些项目的范围,您还限制了潜在破裂的范围。通过限制某些项目的范围,您还限制了维持应用程序状态的负担的范围。在Rust中,该模块的所有孩子都可以看到一个私人的物品。这听起来像是一件坏事,但是我们利用它来防止域API泄漏实施细节,以出于外部关注,例如序列化和存储。
应用程序中的每个核心业务概念都分为自己的(主要)独立文件夹,例如products或customers 。每个模块都封装了有关特定实体集的所有信息:
/store )/queries )/commands )实体可以取决于来自另一个模块的实体,例如添加产品时取决于Product的Order 。每个域模块中都有一个隐私层次结构:
from_data方法from_data到水合实体这些模块有点重量,但是在适当的应用程序中,可以使用宏来简化新的域模块。我没有在此应用程序中使用宏,因此代码仍然易于遵循。
精心制作的模块层次结构的一个问题是,当您最终获得根本不适合当前布局的概念时,它都可以崩溃。发生这种情况的频率越多,就越符合以前存在的布局,因为不可能说出应该是什么。
我们希望这些模块能够管理自己的命运,但是我们不希望它们独立地将它们分为单独的服务。这是为了使事情变得简单。如果您确实想这样做,那么我建议您使用单独的板条箱而不是单独的模块。
该应用程序遵循一个简单的命令查询责任隔离设计。这是一种适合数据驱动的应用程序,没有很多复杂逻辑的方法。命令捕获了一些域交互,并直接在实体上工作,而查询是完全任意的。该应用程序没有使用任何特殊的基础架构来实现CQR,它们只是使用依赖注入模式实现的简单特征。本质上:
Result<()>Result<T>&mut self接收器&self接收器突变性的差异意味着命令可以调用查询,但查询无法调用命令。
实体是应用程序的核心。尽管缺乏真正的生意,但我还是努力使域模型保持富裕。实体不仅是粗糙的州。他们是:
.to_data()来获取实体的读取视图。在查看实体时,您无法在其上调用修改行为。 Rust的借贷系统可以保证这一点。实体可以使用.into_data()将所有权转移到其仅阅读数据中。这是一个单向操作,因此对状态进行的任何更改都不能持续回商店。实体的目的是封装某些关键领域概念的不变性。这里的实体易于使用模拟内存商店或外部数据库。我们应该小心不要依靠状态变化,一个实体在另一个实体中反映在另一个实体,因为它们恰好指向同一来源。
实体还需要注意不要依赖另一个实体的数据类型,因为不能保证数据实际上是有效的。相反,它们依靠实体并根据需要将其转换为数据,因此他们始终知道状态是有效的。
我们使用以下生锈特征来保护我们的实体状态:
Serialize或Deserialize 。这可能会在赛道上进行更改,但是我发现更容易保持可序列化的快速循环以使其向后兼容。实体封装某些状态或数据,并确保对该数据进行的任何更改不会破坏数据期望拥有的任何不变性。我们没有实现Getters,而是将数据视为结构。好处是,您不必像使用Getter方法那样放弃Rust的精美功能来使用数据架构。此视图是只读的,因此更改不能直接写回结构。该实体仍然为此提供设置方法。
您可能会争辩说,以这种方式暴露状态会泄漏实施细节,例如没有公开价值的version 。这可能是正确的。要围绕它,您可以将仅阅读视图的寿命移至字段上,并构成对国家的潜在借来的视图,并将数据结构保留由实体私有的管理。
您还可以争辩说,将不变的结构固定在不存储它们的结构上是脆弱的。当某个字段的隐私边界处于对象级时,就像在C#中一样。生锈有些不同。最紧密的隐私边界是模块及其子女。因此,保持给定田地的不变性的负担落在其定义的模块中的所有项目上,以及所有该模块的孩子。
这听起来像是一个糟糕的泄漏,但是该应用程序利用了构建精心抽象的存储空间。不必在我们的API中露出孔来支撑ORM,而是维护不变性状态只是延伸到模型商店,而不会泄漏回公众。
Id和Version类型都具有幻影通用参数。此参数纯粹是为了让您以Id<ProductData>和Id<OrderData>等不兼容类型的ID表示,但仍能共享其他实现详细信息。
这是一种比使用宏来减少样板更容易遵循的模式,因为您可以回到源中总是存在差异。
每个持久的实体都有一个version字段。该字段是一个非序列标识符,与给定时间点上的实体状态相对应。当从商店中获取实体时,我们会在更新之前检查一下其版本,然后在更新之前检查一下,如果它们不匹配,我们就会受到bal。
版本检查适用于内存商店,因为我们在数据上有一个独家锁定(只有1个呼叫者可以一次修改状态),但是对于适当的DB,将需要其他方法。我们可能可以更新ID和版本匹配的位置,选择更新的记录的数量,如果是0(表示版本不匹配,或者不存在)。
存储层使用简单的交易方案,该方案允许独立数据存储参与交易。中央存储库会跟踪主动交易,并在从数据存储中获取数据时会咨询,以确保可以使用它们。数据的乐观并发性确保多次活动交易不能同时尝试设置相同的值。这违反了真正的隔离,但使事情变得简单,并使我们可以最大程度地减少存储每个值所需的状态。
依赖注入是一种有益的习惯,可以在设计应用程序时倾斜。它使您可以将依赖项分辨率的关注点与App Logic区分开。它还为您提供了扩展应用程序的明显方法。该应用程序采用了一种简单的模式,可以为我们提供这些好处,而无需大量基础架构。
如果您编写.NET应用程序,则此应用程序不会像您可能使用的控制容器的反转。这主要是因为Rust实际上没有。这是一个棘手的问题。不过,即使没有复杂的容器,它也确实利用了简单的依赖注入模式来撰写命令和查询。
依赖注入的主要目标不是支持嘲笑。这是通过将外围问题推向单个组件的逻辑来降低复杂性。
注射的组件生活在自己的模块中。该模块包含:
Resolver类型的INGH块,该类型包含一种返回默认实现的方法而无需其依赖项。impl Trait 。您永远不知道这种默认实现使用的具体类型。Arc , Box )实现的组件的特征。共享的Resolver听起来有点服务 - 固定器,但是由于依赖项分辨率完全包含在Resolver本身上的IMPH块中,所以我们避免了依赖于App Logic中魔术全局状态的问题。
为了减少样板,对于只有一种方法的组件,我们还毯子以Fn特征实现它们。这使您可以避免为他们宣布对他们所有依赖性的通用结构。生锈的编译器将为您照顾。
这种模式很难在散文中描述,您需要看到它。查看domain/products/commands/create_product模块,或domain/products/model/store模块,以获取工作中此依赖项注入模式的示例。
Resolver不是“上帝的对象”吗?上帝的对象“是您应用程序中的一个对象,它将所有重要逻辑收集到一个您无法与组件一起工作的点,而不还要通过上帝的目的工作。它们是一个问题,因为它们变得难以构造或改变。这里的Resolver模式是上帝的对象,但不是要构建单个组件的必要条件。 Resolver仅与依赖者相处或不需要组成的组件,或者与之构建所需的组成部分,或者构建所需的组成部分。