稱呼:
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僅與依賴者相處或不需要組成的組件,或者與之構建所需的組成部分,或者構建所需的組成部分。