发布于 

DDD - 架构设计原则

本站字数:106.9k    本文字数:3.1k    预计阅读时长:10min    访问次数:

你应该在构建和设计软件解决方案的时候是可考虑可维护性。在这个章节,这些原则的大纲就是帮助指导你做架构决策,从而产生整洁、可维护的应用程序。通常,这些原则将会指导你去使用离散的组件构建应用程序,这些组件通常不是和你的应用程序其他部分紧耦合的,而是通过显式接口或者消息系统来进行通信。

关注点分离(Seperation of Concerns)

开发过程中一个指导性的原则就是 关注点分离(Sepration of Concerns) 原则(关注点也可以理解为职责)。这个原则假定软件应该基于工作表现的种类被分割。举个栗子,考虑一个应用程序,需要识别值得展示给用户的条目的业务逻辑,并且使用更加醒目的方式格式化这些条目。在设计的过程中,负责选择要设置格式的条目的行为 应该与 负责设置项目各是的行为 分开,因为这些行为是单独的关注点,彼此之间只是巧偶然的关联。

架构上来说,通过将核心业务分离为基础设施和UI的逻辑两个部分,应用程序从而遵循这个设计原则。理想情况下,逻辑角色和逻辑应该放在被隔离的项目中,同时这个项目不依赖于其他任何项目。这样的分离帮助我们保证业务模型很容易去测试,并且我们可以在不紧耦合底层细节实现的情况下,实现业务逻辑的演进。如果基础设施的关注点依赖于业务层的抽象定义,这样的分离也是很有帮助的。

封装性(Encapsulation)

应用程序不同的部分应该是使用封装性(Encapsulation)使得他们和其他部分隔离。应用程序组件和层能够去调整他们内部的实现,只要外部的合约(接口)没有被修改,那么模块间协作就不会因为实现细节的更改而中断。合理使用封装性原则可以帮助我们归档松耦合和模块化的应用程序设计,因为对象和接口只要保持接口是相同的,就可以替换任何接口的实现。

在类中,封装是通过限制外部访问类的内部状态来实现的。如果外部参与者想要操纵对象的状态,它应该通过定义良好的方法(或属性设置器)来操作,而不是直接访问对象的私有状态。同样,应用程序组件和应用程序本身应该公开定义良好的接口供其合作者使用,而不是允许直接修改其状态。这个方法实现了应用程序的内部实现演进的自由。同时演进过程中,只要保证公共接口不变,就不用担心所做的工作会破坏协作者。

(晦涩难懂的一段)可变的全局状态可以破坏封装性。从一个函数中的可变全局状态获取的值不能依赖于在另一个函数(甚至在同一函数中)具有相同的值(个人理解就像是幻读问题)。值得注意的是,依赖于中央数据库进行应用程序内部和之间集成的数据驱动架构本身选择依赖于数据库所表示的可变全局状态。领域驱动设计和整洁架构中的一个关键考虑因素是如何封装对数据的访问,以及如何确保应用程序状态不会因直接访问其持久性格式而无效。

依赖反转(Dependency Inversion)

在应用程序中的依赖方向应该是抽象方向,而不是实现细节。对于大部分应用程序,编译时依赖流向就是运行时的执行的方向,从而生成直接依赖关系图。也就是说,如果类A调用类B的方法,而类B调用类C的方法,那么在编译时,类A将依赖于类B,类B依赖于类C。

img

应用依赖性反转原则允许A调用B实现的抽象上的方法,使得A可以在运行时调用B,但B可以在编译时依赖于A控制的接口(因此,反转了典型的编译时依赖性)。在运行时,程序执行流保持不变,但接口的引入意味着可以轻松插入这些接口的不同实现。

img

依赖倒置是构建松散耦合应用程序的关键部分,因为可以编写实现细节来依赖和实现更高级别的抽象,而不是相反。由此产生的应用程序更易于测试、模块化和维护。依赖项注入的实践是通过遵循依赖反转原理而实现的。

显式依赖(Explicit Dependencies)

方法和类应该显式地获取任何合作的对象,它们需要这些对象使得功能正确。这样的原则 Ardalis 称这样的原则为显式依赖原则。类构造器为类提供了一个机会去识别他们需要的依赖对象,类构造器一定程度上可以保证对象构造出来是有效的而且功能完全。

如果你定义了可以构造和调用的类,但这些类只有在某些全局或基础结构组件就位时才能正常运行,那么这些类对它们的客户端是不诚实的。构造器契约告诉客户端它只需要指定的东西(如果类只是使用无参数构造器,可能什么都不需要),但在运行时,它发现对象确实需要其他东西。

