架構原則Architectural principles

「如果營造商要像程式設計人員撰寫程式那樣地蓋房子,那麼第一隻經過的啄木鳥將會摧毀文明。」"If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization."
- Gerald Weinberg- Gerald Weinberg

您在架構與設計軟體解決方案時,應該惦記著可維護性。You should architect and design software solutions with maintainability in mind. 本節中所述的準則可協助引導您做出將產生可維護之全新應用程式的架構決策。The principles outlined in this section can help guide you toward architectural decisions that will result in clean, maintainable applications. 一般而言,這些原則會引導您利用與應用程式其他部分未緊密結合,而是透過明確介面或傳訊系統進行通訊的不同元件來建置應用程式。Generally, these principles will guide you toward building applications out of discrete components that are not tightly coupled to other parts of your application, but rather communicate through explicit interfaces or messaging systems.

一般設計原則Common design principles

關注點分離Separation of concerns

開發時的一項指導原則是關注點分離A guiding principle when developing is Separation of Concerns. 這個原則判斷提示軟體應該根據它所執行的工作種類來分離。This principle asserts that software should be separated based on the kinds of work it performs. 比方說,假設應用程式中包含邏輯來識別值得注意的項目,以便顯示給使用者,而且特別格式化此類項目,使其更容易被注意到。For instance, consider an application that includes logic for identifying noteworthy items to display to the user, and which formats such items in a particular way to make them more noticeable. 負責選擇要格式化之項目的行為,應該與負責格式化項目的行為分離,因為這些分離關注點只是碰巧彼此相關。The behavior responsible for choosing which items to format should be kept separate from the behavior responsible for formatting the items, since these are separate concerns that are only coincidentally related to one another.

在架構上,您可以藉由從基礎結構和使用者介面邏輯分離核心商務行為以邏輯方式來建置應用程式,以遵循這個原則。Architecturally, applications can be logically built to follow this principle by separating core business behavior from infrastructure and user interface logic. 在理想情況下,商務規則和邏輯應該位於個別的專案,且不應依賴應用程式中的其他專案。Ideally, business rules and logic should reside in a separate project, which should not depend on other projects in the application. 這有助於確保商務模型很容易測試,並且不必緊密結合低層級的實作詳細資料就能持續改進。This helps ensure that the business model is easy to test and can evolve without being tightly coupled to low-level implementation details. 關注點分離是在應用程式架構中使用層級背後的一項重要考量。Separation of concerns is a key consideration behind the use of layers in application architectures.

封裝Encapsulation

應用程式的不同部分應該使用封裝,將它們與應用程式的其他部分隔離。Different parts of an application should use encapsulation to insulate them from other parts of the application. 應用程式元件和層級應該能夠調整內部實作,而且只要不違反外部的合約,便不必中斷其共同作業者。Application components and layers should be able to adjust their internal implementation without breaking their collaborators as long as external contracts are not violated. 正確地使用封裝可協助達到應用程式設計中的鬆散結合和模組化,因為物件與套件可以取代為替代的實作,只要維持相同的介面即可。Proper use of encapsulation helps achieve loose coupling and modularity in application designs, since objects and packages can be replaced with alternative implementations so long as the same interface is maintained.

在類別中,封裝是藉由限制對類別內部狀態的外部存取而達成。In classes, encapsulation is achieved by limiting outside access to the class's internal state. 如果外部執行者想要操作物件的狀態,則應透過妥善定義的函式 (或屬性 setter) 來達成,而不是直接存取物件的私用狀態。If an outside actor wants to manipulate the state of the object, it should do so through a well-defined function (or property setter), rather than having direct access to the private state of the object. 同樣地,應用程式元件和應用程式本身應該公開妥善定義的介面,供它們的共同作業者來使用,而不是允許直接修改它們的狀態。Likewise, application components and applications themselves should expose well-defined interfaces for their collaborators to use, rather than allowing their state to be modified directly. 如此應用程式的內部設計便可以隨著時間而持續改進,且不必擔心這麼做會中斷共同作業者,只要維護公用合約即可。This frees the application's internal design to evolve over time without worrying that doing so will break collaborators, so long as the public contracts are maintained.

相依性反轉Dependency inversion

應用程式內的相依性方向應該是抽象的方向,而不是實作詳細資料。The direction of dependency within the application should be in the direction of abstraction, not implementation details. 大部分的應用程式撰寫時,編譯時間相依性會以執行階段執行的方向流動。Most applications are written such that compile-time dependency flows in the direction of runtime execution. 這會產生直接相依性圖形。This produces a direct dependency graph. 也就是說,如果模組 A 呼叫模組 B 中的函式,而它呼叫模組 C 中的函式,則在編譯階段 A 會相依於 B,B 會相依於 C,如圖 4-1 所示。That is, if module A calls a function in module B, which calls a function in module C, then at compile time A will depend on B which will depend on C, as shown in Figure 4-1.

圖 4-1.Figure 4-1. 直接相依性圖形。Direct dependency graph.

