三层架构的核心思想.01
分层架构的关键性思想
.逐层调用原则及单向调用原则
假设约定将N层架构的各层依次编号为1、2、...、K、...、N-1、N,其中层的编号越大,越处于上层。
那么,我们设计的架构应该满足以下两个原则:
1.第K(1<K<=N)层只准依赖第K-1层,而不可依赖其他底层。
2.如果P层依赖Q层,则P的编号一定大于Q。
其中第一个原则,保证了依赖的逐层性,以及整个架构依赖是逐层向下的,而不能跨层依赖。第二个原则,保证
了依赖的单向性,及只能上层依赖底层,而不能底层反过来依赖上层。
.针对接口编程,而不是针对实现编程
这里所指的接口,不是特指编程语言中的具体语言元素(如C#中由Interface定义的语言接口),而是指一种
抽象的,在语义层面上起着接合作用的语义体。它的具体实现,可能是接口,可能是抽象类,甚至可能是具体类。
我认为,从不同的视角,接口可以有以下两种定义:
1.接口是一组规则的集合,它规定了实现本接口的类或接口必须拥有的一组规则。体现了自然界"如果你是...则必须是..."
的理念。注解:比如 我定义了一个人的接口,该接口有吃饭、睡觉、思考。所有继承该"人"的接口,就必须是会(或实现)
吃饭、睡觉、思考。
2.接口是在一定粒度视图上同类事物的抽象表示。注意这里我强调了在一定粒度视图上,因为"同类事物"是相对的,它因
粒度视图不同而不同。
具体到N层架构中,针对接口编程的意义是这样的:
现仍约定将N层架构的各层依次编号为1、2、...、K、...、N-1、N,其中层的编号越大,则越处在上层, 那么第K层
不应该依赖具体一个K-1层,而应该依赖一个K-1层的接口,即在第K层中不应该有K-1层中的某个具体类。注解:比如
"U盘"有"U盘接口",我们用电脑连接U盘,使用U盘,是依赖于连接U盘的接口来使用U盘的。
.依赖倒置原则(DIP)
在软件设计原则中,有一种重要的思想叫做依赖倒置。它的核心思想是:
“不能让高层组件依赖底层组件,而且不管高层组件和底层,两者都应该依赖于抽象。”
那么,这个原则和我们上面的原则是否矛盾呢?其实并不矛盾。
依赖倒置原则核心思想中提到的[依赖]是指[具体依赖],而上面定义的[依赖]全部指[抽象依赖]。我对这两种依赖的定义如下:
1.具体依赖--如果P层中有一个或一个以上的地方实例化了Q层中的某个具体类,则说P层具体依赖于Q层。
2.抽象依赖--如果P层没有实例化Q层中的具体类,而是在一个或一个以上的地方实例化了Q层中的某个接口,则说
P层抽象依赖于Q层,也叫接口依赖于Q层。
从这两个定义可以看到,所谓的依赖倒置原则,正是上面提到的针对接口编程,而不是针对实现编程,两者在本质上是统一的。
注1:面向接口编程就是"依赖倒置原则"的实际运用。
注2:[依赖]就是软件设计中通常所说的[耦合]。
摘.Rational Rose是这样定义[依赖]关系的:[依赖]描述了两个模型元素之间的关系,如果被依赖的模型元素发生变化
就会影响到另一个模型元素。典型的,在类图上,[依赖]关系表明客户类的操作会调用服务器类的操作。(个人理解,依赖产生调用。)
Martin Fowler在《Reducing Coupling》一文中这样描述耦合:"如果改变程序的一个模块要求另一个模块同时发生变化,
就认为这两个模块发生了耦合。"
从上面的定义可以看出:如果模块A调用模块B提供的方法,或访问模块B中的某些数据成员(当然,在面向对象开发中一般不提倡这样做),
我们就认为模块A依赖于模块B,模块A和模块B之间发生了耦合。
综上所述,可以看出,PetShop的分层架构应该是这样一种架构:
1.N层架构的各层依次编号为1、2、...、K、...、N-1、N,其中各层的编号越大,则越处在上层。
2.架构中仅存在一种依赖,即第K层抽象(或接口)依赖第K-1层,其中1<K<=N。
.封装变化原则
封装变化的原则定义为:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混杂在一起。
.开放-关闭原则
开放-关闭原则定义为:对扩展开放,对修改关闭。
具体到N层架构中,可以描述为:当K-1层有了一个新的具体实现时,它应该可以在不修改K层的情况下,与K层无缝连接,顺利交互。
.单一职责原则
在这个架构中,任何一个类都应该有单一的职责,属于单独的一层,而不能同时担负两种职责或属于多个层。
.接口平行原则
这个原则是说,某一个实体对应的接口应该是平级且平行的,而不应该跨越多个实体或多个级别。
比如,在BBS系统中,用户是一个实体,它对应如下接口组:"添加用户"、"修改用户信息"、"删除用户"、"用户登陆"、
"按ID获取用户信息"、"获得全部用户信息"、...,这些接口都是对用户操作,他们是平级且平行的。如果这里出现一个"发布帖子",
这个"发布帖子"接口的操作实体就不是用户,所以它不应该出现在这里。
----------------------------------------------------------------------------------------------
关于依赖倒置原则,我找到一篇说得比较清楚通俗易懂的,引用如下:
原文连接:http://hi.baidu.com/sanlng/blog/item/ecd9f4439068a11273f05d14.html
一、什么是依赖倒置原则
1、高层模块不应该依赖于底层模块,二者都应该依赖于抽象
2、抽象不应该依赖于细节,细节应该依赖于抽象
二、关于高层模块与底层模块
高层模块是系统不经常发生变化的部分,是一个系统区别于其它系统的重要标志,也是直接面向客户的部分,
它包含了系统的策略选择与业务模型。 低层模块是系统中经常发生变化的部分,是系统的实现,是用于驱动系统工作的,
它不是(直接)面向客户的。
三、违反DIP原则的后果
DIP原则其实强调的是:不要让不经常发生变化的部分去依赖于经常发生变化的部分。因为一旦经常发生变化的
那部分发生了变化,那不经常发生变化的那部分也要随之变化。这是不合理的设计。更坏的情况是,违反DIP的设计
会使你的高层模块很难在不同的场合在被重用,因为此时高层模块的工作是依赖于底层模块的,这种依赖性使高层模块很难独立开来。
四、依赖抽象与接口所有权的倒置
高层模块与低层模块都应该依赖于抽象,为什么这样说?这是因为抽象的东西不同于具体的东西,抽象的东西发生变化的频率要低,
让高层模块与底层模块去依赖于一个比较的稳定的东西比去依赖一个经常发生变化的东西的好处是显而易见的。表现在代码中,就是多使用
接口与抽象类,而少使用具体的实现类。(这样说可能有点不合适,如果一个具体类不经常发生变化,那完全可以让高层模块去依赖于它)。
面向对象的设计中,提倡“面向接口”的编程,在某种程度上,接口与一个抽象类是一样的。面向接口的编程其实就是
利用了抽象将高层模块(如一个类的调用者)与具体的被操作者(如一个具体类)隔离开来,从而使具体类在发生变化时不致于对调用者产生影响。
为了使底层模块的修改不影响高层模块,我们在设计时应该采用面向接口的编程方法,让高层与底层都去依赖接口(抽象),
但是接口是由谁来声明呢?接口应该是客户(即高层模块)来定义,而底层则去实现这些接口,这样,就象是客户提出了它需要的服务,
而底层则去实现这些服务,这样当底层实现逻辑发生改变化时,高层模块将不受响。但有些时候我们不是这样的,我们在定义接口时可能是
由底层去定义并公开接口的,这样做会有问题,因为当底层的接口改变时,高层同样会受到牵连。这就是“接口所有权”的倒置,
即由客户定义接口,而不是由“底层”定义接口。
五、DIP的关键
DIP是区别于过程化设计与面向对象设计的重要特性。DIP的关键其实在于找到系统中“变”与“不变”的部分,并利用接口将其隔离,
这不是一件容易的事件。因为在系统设计的初期,我们还难预料到系统中那个部分将来是经常会发生变化的部分,只有当事情发生了,
我们才有可能知道。这时,我们应该应用DIP来对系统做出个性,从使你的系统具有应对变化的弹性。
六、遵循DIP会带来的好处
首先,如果你的系统在设计时遵循了DIP原则,那么你的系统在重用时将会变化的容易。再者,由于遵循了DIP,你的系统在应对需求的
变化时就有了一定的弹性(更多需求的变化都表现在底层),同时,由于这种弹性,你的系统将更容易维护。
三层架构的学习参考了张洋(EricZhang)的系列随笔: [http://www.cnblogs.com/leoo2sk ]
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)