通过显式依赖原则,你的类和方法对于他们的客户端说明了,你的类和方法需要的保证功能正常的依赖。遵循这个原则可以使得你的代码有更多的自文档性。并且你的代码契约更加用户友好,因为用户将开始相信,只要他们提供类和对象在方法或者类构造器中表示的参数,对象就可以在运行时工作正常。

单一职责(Single Responsibility)

单一职责原则是一个面向对象设计原则,但是也可以认为是一个架构设计原则类似于关注点分离(Separation of Concerns)原则。它表述为,对象应该仅仅有一个职责,并且他们应该仅有一个改变的理由。特殊地,对象应该改变的唯一情况就是,它执行其一项职责的方式必须需要更新。遵循这样的原则有助于设计更加松散耦合和模块化的系统,因为许多新类型的行为可以被实现为新的类,而不是在一个类上面新增更多的职责(新增方法)。新增新的类永远比修改一个类更加安全,因为没有代码依赖于这个新增的类(开闭原则)。

在一个单机应用中,我们可以在应用程序的高层使用这个单一职责原则。表现层的职责应该保留在UI项目中,同时数据访问的职责应该保留到基础设施(Infrustructure)项目中。业务逻辑应该被封装到应用程序核心(Application Core)项目中,如果封装到项目中他可以被很容易地测试并且可以独立地进行演进不受其他职责的影响。

当这个原则被用于应用程序架构,并且将其应用于逻辑,那么你将得到微服务架构。给定的微服务应该有一个单一的职责。 如果您需要扩展系统的行为,通常最好通过添加额外的微服务来实现,而不是向现有微服务添加责任。

Don’t repeat yourself (DRY)

应用程序应该避免在多个地方指定与特定概念相关的行为,因为这种做法是错误的常见来源。在某些情况下,在需求改变的情况下将会需要修改(对象/程序的)行为。因为在其他地方重复了很多这个行为,只修改这个对象的行为很可能会导致更新失败,进而导致整个系统的行为不一致。

和重复逻辑相比,将其逻辑封装在程序结构中更好。使得这个行为的构造在程序中只有一处,如果程序其他部分想要使用这个构造的时候只需要新建这个构造就好了。

注意

避免将只是巧合重复的行为绑定在一起。例如,仅仅因为两个不同的常量都有相同的值,这并不意味着你应该只有一个常量,如果概念上它们指的是不同的东西。复制总是比耦合到错误的抽象更可取。

持久化透明(Persistence Ignorance)

持久化透明(PI)指的是需要被持久化的类型,但是类型的代码并不会被持久化使用的技术所影响。这样的类型在.Net中有时候被称为 POCOs(Plain Old CLR Object)。因为他们呢不需要从一个特定的类派生而来,或者说去实现某个特定的接口。持久化透明是有价值的,因为它允许相同的业务逻辑模型可以通过多种方式去持久化,为应用程序提供了额外的灵活性。持久化的选择可能会随着时间而改变,从一个数据库技术到另一个,或者为持久化提供其他额外的形式(例如,使用Redis缓存,或者额外使用PostgreSQL作为关系型数据库)。

下面有展示一些违背了这个原则的例子:

  • 一个必须的基类
  • 一个必须的接口实现
  • 类有责任去实现保存他们自己(例如,活跃记录模式(Active Record))
  • 必须有的无参构造器
  • 属性(Properties)需要 virtual 关键字
  • 持久化规范需要注解

类具有上述任何特性或行为的要求增加了要持久化的类型和持久化技术的选择之间的耦合,使得将来采用新的数据访问策略更加困难。

限界上下文(Bounded Context)

限界上下文(Bounded Context)是DDD的核心模式。他们提供了一个处理大型应用程序或者组织的方式,通过将其切分成相互隔离的概念模块。然后,每个概念模块代表了一个和其他模块分离的上下文(因为每个上下文都是限界的),并且可以独立的演进。最理想的情况,每个限界上下文应该可以自由的在它的范围内拥有自己的概念名,并且应该有自己持久化存储的独占访问。

对于一个最小的,独立的 Web 应用程序应该尽量拥有它自己的限界上下文,并且为他们的业务模型提供单独的持久化存储,而不是和其他应用程序共享一个数据库。限界上线问之间的通信应该通过编程的接口,而不是通过共享的数据库,允许业务逻辑和事件响应发生的更改。限界上下文和微服务紧密映射,而且微服务在理想情况下也应该被实现为他们自己的有界上下文。

参考资料