依赖之殇

简介:

没有对象协作的系统是不可想象的,因为此时的系统就是一个庞大的类,一个无所不知的“上帝类”。每个对象都有自己的自治领域,“各人自扫门前雪”,对象定义的法则就是这么自私。单一职责原则(SRP)[1]体现的正是这样的道理。对象的职责越少,则对象之间的依赖就越少。这一前提就是对象具有足够的高内聚与细粒度。这样的对象一方面有利于对象的重用,另一方面也保证了对象的稳定性。

对象的职责可以是自己承担,也可以委派给其他对象。因此,有对象就必然有依赖,正如有人就有江湖。那么,我们该如何降低对象之间的依赖?第一要则是依赖于抽象,如依赖倒置原则(DIP)[2]所云。如果无法依赖于抽象,则至少应该保证你所依赖的对象是足够稳定的。事实上,最稳定的对象就是抽象对象,所以万法归一,稳定才是降低依赖的基础。

依赖之殇的源头是“变化”。变化与稳定显然是矛盾的,软件设计的最大问题就是如何协调这两者之间的矛盾。我们需要像高明的杂技师,要学会掌握平衡,能够在钢丝绳上无碍的行走。那么,如何解决变化带来的影响呢?答案是利用封装来隔离变化。

封装的一种方式是抽象,因为相对于实现而言,接口总能保持一定的稳定性。例如税收策略。对于调用方而言,只是希望能够得到准确的税值,至于如何计算,则不是他关心的内容。抽象出计算税值的接口,就能够隔离调用方与可能变化的税收策略之间的依赖关系,如下图所示:

   

利用抽象还可以解除对特定实现环境例如外部资源、硬件或数据库的依赖。此时抽象隔离的变化可能是外部环境提供的API。例如,在考勤系统中,利用抽象隔离不同型号考勤机的变化。

利用抽象解除对象之间的依赖,还可以保证系统具有良好的可测试性。因为调用者依赖于抽象接口,就为我们引入Mock对象(当然也可以是Fake对象)执行单元测试提供了方便。尤其是当我们对领域对象进行测试时,如果领域对象需要对数据库操作,可以通过依赖抽象的持久对象(或仓储对象)实现职责的委派。此时,我们可以引入持久对象的Mock对象,模拟领域对象持久化的职责,既分离了领域对象与数据库资源的依赖关系,又能够提高单元测试的效率。

 public interface IOrderRepository 
{

    void Add(OrderInfo order);

    void Remove(OrderInfo order);

}

public class Order

{

    private IOrderRepository m_repository;

    public Order(IOrderRepository repository)

    {

        m_repository = repository;

    }

 

    public void Place(OrderInfo order)

    {

        if (order.Validate())

        {

            m_repository.Add(order);

        }

    }

}

 

利用封装隔离变化,并非必须依赖于抽象,根据不同的场景,降低要求,依赖于较为稳定的具体类对象也是可行的。这是一种降低复杂度的设计方式。例如,我们可以引入一个Helper类来封装第三方API的调用,从而实现调用方与第三方API的隔离。例如为SQL Server数据库操作定义一个Helper类: 
public static class SQLHelper 

    public int ExecuteNonQuery() {}  
    public DataSet ExecuteQuery() {} 
}

这样的设计类似于Gateway模式[3],利用一个Gateway对象来封装外部系统或资源访问。具体类对象显然不如抽象接口稳定,因此在设计时,我们需要遵循单一职责原则。这样的设计体现了DRY[4]原则,利用封装避免代码的重复,避免解决方案蔓延的坏味道[5]。合理的封装可以将变化点集中或限制到一处,以应对变化。一个常见的例子是利用简单工厂模式,将所有对象的创建集中在一个类中(当然也可以按模块创建不同的静态工厂)。即使创建的产品对象发生了变化,我们也可以只修改静态工厂类一处的实现。简单工厂模式常常可以应用在领域层中,通过工厂对象创建持久层对象(或所谓的数据访问对象)。

