![]() |
|
![]() |
||||||||||||||||||
讨论了半天,终于是时候对依赖和耦合进行一点儿总结了,也是该进行一点目标诉求了。在软件设计领域,有那么几个至高原则值得每个开发者铭记于心,它们是: · 面向抽象编程 · 低耦合,高内聚 · 封装变化 · 实现重用:代码重用、算法重用 对了,就是这些平凡的字眼,汇集了面向对象思想的核心内容,也是本文力求阐释的禅意心经。关于面向抽象编程和封装变化,会在后面详细阐释,在此我们需要将注意力关注于“低耦合,高内聚”这一目标。 低耦合,代表了实现最简单的依赖关系,尽可能地减少类与类、模块与模块、层次与层次、系统与系统之间的联系。低耦合,体现了人类追求简单操作的理想状态,按照软件开发的基本实现技巧来追求软件实体之间的关系简单化,正是大部分设计模式力图追求的目标;低耦合,降低了一个类或一个模块发生修改对其他类或模块造成的影响,将影响范围简单化。在本文阐释的依赖关系方式中,实现单向的依赖,实现抽象的耦合,都是实现低耦合的基础条件。 高内聚,一方面代表了职责的统一管理,一方面体现了关系的有效隔离。例如单一职责原则其实归根结底是对功能性的一种指导性体现,将功能紧密联系的职责封装为一个类(或模块),而判断的准则正是基于引起类变化的原因。所以,封装离不开依赖,而抽象离不开变化,二者的概念和本质都是相对而言的。因此,高内聚的目标体现了以隔离为目标进行统一管理的思想。 那么,为了达到低耦合、高内聚的目标,通常意义上的设计原则和设计模式其实都是朝着这个方向实现的,因此仅仅总结并非普遍意义的规则: · 尽可能实现单项依赖。 · 不需要进行数据交换的双方,不要实现多此一举的关联,人们将此形象称为“不要向陌生人说话(Don't talk to strangers)”。 · 保持内部的封装性,关联的双方不要深入实现细节进行通信,这是保证高内聚的必需条件。 2.关于抽象和具体什么是抽象呢?首先不必澄清什么是抽象,而从什么算抽象说起,稳定的、高层的就代表了抽象。就像一个公司,最好保证了高层的稳定,才能保证全局的发展。在进行系统设计时,稳定的抽象接口和高层逻辑,也代表了整个系统的稳定与柔性。兵熊熊一窝,将良良一窝,软件的构建也正如打仗,良好的设计都是自上而下的。而对具体的编程实践而言,接口和抽象类则代表了语言层次的抽象。 追溯概念的分析,一一过招,首先来看依赖于具体,如图3-3所示。 因此,为了分离这种紧耦合,最好的办法就是隔离,引入中间层来分离变化,同时确保中间层本身的稳定性,因此抽象的中间层是最佳的选择(如图3-4所示)。 图3-3 依赖的关系 图3-4 依赖的关系(引入抽象层) 以例而理,从最常见的服务端逻辑举例,如下所示: public interface IUserService { } public class UserService : IUserService { } 如果依赖于具体: public class UserManager { private UserService service = null; } 或者依赖于抽象: public class UserManager { private IUserService service = null; } 二者的区别仅在于引入了接口IUserService,从而使得UserManager对于UserService的依赖由强减弱。然而对于依赖的方式并非仅此一种,设计模式中的智慧正是通过各种编程技巧进行依赖关系的解耦,值得关注和学习,后文将对设计模式进行概要性的讨论。 对WCF熟悉的读者一定不难看出这种实现方式如此类似于WCF的推荐模式,也是契约编程的基本思想。关于WCF及SOA的相关内容,我们在后文进行了相关的讨论。 总结一番,什么是抽象,什么是具体?在作者看来,抽象就是系统中对变化封装的战略逻辑,体现了系统的必然性和稳定性,能够被具体层次复用和覆写;而具体则包含了与具体实现相关的逻辑,体现了系统的动态性和变动性。因此,抽象是稳定的,而具体是变动的。 Bob大叔在《Agile Principles, Patterns, and Practices》一书中直言,程序中所有的依赖关系都应终止于抽象类或者接口,就是对面向抽象编程一针见血的回应,其原因归根到底源自于对抽象和具体的认知和分解:关联应该终止于抽象,而不是具体,保证了系统依赖关系的稳定。具体类发生的修改,不会影响其他模块或者关系。那么如何做到这种理想的依赖于抽象的设计呢? · 层次清晰化 将复杂的问题简单化,是人类思维的普世智慧,也自然而然是实现软件设计的基本思路。将复杂的业务需求通过建模过程的抽象化提炼,去粗取精,去伪存真,凡此种种。而抽象的过程,其目标之一就是形成对于复杂问题简单化的处理过程,只有形成层次简单的逻辑才能将复杂需求中的关系梳理清晰,而依赖的本质正如上文所言,不就是处理关系吗? 所以,清晰的层次划分,进而形成的模块化,是实现系统抽象的必经之路。 · 分散集中化 由需求而设计的过程,就是一个分散集中化的过程,把需求相关的业务通过开发流程的需求分析过程进行整理,逐步形成需求规格说明、概要设计和详细设计等基本流程。分散集中化,是一个梳理需求到形成设计的过程,因此对于把握系统中的抽象和具体而言,是一个重要的分析过程和手段。现代软件工程已经对此形成了科学的标准化流程处理逻辑,例如可以借助UML更加清晰地设计流程、分析设计要素,进行标准化沟通和交流。 · 具体抽象化 将具体问题抽象化,是本节关注的要点,而处理的方法是什么呢?答案就在设计模式。设计模式是前辈智慧的总结和实践,所以熟悉和学习设计模式,是学习和实践设计问题的必经之路。然而,没有哪个问题是由设计模式全权解决,也没有哪个模式能够适应所有的问题,因此要努力的是尽量积累更多的模式来应对多变的需求。作为软件设计话题中最重量级的话题,关注模式和实践模式是成长的记录。 · 封装变化点 总的来说,抽象和变化就像一对孪生兄弟,将具体的变化点隔离出来以抽象的方式进行封装,在变化的地方寻找抽象是面对抽象最理想的方式。所以,如何去寻找变化是设计要解决的首要问题,例如工厂模式的目标是封装对象创建的变化,桥接模式关注对象间的依赖关系变化等。23个经典的设计模式,从某种角度来看,正是对不同变化点的封装角度提出的不同解决方案。 这一设计原则还被称为SoC(Separation of Concerns)原则,定义了对于实现理想的高耦合、低内聚目标的统一规则。 3.设计的哲学之所以花如此篇幅来讲述一个看似简单的问题,其实最终理想是回归到软件设计目标这个命题上。如果悉心钻研就可发现,设计的最后就是对关系的处理,正如同生活的意义在于对社会的适应一样。因此,回归到设计的目标上就自然可知,完美的设计过程就是对关系的处理过程,也就是对依赖的梳理过程,并最终形成一种合理的耦合结果。 所以,面向对象并不神秘,以生活的现实眼光来看更是如此。把面向对象深度浓缩起来,可以概括为: · 目标:重用、扩展、兼容。 · 核心:低耦合、高内聚。 · 手段:封装变化。 · 思想:面向接口编程、面向抽象编程、面向服务编程。 其实,就是这么简单。在这种意义上来说,面向对象思想是现代软件架构设计的基础。下面以三层架构的设计为例,来进一步感受这种依赖哲学在具体软件系统中的应用。关于依赖的抽象和对变化隔离的基本思路,其实也是实现典型三层架构或者多层架构的重要基础。只有使各个层次之间依赖于较稳定的接口,才能使得各个层次之间的变化被隔离在本层之内,不会造成对其他层次的影响,这完全符合开放封闭原则追求的优良设计理念。将这种思路表达为设计,可以表示为如图3-5所示的形式。 图3-5 多层架构的依赖 由图3-5可知,IDataProvider作为隔离业务层和数据层的抽象,IService作为隔离业务层和表现层的抽象,保证了各个层次的相对稳定和封装。而体现在此的设计逻辑,就正是对于抽象和耦合基本目标概念的体现,例如作为重用的单元,抽象隔离保证了对外发布接口的单一和稳定,所以达到了最高限度的重用;通过引入中间的稳定接口,达到了不同层次的有效隔离,层与层之间体现为轻度耦合,业务层只持有IDataProvider就可以获取数据层的所有服务,而表现层也同样如此;最后,这种方式显然也直接实践了面向接口编程,面向抽象编程的经典理念。 同样的道理,对于架构设计的很多概念,放大可以扩展为面向服务设计所借鉴,放小这正是反复降调的依赖倒置原则在类设计中的基本思想。因此,牢记一位软件大牛的说法:软件设计的任何问题,都可以通过引入中间逻辑得到解决。而这个中间逻辑,很多时候被封装为抽象,是最为合理和智慧的解决方案。 让我们再次高颂《老子》的小国寡民论,来回味关于依赖哲学中,如何实现更好的和谐统一以及如何遵守科学的软件管理思想:邻国相望,鸡犬之声相闻,民至老死,不相往来。
|
类 别 | 依 赖 | 模 式 |
创建型模式 | 创建型模式的核心关注点就在于对象创建的依赖关系上,将对象的依赖从new操作中解脱出来,隔离应用系统和类型实例化间的依赖。例如通过引入工厂,将对象创建职责委托于工厂类,解除了应用系统与类型对象在实例化过程中的直接引用。 详细的讨论,参考 | 工厂方法、抽象工厂、单例、创建者、原型模式 |
续表
类 别 | 依 赖 | 模 式 |
结构型模式 | 结构型模式,是将简单类型组合为复杂类型的过程,通过灵活的设计要素,最终保证不同类型间保持尽量间接的引用和尽量松散的耦合,在复杂类型有更多变化与诉求时,以最小的代价兼容变化,扩展诉求。 例如,适配器模式的两种不同适配方式,分别代表了通过继承和组合方式实现对象适配的处理;而代理模式,则通过引入代理,来隔离不同层次间的依赖,同时保证真实对象的安全性与封装性 | 桥接、适配器、组合、外观、装饰、享元、代理 |
行为型模式 | 行为型模式,关注对象行为的扩展和对象间数据关系的通信,以面向对象方式描述控制流。 例如,职责链避免请求的发送者和接收者直接的直接耦合,而是将多个对象连成一条处理链条;而命令模式的核心则在于将行为的请求者和行为实现者之间通过封装的命令对象解耦 | 模板方法、迭代器、中介者、职责链、解释器、命令、观察者、备忘录、状态、策略、访问者 |
把握软件开发的历史脉搏,依赖关系的落脚点也在其演义过程中逐渐发生着改变,从面向过程以函数为核心,到面向对象以对象为核心,面向组件以组件为核心,再到面向服务中以服务为核心。基于契约编程的思想实现了更松散的耦合模型,当开发者调用Facebook服务获取好友列表这样的服务时,并不需要关心具体的服务内部逻辑,也不需要关注服务的物理存储,更不需要关心服务之间的关联关系,而只需要关注服务本身即可,如图3-14所示。
图3-14 高层依赖于抽象
基于契约编程的依赖,就是对契约本身的依赖,也就是对具体服务的依赖。因此,SOA架构下的依赖是天生松散耦合、高度抽象、实现技术无关且物理无关的。
· 松散耦合,服务之间的关系、应用与服务的关系都是松散的、单一的,基于消息的低耦合依赖。
· 高度抽象,服务本身不仅抽象了逻辑,还同时抽象了通信的契约与寻址。高度抽象的服务实体,是实现SOA架构的基础单元。
· 实现技术无关,服务本身的实现可以是各种各样的技术平台、版本。
· 物理无关,应用访问的服务部署,在物理上和应用本身是可以完全分离的,可以是局域网或者是互联网上任何位置,可以是不同的服务提供商,也可以运行在不同的时区。
· 基于消息,客户端与服务端基于标准的消息协议进行数据通信。
关于依赖的哲学,最典型的违反莫过于对象创建的依赖。自面向对象的大旗树立以来,对于对象创建话题的讨论就从未停止。不管是工厂模式还是依赖注入,其核心的思想就只有一个:如何更好地解耦对象创建的依赖关系。所以,在这一部分,我们就以对象创建为主线,来认识对于依赖关系的设计轨迹,分别论述一般的对象创建、工厂方式创建和依赖注入创建三种方式的实现、特点和区别。
一般而言,以new关键字进行对象创建,在.NET世界里是天经地义的事情。在本书7.1节“把new说透”中,就比较透彻地分析了new在对象创建时的作用和底层机制。对.NET程序员而言,以new进行对象创建已经是习以为常的事情,大部分情况下这种方式并没有任何问题。例如:
public abstract class Animal
{
public abstract void Show();
}
public class Dog : Animal
{
public override void Show()
{
Console.WriteLine("This is dog.");
}
}
public class Cat : Animal
{
public override void Show()
{
Console.WriteLine("This is cat.");
}
}
public class NormalCreation
{
public static void Main2()
{
Animal animal = new Dog();
}
}
对animal对象而言,大部分情况下具体的Dog类是相对稳定的,所以这种依赖很多时候是无害的。这也是我们习以为常的原因之一。
然而,诚如在本文开始对抽象和具体的概念进行分析的结论一样,依赖于具体很多时候并不能有效地保证其稳定性的状态。以本例而言,如果有新的Bird、Horse加入到动物园中来,管理员基于现有体系的管理势必不能适应形式,因为所有创建而来的实例都是依赖于Dog的。所以,普遍的对象创建方式,实际上是对DIP原则的典型违反,高层Animal的创建依赖于低层的Dog,和普世的DIP基本原则是违背的。
因此,DIP并不是时时被OO所遵守,开发者要做的只是适度的把握。为了解决new方式创建对象的依赖违反问题,典型的解决思路是将创建的依赖由具体转移为抽象,通常情况下有两种方式来应对:工厂模式和依赖注入。
以工厂模式进行对象创建的方法,主要包括两种模式:抽象工厂模式和工厂方法模式,本文不想就二者的区别和意义展开细节讨论,如果有兴趣可以参阅GoF的《设计模式:可复用面向对象软件的基础》一书。
本文将视角拉回到WCF的IChannelFactory和各种Channel的创建上,以此借用WCF架构中Channel Layer的设计思路,应用工厂模式进行对象创建的设计和扩展,来了解应用工厂模式进行对象创建依赖关系解除的实质和实现。
对于WCF中Channel的概念可以参考相关的资料,在此你只需将其看成一个简单类型即可。
首先来了解一下Channel的创建过程:
public class FactoryCreation
{
public static void
{
EndpointAddress ea = new EndpointAddress("http://api.anytao.com /UserService");
BasicHttpBinding binding = new BasicHttpBinding();
IChannelFactory<IRequestChannel> facotry = binding.BuildChannelFactory<IRequestChannel>();
facotry.Open();
IRequestChannel channel = facotry.CreateChannel(ea);
channel.Open();
//Do something continue...
}
}
在示例中,IRequestChannel实例通过IChannelFactory工厂来创建,因此关注工厂方式的创建焦点就着眼于IChannelFactory和IRequestChannel上。实质上,在WCF channel Layer中,Channel Factory是创建和管理Channel的工厂封装,通过一个个的Channel Factory来创建一个个对应的Channel实例,所有的Channel Factory必须继承自IChannelFactory,其定义为:
public interface IChannelFactory<TChannel> : IChannelFactory, ICommunicationObject
{
TChannel CreateChannel(EndpointAddress to);
TChannel CreateChannel(EndpointAddress to, Uri via);
}
通过类型参数TChannel来注册创建实例的类型信息,进而根据EndpointAddress信息来创建相应的对象实例。当然,WCF中的工厂模式应用,还有很多内容值得斟酌和学习。现有篇幅不可能实现完全类似的设计结构,借鉴于WCF的设计思路,对Animal实例的创建进行一点改造,实现基于泛型的工厂模式创建设计,首先定义一个对象创建的模板:
public interface IAnimalFacotry<TAnimal>
{
TAnimal Create();
}
然后实现该模板的泛型工厂方法:
public class AnimalFacotry<TAnimalBase, TAnimal> : IAnimalFacotry<TAnimalBase> where TAnimal : TAnimalBase, new()
{
public TAnimalBase Create()
{
return new TAnimal();
}
}
其中类型参数TAnimalBase代表了高层类型,而TAnimal则代表了底层类型,其约定关系在where约束中有明确的定义,然后是一个基于对工厂方法的封装:
public class FacotryBuilder
{
public static IAnimalFacotry<Animal> Build(string type)
{
if (type == "Dog")
{
return new AnimalFacotry<Animal, Dog>();
}
else if (type == "Cat")
{
return new AnimalFacotry<Animal, Cat>();
}
return null;
}
}
最后,可以欣赏一下基于工厂方式的对象创建实现:
class Program
{
static void Main(string[] args)
{
IAnimalFacotry<Animal> factory = FacotryBuilder.Build("Cat");
Animal dog = factory.Create();
dog.Show();
}
}
你看,对象创建的依赖关系已经由new式的具体依赖转换为对于抽象和高层的依赖。在本例中,完全可以通过反射方式来消除if/else的运行时类型判定,从而彻底将这种依赖解除为可配置的灵活定制。这正是抽象工厂方式的伟大意义,其实在本例中完全可以将IAnimalFacotry扩展为IAnyFactory形式的灵活工厂,可以在类型参数中注册任何类型的TXXXBase和TXXX,从而实现功能更加强大的对象生成器,只不过需要更多的代码和扩展,读者可以就此进行自己的思考。
领略了工厂模式的强大威力,下面继续介绍更加灵活解耦的依赖注入方式,继续回到对于Animal实例化的依赖倒置环节,来看看注入方式下如何通过容器来实现实例创建过程。在此选择Unity基础容器来实现,引入Microsoft.Practices.Unity.dll程序集和Microsoft.Practices.Unity命名空间,然后就可以很容易地通过Unity容器来完成对象创建依赖关系的隔离:
class UnityCreation
{
public static void Main()
{
IUnityContainer container = new UnityContainer();
container.RegisterType<Animal, Dog>();
Animal dog = container.Resolve<Animal>();
dog.Show();
}
}
Unity提供了强大而灵活的依赖注入支持:方法调用注入、属性注入和构造器注入等多种方式,可以在运行时或通过配置方式来注册和获取类型,是实现处理对象间依赖的有效方式。关于依赖注入,前文已有较多笔墨铺陈,在此就不做过多讨论。
综上而言,以对象创建这样一个常见而又简单的话题为焦点来讨论对这种依赖关系的场景复现。实际上,对象间的关系就像人类社会一样复杂多变,随时准备应对变化的依赖,着眼于对抽象的把握,是把复杂简单化的最佳实践,就类似于以工厂模式或者依赖注入方式将实体对象创建简单化处理的过程一样,是有意义的。
关于依赖的哲学,值得总结的有很多:
· 以new创建对象,是对依赖倒置原则的典型违反,可以通过工厂模式或者依赖注入来解决。
· 一个对象持有另外一个具体对象的引用可能破坏了依赖倒置。
· 所有结构良好的面向对象架构都具有清晰的层次定义,每个层次通过一个定义良好的受控接口向外提供一组内聚的服务。
· 依赖倒置预示着程序中的依赖关系不应是具体的类型,而应是抽象类和接口。
· 依赖倒置适用于当一个类向另一个类发送消息的任何情况。
从小国寡民到和谐社会,不是一段寻常的路,走得越远想得越多,才能挥洒自如。正如对依赖的体会,一点一滴积累下来,剥丝抽茧才能层层深入。本文的循序之旅不可能尽述本质,如果能做到一点点亮思想的火柴就已是功得圆满。对于设计的精妙,体会抽象的层次,升华依赖的哲学,就是本文所得。
![]() |
![]() |
![]() |
|||