套用相依性反轉原則會允許 A 呼叫由 B 實作之抽象的方法,使得 A 能夠在執行階段呼叫 B,但 B 在編譯階段會相依於由 A 控制的介面 (因此「反轉」一般的編譯階段相依性)。Applying the dependency inversion principle allows A to call methods on an abstraction that B implements, making it possible for A to call B at runtime, but for B to depend on an interface controlled by A at compile time (thus, inverting the typical compile-time dependency). 在執行階段,程式執行流程維持不變,但是介面的引進表示可以輕鬆地插入這些介面的不同實作。At run time, the flow of program execution remains unchanged, but the introduction of interfaces means that different implementations of these interfaces can easily be plugged in.

圖 4-2.Figure 4-2. 反轉相依性圖形。Inverted dependency graph.

相依性反轉是建置鬆散結合應用程式的重要部分,因為可以撰寫實作詳細資料以相依於和實作較高的抽象層級,而不是利用其他方式。Dependency inversion is a key part of building loosely-coupled applications, since implementation details can be written to depend on and implement higher level abstractions, rather than the other way around. 因此,所產生的應用程式會比較可測試、模組化且可維護。The resulting applications are more testable, modular, and maintainable as a result. 遵循相依性反轉準則,即可達成「相依性插入」。The practice of dependency injection is made possible by following the dependency inversion principle.

明確相依性Explicit dependencies

方法和類別應該明確需要正常運作所需的任何共同作業物件。Methods and classes should explicitly require any collaborating objects they need in order to function correctly. 類別建構函式會提供一個機會,讓類別能識別它們處於有效狀態並正常運作所需的項目。Class constructors provide an opportunity for classes to identify the things they need in order to be in a valid state and to function properly. 如果您定義的類別,可以建構和呼叫,但只有在特定全域或基礎結構元件已就緒時才能正常運作,那麼這些類別對其用戶端便「不誠實」。If you define classes that can be constructed and called, but which will only function properly if certain global or infrastructure components are in place, these classes are being dishonest with their clients. 建構函式合約告訴用戶端它只需要指定的項目 (如果類別只使用預設建構函式則可能沒有任何項目),但在執行階段變成物件確實需要其他項目。The constructor contract is telling the client that it only needs the things specified (possibly nothing if the class is just using a default constructor), but then at runtime it turns out the object really did need something else.

藉由遵循明確的相依性原則,您的類別和方法對其用戶端便會誠實告知他們需要要什麼才能運作。By following the explicit dependencies principle, your classes and methods are being honest with their clients about what they need in order to function. 這樣讓您的程式碼可以更加自我記錄,且讓程式碼合約更加易懂易記,因為使用者會信任,只要他們以方法或建構函式參數的形式提供所需的項目,他們正在使用的物件便會在執行階段有正確的行為。This makes your code more self-documenting and your coding contracts more user-friendly, since users will come to trust that as long as they provide what's required in the form of method or constructor parameters, the objects they're working with will behave correctly at runtime.

單一責任Single responsibility

單一責任原則適用於物件導向設計,但也可視為類似關注點分離的架構原則。The single responsibility principle applies to object-oriented design, but can also be considered as an architectural principle similar to separation of concerns. 它指出物件應該只有一項責任,而且應該只有一個變更的原因。It states that objects should have only one responsibility and that they should have only one reason to change. 具體而言,物件唯一應該變更的情況是,它執行它的一項責任的方式必須更新時。Specifically, the only situation in which the object should change is if the manner in which it performs its one responsibility must be updated. 遵循此原則有助於產生更鬆散結合且模組化的系統,因為許多種新行為可以實作為新的類別,而不是新增額外的責任給現有類別。Following this principle helps to produce more loosely-coupled and modular systems, since many kinds of new behavior can be implemented as new classes, rather than by adding additional responsibility to existing classes. 新增類別一比變更現有類別安全,因為還沒有程式碼相依於新的類別。Adding new classes is always safer than changing existing classes, since no code yet depends on the new classes.

在整合應用程式中,我們可以在高層級套用單一責任原則至應用程式中的層級。In a monolithic application, we can apply the single responsibility principle at a high level to the layers in the application. 簡報責任應保留在 UI 專案,而資料存取責任則應保留在基礎結構專案中。Presentation responsibility should remain in the UI project, while data access responsibility should be kept within an infrastructure project. 商務邏輯應該保留在應用程式核心專案,在這裡它可以輕鬆地測試,且可以獨立於其他責任之外地持續改進。Business logic should be kept in the application core project, where it can be easily tested and can evolve independently from other responsibilities.

當此原則套用至應用程式架構,並帶到其邏輯端點時,便會得到微服務。When this principle is applied to application architecture, and taken to its logical endpoint, you get microservices. 指定的微服務應該具有單一責任。A given microservice should have a single responsibility. 如果您需要擴充系統的行為,通常新增其他微服務會比較好,而不要新增責任至現有的微服務。If you need to extend the behavior of a system, it's usually better to do it by adding additional microservices, rather than by adding responsibility to an existing one.