依赖源于对象的协作。传递依赖的方式可以通过属性,构造函数或方法的参数。若要保证对象间的松散耦合,构造函数或方法的参数以及属性的类型就应定义为抽象类型,如前面例子中的Order类。这是依赖解耦的关键方式,完全符合“面向接口设计”的编程思想,同时,它也有利于我们在后期实现“依赖注入”。

然而,产生依赖的方式绝不仅限于上述三种情形。例如,方法的返回值以及方法体中局部对象的创建,同样可能产生依赖。比较而言,这种依赖关系更难解除,因为它与具体的实现紧密相关。换句话说,因为这两种情形的依赖都涉及到具体对象的创建,且由实现者完成,而不能转交给调用方。例如,在如下的设计中,消息头会决定消息编码的方式。

 

MessageHeader的GetEncoder()方法需要返回一个IEncoder对象,这就要求在方法体中创建一个具体的IEncoder对象。要解除这样的依赖关系非常困难,如需彻底解除,一种可能是利用反射技术,通过具体类的类型来创建。还有一种可能是利用“惯例优于配置”实现解耦[6]。如果不需要彻底解除依赖,也可以利用“表驱动法”,或者直接将条件分支语句封装到方法中。

如果在方法实现中需要创建一个局部对象,我们可以考虑简单工厂模式或Registry模式[3]。例如,在Role对象的IsAuthorized()方法中,需要创建一个PriviledgeFinder对象,通过调用它的FindPriviledges()方法获得角色对应的权限集。此时,我们可以在Registry对象中提供PriviledgeFinder对象: 
interface IPriviledgeFinder 
{

    IList<Priviledge> GetPriviledges(int roleID);

}

public class PriviledgeFinder:IPriviledgeFinder

{}

public class Registry

{

    private Registry()

    { }

 

    private static Registry Instance = new Registry();

 

    protected virtual IPriviledgeFinder m_priviledge = new PriviledgeFinder();

 

    public static IPriviledgeFinder PriviledgeFinder()

    {

        return Instance.m_priviledge;

    }

}

public class Role

{

    public bool IsAuthorized()

