在对NLayerApp实际项目进行讨论之前,让我们首先学习一下(或者应该说重温一下)分层/多层架构与应用系统设计原则。很多朋友会认为这些都是老掉牙的内容,只要是软件从业人员,都会对这些内容非常熟悉。然而,果真如此吗?我在这里整理这部分内容,一方面是为介绍NLayerApp打下基础,而另一方面,则是希望借此机会将这些理论性的东西做个归纳,也希望读者朋友能够认真阅读,毕竟温故知新嘛。
需要说明的是,从本章节开始,大多数理论性的东西都源自Microsoft Spain团队针对NLayerApp所编写的《Architecture Guide Book》,事实上这本Guideline的英文版至今也还没有完成,我会从中抽出部分章节做些翻译和归纳,有兴趣的朋友请直接上microsoftnalyerapp.codeplex.com站点上下载英文版阅读。
Layers与Tiers
对Layers与Tiers这两个单词进行区分是非常重要的。从中文翻译看,两者都是层的意思,因此我们往往会将这两个概念弄混。Layer一词更多的是表示对系统组件或功能的逻辑区分,它并没有包含将组件分布到不同的区域、不同的服务器上的意思。而Tier则是表示系统组件和功能在服务器、网络环境以及远程位置的物理部署。尽管这两个概念同时使用者非常相近的一些术语,比如展示、服务、业务和数据等,但我们必须了解它们之间的差别。下面这幅图表明了多层(N-Layer)逻辑架构与三层(3-Tier)物理结构之间的差异:
需要注意的是,对于具有一定复杂度的应用程序而言,采用多层(N-Layer)逻辑架构的实现方式是非常必要的,这会降低系统的复杂度,并在设计、开发、测试、部署及维护等各个环节为应用系统带来高可用性、高延展性等正面效应。然而,并非所有的应用程序必须以三层(3-Tier)/多层(N-Tier)物理结构进行部署,我们可以将多个逻辑层部署在同一台机器上,也可以根据需求,将这些逻辑层部署在网络中的不同机器上。
逻辑分层(Layer)的设计
在讨论DDD的分层之前,先让我们看看传统的分层方式。就像上文所述,我们应该根据项目的实际需求,将组件/功能模块合理地划分到逻辑层中。同一层中的组件,应该是高内聚的,并具有相同的抽象层次。层与层之间应该低耦合。对于以分层设计的应用程序而言,最关键的问题就是如何处理层与层之间的依赖关系。考察传统的多层架构应用,处于某层的组件,只能对同层或下层的其它组件进行访问,这样做可以有效地降低层与层之间的依赖关系。通常会有两种分层设计:严格分层与灵活分层
- 严格分层迫使组件只能访问同层的其它组件,或者只能访问直接下层的其它组件,于是,第N层的组件只能访问第N或N-1层的组件,而第N-1层的组件只能访问第N-1或N-2层的组件,以此类推
- 灵活分层允许组件访问同层的其它组件,以及所有下层的其它组件,于是,第N层的组件可以访问第N或N-1、N-2…层的组件
使用灵活分层的架构可以提高系统性能,因为这样的结构无需引入过多的请求/反馈的传递操作,因为一个层可以直接访问位于其下的任何层;而严格分层却降低了层与层之间的耦合性,对低层的修改不会对整个系统造成广泛的影响。根据Eric Evans在其《领域驱动设计-软件核心复杂性应对之道》一书中的描述,DDD的分层选用的是灵活分层模式。
让我们再把讨论的粒度细化,来看看层中的组件与组件之间的关系。事实上,在很多复杂的应用中,位于同一层的组件与组件虽然具有相同的抽象层次,它们也不一定是高内聚的。因此,我们可以引入模块(Module)的概念,将同一层中高内聚的组件放在同一个模块中,于是,每个层又会由一个或多个高内聚的子系统(模块)所组成,如下UML组件图所示:
使用分层架构,有如下几点好处:
- 提高系统的可测试性
- 对解决方案的维护和管理变得更加简单。层内高内聚、层间低耦合的结构,使得系统实现与分层组织方式变得非常灵活方便
- 其它外部应用程序能够非常方便地使用不同的层所提供的特定功能
- 当系统以层的方式进行组织时,分布式开发也变得非常简单易行
- 在某些情况下,分层系统的物理部署方式能够给系统带来延展性,当然,应该有效地评估具体的实践方式,因为这种做法有可能损伤系统性能
应用系统基本设计原则 - SOLID
应用系统的设计应该遵循一些基本的设计原则,这能帮助你有效地创建一个低成本、高可用、高可扩展的应用程序。在这里,我们引入一个SOLID设计原则,SOLID由如下几点构成:
- Single Responsibility Principle(单一职责原则)
- Open Close Principle(开-闭原则)
- Liskov Substitution Principle(里氏替换原则)
- Interface Segregation Principle(接口分离原则)
- Dependency Inversion Principle(依赖反转原则)
下面简要介绍一下这几个原则。
- 单一职责原则:每个类应该只有一个独一无二的职责,或者说每个类只能有一个主要功能,由此派生出一个结论:每个类应该尽可能少地依赖于其它类
- 开-闭原则:每个类,应该对扩展进行开放,而对修改进行封闭,也就是支持扩展,而不是支持修改:类中的方法可以通过继承关系进行扩展,而不会改变类本身的代码
- 里氏替换原则:子类可以被基类型(基类或者接口)替换。应用程序依赖抽象运行,其行为不会因为具体实现的改变而更改,应用程序应该依赖于抽象(基类或者接口),而不是具体实现。接下来将要讨论到的依赖注入(Dependency Injection)就与这条原则有关
- 接口分离原则:接口的职责也应该是单一的,接口中应该包含哪些方法,需要进行严格的评估,如果其中某些方法的职责与接口的本身定义不相符合,则应该将其分离到其它接口中。类需要根据其调用者所需要的不同接口类型,来暴露不同的接口
- 依赖反转原则:抽象不能依赖于具体,而具体则应该依赖于抽象。类之间的直接依赖应该用抽象来取代,这样做的一个优点是,我们可以实现自上而下的设计方式:在下层的具体实现还没有确定的情况下,只要能够在抽象层面将接口确定,就能够完成上层的设计与开发,这同样给可测试性带来便捷
除了以上所述的SOLID原则之外,还有以下几个关键的设计原则可供参考:
- 组件设计应该是高内聚的:相信大家都很熟悉这点了,就不多说了。例如:不要将数据访问逻辑写进领域模型的业务逻辑中,这与上述单一职责原则是密切相关的
- 将Cross-Cutting的代码从特定于应用程序的逻辑中分离开来:Cross-Cutting的代码是一些面向横面的代码,比如安全、操作管理、日志以及测量/计量系统等。将这些代码与应用系统业务逻辑混在一起会增加系统的复杂性,给将来的扩展和维护造成很大的麻烦。这与面向方面编程(Aspect-Oriented Programming,AOP)有关
- 关注点分离(Separation of Concerns,SoC):将应用系统分成多个子部分(子系统),各个部分之间的功能尽量不要重复,其目的就是为了减少交互点,以实现高内聚和低耦合
- Don’t Repeat Yourself(DRY):一个特定的功能只能在某个特定的组件中实现一次,同样的功能不要在多个组件中重复多次
- 避免YAGNI(You Ain’t Gonna Need It)效应:只考虑和设计必须的功能,避免过度设计
好了,本讲就介绍到这里,估计对大多数接触过架构的软件朋友来说,本讲的部分内容都是废话。下一讲开始,我会花部分笔墨在DDD/DDDD的分层介绍上,虽然有可能还是废话,但这对我们理解NLayerApp的解决方案组织结构会有相当的帮助。