深入了解微服務架構Learn more about microservices architecture

不重複原則 (DRY)Don't repeat yourself (DRY)

應用程式應避免在多個位置指定與特定概念相關的行為,因為這是常見的錯誤來源。The application should avoid specifying behavior related to a particular concept in multiple places as this is a frequent source of errors. 在某個時間點,需求變更將需要變更此行為,而行為的至少一個執行個體無法更新的可能性,會導致系統的行為不一致。At some point, a change in requirements will require changing this behavior and the likelihood that at least one instance of the behavior will fail to be updated will result in inconsistent behavior of the system.

請不要複製邏輯,而是要將它封裝在程式設計建構中。Rather than duplicating logic, encapsulate it in a programming construct. 讓此建構成為此行為的單一授權,而且讓應用程式中需要這個行為的任何其他部分都使用新建構。Make this construct the single authority over this behavior, and have any other part of the application that requires this behavior use the new construct.

注意

避免將湊巧重複的行為繫結在一起。Avoid binding together behavior that is only coincidentally repetitive. 例如,只是兩個不同的常數具有相同的值,並不表示您應該只有一個常數,如果在概念上它們是指不同項目的話。For example, just because two different constants both have the same value, that doesn't mean you should have only one constant, if conceptually they're referring to different things.

持續性無知Persistence ignorance

續性無知 (PI) 指的是需要持續的,但其程式碼不會受到持續性技術選項影響的類型。Persistence ignorance (PI) refers to types that need to be persisted, but whose code is unaffected by the choice of persistence technology. 這類的類型在 .NET 中有時稱為簡單的 CLR 物件 (POCO),因為它們不需要繼承特定的基底類別或實作特定介面。Such types in .NET are sometimes referred to as Plain Old CLR Objects (POCOs), because they do not need to inherit from a particular base class or implement a particular interface. 持續性無知的價值在於它允許以多種方式保存相同的商務模型,為應用程式提供額外的彈性。Persistence ignorance is valuable because it allows the same business model to be persisted in multiple ways, offering additional flexibility to the application. 持續性選項可能會隨著時間變更,從一種資料庫技術變為另一種技術,或是除了應用程式一開始的選項之外,可能還需要其他形式的持續性 (例如,除了關聯式資料庫之外,還使用 Redis 快取或 Azure DocumentDb)。Persistence choices might change over time, from one database technology to another, or additional forms of persistence might be required in addition to whatever the application started with (for example, using a Redis cache or Azure DocumentDb in addition to a relational database).

違反這個原則的一些範例包括:Some examples of violations of this principle include:

  • 必要的基底類別。A required base class.

  • 必要的介面實作。A required interface implementation.

  • 負責自行儲存的類別 (例如使用中的記錄模式)。Classes responsible for saving themselves (such as the Active Record pattern).

  • 必要的預設建構函式。Required default constructor.

  • 需要虛擬關鍵字的屬性。Properties requiring virtual keyword.

  • 持續性特定的必要屬性。Persistence-specific required attributes.

類別有任何上述功能或行為的要求,會為要持續保存的類型與持續性技術選擇之間新增結合,使得更難以在未來採用新的資料存取策略。The requirement that classes have any of the above features or behaviors adds coupling between the types to be persisted and the choice of persistence technology, making it more difficult to adopt new data access strategies in the future.

繫結內容Bounded contexts

繫結內容是 Domain-Driven 設計的中心模式。Bounded contexts are a central pattern in Domain-Driven Design. 它們藉由將大型應用程式或組織的複雜性分成不同的概念模組,提供處理複雜性的方法。They provide a way of tackling complexity in large applications or organizations by breaking it up into separate conceptual modules. 每個概念模組會代表與其他內容不同的內容 (因此而繫結),並可以獨立持續改進。Each conceptual module then represents a context which is separated from other contexts (hence, bounded), and can evolve independently. 每個繫結內容在理想情況下應該能自由選擇自己的概念名稱,而且應該對它自己的持續性存放區具有獨佔存取權。Each bounded context should ideally be free to choose its own names for concepts within it, and should have exclusive access to its own persistence store.

至少,個別 Web 應用程式應該致力於成為自己的繫結內容,並且具有自己商務模型的持續性存放區,而不與其他應用程式共用一個資料庫。At a minimum, individual web applications should strive to be their own bounded context, with their own persistence store for their business model, rather than sharing a database with other applications. 繫結內容之間的通訊會透過程式設計介面進行,而不是透過共用的資料庫,這樣可讓商務邏輯和事件發生以回應發生的變更。Communication between bounded contexts occurs through programmatic interfaces, rather than through a shared database, which allows for business logic and events to take place in response to changes that take place. 繫結內容與微服務密切對應,而微服務在理想的情況下也實作為自己的個別繫結內容。Bounded contexts map closely to microservices, which also are ideally implemented as their own individual bounded contexts.

其他資源Additional resources