    {

        IList<Priviledge> priviledges = 
            Registry.PriviledgeFinder().GetPriviledges(this.ID);

    }


 

上述实现实际上仍然利用了“将变化集中在一处”的设计原则。注意Registry类中的m_priviledge属性是virtual的受保护属性,它提供了一种变化的可能,可以交由子类去实现。

如何知道一个类是否过多的依赖其他类?一个办法就是创建这个类,并保证创建的对象能够正常使用。如果创建的过程非常复杂,就说明该类的依赖过多。此时,可以考虑分解该类的职责。如果这些依赖是必须的,则可以考虑利用封装,例如将对外部对象的调用修改为在内部创建(应用builder模式);也可以考虑使用Factory Method模式或者利用简单工厂。

依赖关系不仅仅只限于类与类之间,包(组件、模块、层)与包(组件、模块、层)之间同样存在依赖关系。良好的设计需要包之间保持松散耦合。大体上讲,包之间的依赖解除与类之间的依赖解除方式是一致的。即:要求一个包尽量依赖于一个稳定的包。注意,一个包依赖于另一个包,就代表着它依赖于这个包的每一个类。Robert C. Martin说:“我放入一个包中的所有类是不可分开的,仅仅依赖于其中一部分的情况是不可能的。”[7]因此,我们可以将一个包看做是一个类,它仍然要求职责的高内聚。在包中对类的分配,就相当于是对类进行一次分类。共同封闭原则[7]要求:“包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其他的包不造成任何影响。”简言之,我们在对包进行设计时,需要避免将不同的职责耦合在一个包中,它会造成变化点的扩散。

解除包之间依赖关系的一个重要方法仍然是抽象。使用Seperated Interface模式[3],在一个包中定义接口,而在另一个与这个包分离的包中实现这个接口。例如在分层架构模式中,我们常常对数据访问层进行抽象,使得业务逻辑层依赖于该抽象层,而不是它的低层模块。

上图的设计实际上是依赖倒置原则的体现。在项目开发中,这种将抽象与实现分别放在不同的包中,是系统设计中常见的方式。这样的设计也能够更好地应用在分布式开发场景中。

[1]单一职责原则(Single Responsibility Principle):就一个类而言,应该只专注于做一件事和仅有一个引起变化的原因; 
[2]依赖倒置原则(Dependency Inversion Principle):高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象; 
[3]Martin Fowler, Patterns of Enterprise Application Architecture; 
[4]DRY原则,即“不要重复你自己(Don't Repeat Yourself)”它要求“系统中的每项知识只应该在一个地方描述。” 
[5]Joshua Kerievsky, Refactoring to Patterns; 
[6]文章《解除具体依赖的技术》 
[7]Robert C. Martin Agile Software Development:Principles,Patterns and Practices








本文转自wayfarer51CTO博客,原文链接:http://blog.51cto.com/wayfarer/279900,如需转载请自行联系原作者

相关文章
|
2月前
|
机器学习/深度学习 算法 测试技术
「软件项目管理」一文详解软件项目成本计划
该文章详细解释了软件项目成本估算的过程与方法,涵盖了代码行估算法、功能点估算法、用例点估算法、类比估算法、自下而上估算法、参数模型估算法及专家估算法等多种技术,并探讨了成本预算的制定步骤。
「软件项目管理」一文详解软件项目成本计划
|
2月前
|
数据挖掘 项目管理 调度
「软件项目管理」一文详解软件项目质量计划
该文章全面介绍了软件项目质量计划的制定方法,涵盖了质量模型、质量管理过程、质量保证与控制技术,并提出了软件质量改善的具体建议,帮助项目管理人员有效地提升软件产品的质量水平。
「软件项目管理」一文详解软件项目质量计划
|
2月前
|
存储 项目管理
「软件项目管理」一文详解软件配置管理计划
该文章详细阐述了软件配置管理计划的各个方面,包括配置管理的基本概念、配置项的标识、变更控制流程以及配置审计的方法,并提供了实施配置管理的最佳实践。
「软件项目管理」一文详解软件配置管理计划
|
8天前
|
缓存 前端开发 JavaScript
前端架构思考:代码复用带来的隐形耦合,可能让大模型造轮子是更好的选择-从 CDN 依赖包被删导致个站打不开到数年前因11 行代码导致上千项目崩溃谈谈npm黑洞 - 统计下你的项目有多少个依赖吧!
最近,我的个人网站因免费CDN上的Vue.js包路径变更导致无法访问,引发了我对前端依赖管理的深刻反思。文章探讨了NPM依赖陷阱、开源库所有权与维护压力、NPM生态问题,并提出减少不必要的依赖、重视模块设计等建议,以提升前端项目的稳定性和可控性。通过“left_pad”事件及个人经历,强调了依赖管理的重要性和让大模型代替人造轮子的潜在收益
|
2月前
|
缓存 测试技术 应用服务中间件
聊聊传统项目与互联网项目
聊聊传统项目与互联网项目
|
前端开发 JavaScript Linux
说几个能帮助你高效工作的插件
说几个能帮助你高效工作的插件
153 0
|
前端开发 JavaScript 小程序
新来个技术总监,给公司项目引入了全新的业务架构,堪称最佳实践!
新来个技术总监,给公司项目引入了全新的业务架构,堪称最佳实践!
|
移动开发 小程序 JavaScript
讲真,这几个完整的开源Java项目能让你的能力提高一大截
前言 今天有一个读者问了,一个很神奇的问题:
178 0
|
消息中间件 Kubernetes JavaScript
讲真,这几个完整的开源Java项目能让你的能力提高一大截(下)
前言 今天有一个读者问了,一个很神奇的问题:
238 0
|
运维 Ubuntu Linux
干货 | 应用打包还是测试团队老大难问题?
干货 | 应用打包还是测试团队老大难问题?