实用模式--聚合和耦合

实用模式
聚合和耦合
很多软件设计一直都存在一个问题:这段代码应放置在哪里?我一直在寻找编排代码的最佳方法,以便能够更轻松地编写、理解代码,并在以后更方便地进行更改。如果我的代码构造很漂亮,我将可名扬四海,无限荣光。如果构造得很糟糕,那些追随我的开发人员会一直对我埋怨不停。
我特别想在我的代码结构方面实现三个具体目标:
  1. 让代码中需要一起更改的部分尽可能靠近在一起。
  2. 允许代码中不相关的部分独立更改(也称为“正交性”)。
  3. 最大程度减少代码中的重复部分。
要实现这三个目标,我需要一些工具来协助我了解应将新代码放置到什么位置,并借助其他一些工具帮助我识别出是否将代码放置到了错误位置。
大体上说,这些目标都与聚合和耦合这两个典型代码质量紧密相关。我通过达到更高程度的聚合和更松散的耦合实现这些目标。当然,我们首先需要了解这些质量的含义以及为何聚合和耦合是有用的概念。然后我想讨论一些我将称之为“设计矢量”的内容,它们可以帮助我们实现更佳的结构,并帮助我们认识到何时需要丢弃那些已经不知不觉在我们代码中形成的错误结构。
补充一点,我是名为 StructureMap 的开源控制反转 (IOC) 工具的主要开发人员。我将使用源自 StructureMap 的一些现实示例来说明这些矢量所针对的设计问题。换言之,千万不要再犯我已犯过的错误。
降低耦合
在软件设计的几乎所有讨论场合,你都会随时听到“松散耦合”或“紧密耦合”这样的术语。类或子系统之间的耦合是这些类或子系统之间互联程度的量度标准。紧密耦合表示相关的类必须了解彼此的内部细节、更改将波及整个系统,并且系统可能更难以了解。 图 1 显示了一个精心设计的业务处理模块示例,该模块与除业务逻辑之外的其他各种关注问题紧密耦合。
假设我们真正关心的是实际的业务处理,但我们的业务逻辑代码是与数据访问方面的焦点以及配置设置结合在一起的。那么,这种代码有可能出现什么错误呢?
第一个问题是该代码会因侧重点失偏而有些难以理解。我将在有关聚合的下一节中深入讨论这一点。
第二个问题是数据访问策略、数据库结构或配置策略中的任何更改同样也会波及整个业务逻辑代码,因为它们全部都属于同一个代码文件。这种业务逻辑对底层基础结构过于了解。
第三,我们不能独立于特定的数据库结构或在没有 AppSettings 键的情况下重用该业务逻辑代码。我们也不能重用在 BusinessLogicClass 中内嵌的数据访问功能。数据访问与业务逻辑之间的耦合可能不是问题,但如果我们希望改变此业务逻辑的用途,以对照由分析人员直接输入到 Excel 电子表格中的数据来使用它会怎样呢?如果我们要单独测试或调试该业务逻辑又会怎样呢?我们无法实现上述的任何操作,因为该业务逻辑与数据访问代码是紧密耦合的。如果我们能将业务逻辑从其他关注问题中隔离出来,则对它进行更改就会变得轻松得多。
总之,在类与模块之间实现松散耦合的实际目标是为了:
  1. 使代码更易于阅读。
  2. 将类的麻烦的内部运行隐藏在设计完善的 API 之后,从而使其他开发人员可以更轻松地使用这些类。
  3. 隔离对较小区域代码的可能更改。
  4. 在全新的上下文中重用类。
代码气味
在设计新代码时知道如何做该做的事情显然很好,但能够认识到您的现有代码或设计已出现问题则更为重要。 与不适当的亲密一样,“代码气味”(由 Martin Fowler 在著作《重构:改进既有代码的设计》中定义)是可用于发现代码中潜在问题的工具。
代码气味是表示代码中可能出错的一种信号。这并不意味着您需要立即移除您的现有代码并当场将其丢弃,但您确实需要仔细研究一下散发出恼人“气味”的代码。通过简单的检查即可发现代码气味。了解代码气味的作用很大,可帮助您在代码和类设计中的小问题演变为大问题之前就将其解决掉。
通常所描述的许多(如果不是极数)代码气味是拙劣的聚合或有负面影响的紧密耦合的标志。下面提供了一些其他示例:
发散式变化需要针对不同原因以不同方式进行更改的单个类。这种气味是表示该类没有聚合性的标志。您可以重构此类以将清楚的职责提取到新类中。
特性依恋ClassA 中的某方法似乎对 ClassB 的工作和数据字段过于感兴趣。从 ClassA 到 ClassB 的特性依恋是从 ClassA 到 ClassB 紧密耦合的体现。通常的解决办法是尝试将 ClassA 中感兴趣的方法的功能移至 ClassB,该功能已经与任务中涉及的大部分数据比较靠近。
霰弹式修改系统中某个特定类型的更改重复引起对一组类的多次小的更改。霰弹式修改通常意味着某单个逻辑概念或函数在多个类之间展开。可通过将代码中需要一起更改的所有部分集中到单个聚合类中来解决该问题。
提高聚合
聚合的理论定义是类的所有职责、数据和方法彼此关联紧密程度的度量标准。我更倾向于将聚合视为是判断某个类在系统内是否有明确定义的角色的度量标准。我们通常认为高度聚合是件好事,并且将“高度聚合”视为魔咒一样反复叨念。但其中的原因是什么?
让我们将编码看作是与计算机进行的一次对话。更准确地讲,我们在与计算机同时进行多个对话。我们的对话内容是有关如何实现安全性、基础结构方面的问题应如何表现以及业务规则是什么。
如果您处于一个正同时进行多个不同对话的喧闹聚会中,则很难将注意力集中在某一个您正设法进行的对话。在一个只进行一个对话的安静环境中进行对话就会容易得多。
对聚合的一个简单测试就是观察某个类并判断该类的所有内容是否与类的名称直接相关并由该名称描述,含糊的类名称(例如 InvoiceManager)不算在内。如果该类的职责不与其名称相关,则这些职责有可能属于其他类。如果您发现一些方法和字段的子集可轻松地在另一个类名称下单独分组,则您可能应将这些方法和字段提取到一个新类中。
举例说明,如果您在 TrainStation、Train 和 Conductor 类中发现一些方法和数据看起来与 TrainSchedule 类的主题最精确匹配,可将这些方法和数据移至 TrainSchedule 中。我最喜爱的有关设计的一句熟语在此处比较适用:将代码放置在您认为应该能找到它的位置。对我而言,将涉及列车时刻表的功能放置在 TrainSchedule 类中是最符合逻辑的。
延续我之前的对话比喻,由聚合类和子系统组成的系统就像是一个设计合理的在线讨论组。在线组中的每个区域都仅仅集中在一个特定主题,这样比较容易跟踪讨论内容,如果您正在寻找有关某特定主题的对话,则只能访问一个房间。
消除不适当的亲密
不适当的亲密指的是某个类中某个方法对另一个类过于熟知。不适当的亲密是两个类之间存在有负面影响的紧密耦合的迹象。假设我们有一个业务逻辑类,该类调用类 DataServer1 的实例以获取其进行业务逻辑处理所需的数据。 图 2 显示了一个示例。在这种情况下,Process 方法需要了解 DataServer1 的大量内部工作,并对 SqlDataReader 类略知一二。
现在让我们重新编写 图 2 中的代码以消除不适当的亲密:
public void Process() {
  DataServer2 server = new DataServer2();
  foreach (DataItem item in server.GetWorkItemData(5)) {
    processItem(item);
  }
}
正如您在此版本代码中看到的那样,我已将所有 SqlConnection 和 SqlDataReader 对象操作封装在 DataServer2 类内。DataServer2 也被假定为负责其自身的配置,因此新的 Process 方法无需了解任何有关设置 DataServer2 的内容。从 GetWorkItemData 返回的 DataItem 对象也是强类型化的对象。
现在,我们对照松散耦合的某些目标分析一下两个版本的 Process 方法。首先,在使代码易于阅读的方面效果如何?第一个版本和第二个版本的 Process 都执行了相同的基本任务,但哪一个更易于阅读和理解呢?就个人而言,我无需费力地理解数据访问代码就可以更轻松地阅读和理解业务逻辑处理。
在使类易于使用的方面效果如何?DataServer1 的使用者需要了解如何创建 SqlConnection 对象、了解返回的 DataReader 的结构,以及迭代并整理 DataReader。DataServer2 的使用者只需调用无参数的构造函数,然后调用可返回一系列强类型化的对象的单个方法即可。DataServer2 将负责自身的 ADO.NET 连接设置并整理打开的 DataReaders。
在隔离对较小区域代码的可能更改的方面效果如何?在第一个版本的代码中,对 DataServer 工作方式的几乎所有更改都会影响 Process 方法。在封装性更好的第二个版本的 DataServer 中,您可将数据存储切换到 Oracle 数据库或 XML 文件,而不会对 Process 方法产生任何影响。
Demeter 定律
Demeter 定律是一种设计经验法则。该定律的简要定义为:仅与您的直接伙伴交谈。Demeter 定律是有关代码潜在威胁的警告,如 图 3 所示。
ClassThatNeedsInsuranceClaim 类需要获取 InsuranceClaim 数据。它有一个对 Repository 类的引用,Repository 类本身包含一个 DataService 对象。ClassThatNeedsInsuranceClaim 到达 Repository 内部以获取内部 DataService 对象,然后调用 Repository.FindClaims 获得其数据。请注意,对 _repository.InnerService.FindClaims(customer) 的调用明显违背了 Demeter 定律,因为 ClassThatNeedsInsuranceClaim 将直接调用其 Repository 字段的属性的方法。现在,请将您注意力转向 图 4,该图显示了同一代码的另一个示例,但这次它遵循了 Demeter 定律。
我们实现了什么?Repository2 比 Repository 更易于使用,因为您有一个直接的方法来调用 InsuranceClaim 信息。在我们违反 Demeter 定律时,Repository 的使用者与 Repository 的实现紧密耦合。在修订过的代码中,我可以更好地更改 Repository 实现以添加更多高速缓存或以完全不同的对象换出基础 DataService。
Demeter 定律是一个功能强大的工具,可帮助您发现潜在的耦合问题,但不可盲目地遵循 Demeter 定律。违背 Demeter 定律的确会使您的系统实现更紧密的耦合,但有时候您可能会认为耦合到代码的某个稳定元素的潜在成本要比编写大量委托代码来避免违背 Demeter 定律的成本低。
只是告知,不要询问
“只是告知,不要询问”设计原则主张您告知对象将要执行什么任务。您不想做的事情是询问某对象其内部状态、对该状态做出决策,然后告知该对象将要执行什么任务。遵守“只是告知,不要询问”的对象交互风格是确保正确安置职责的有效途径。
图 5 说明了违背“只是告知,不要询问”原则的情况。该代码的任务是购买某种商品、确认 $10,000 以上的购买金额是否可能有折扣,最后检查帐户数据来判断是否有充足的资金。先前的 DumbPurchase 和 DumbAccount 类都无此功能。帐户和购买业务规则都是在 ClassThatUsesDumbEntities 中编码的。
这种类型的代码在几个方面可能存在问题。在与此类似的系统中,可能会出现重复,因为某个实体的业务规则分散在这些实体之外的程序代码中。您可能会不知不觉地重复逻辑,因为先前编写的业务逻辑所在位置并不明显。
图 6 显示了同一代码,但这次遵循了“只是告知,不要询问”的模式。在此代码中,我将用于购买和帐户的业务规则移动到它们自己的 Purchase 和 Account 类中。当我们打算进行购买时,只需告诉 Account 类从自身扣除购买金额即可。Account 和 Purchase 了解它们自身及其内部规则。Account 的使用者只需要知道去调用 Account.Deduct(Purchase,PurchaseMessenger) 方法就可以了。
Account 和 Purchase 对象比较易于使用,因为您无需对这些类有太多了解即可执行我们的业务逻辑。我们也潜在地减少了系统中的重复。我们不费吹灰之力就可以在整个系统中重用 Accounts 和 Purchases 的业务规则,因为这些规则位于 Account 和 Purchase 类的内部,而不是隐藏在使用这些类的代码内。另外,更改 Purchases 和 Accounts 的业务规则将更加轻松,因为这些规则只能在系统中的一个位置处找到。
与“只是告知,不要询问”紧密关联的是“信息专家”模式。如果您对您的系统有新的职责,那么新职责应属于哪个类呢?“信息专家”模式会问道,谁了解履行该职责所必需的信息?换言之,任何新职责的第一候选项都是已具有受该职责影响的数据字段的类。在购买示例中,Purchase 类知道您用于确定可能的折扣率的某个购买项的信息,因此 Purchase 类自身就是计算折扣率的直接候选项。
只说一次
作为一个行业,我们已了解到刻意编写可重用代码的成本是非常昂贵的,但我们仍因其明显的优点而设法实现重用。这对我们来说可能有必要在系统中查找重复项并找到消除或集中该重复项的方法。
在系统中提高聚合的最有效方法之一就是只要发现重复项就将其消除。如果您认为自己并不完全了解系统今后将要如何变化,但您可以通过在类结构中保持良好的聚合和耦合来改进代码接受更改的能力,这可能就是最佳的方法。
多年前我曾参与过一个大型装运应用程序的辅助设计工作,该应用程序用于管理某工厂车间的货箱流。在其最初的成形阶段,该系统轮询一个消息队列以获取外来消息,然后通过应用一大组业务规则以确定货箱的下一站目的地来响应这些消息。
次年,该业务需要从桌面客户端启动货箱路线逻辑。令人遗憾的是,业务逻辑代码与用于读取和写入 MQ Series 队列的机制间的耦合过于紧密。根据判断,将原始业务逻辑代码从 MQ Series 基础结构中解脱出来风险极大,因此在新的桌面客户端的并行库中重复了整个业务规则主体。该决定使得新的桌面客户端变得切实可行,但也使今后的所有工作更加困难,因为对货箱路线逻辑的每个更改都需要对两个截然不同的库进行并行更改,而这些种类的业务规则经常发生更改。
我们从这个实际案例中得到了几个教训。代码中的重复对于构建系统的组织会产生实际成本,而该重复很大程度上是由于类结构中耦合和聚合质量不佳导致的。这种情况会直接影响公司的盈亏状况。
找到在代码中检查重复的一种途径,就是增加一个在以后改进设计的机会。如果您发现两个或多个类有某些功能重复,您可以判定重复的功能一定是完全不同的职责。改进代码库的聚合质量的最佳方法之一是,将重复项提取到可在整个代码库中共享的单独类中。
但我得到的痛苦经验是,即使看似无负面影响的重复项也会让你头疼不已。随着 Microsoft .NET Framework 2.0 中泛型的出现,许多人都开始创建如下所示的参数化的 Repository 类:
public interface IRepository<T> {
  void Save(T subject);
  void Delete(T subject);
}
在此接口中,T 是 Invoice、Order 或 Shipment 之类的域实体。StructureMap 的用户希望能够调用此代码并获得完全成形的能处理特定域实体的存储库对象,如 Invoice 对象:
IRepository<Invoice> repository = 
  ObjectFactory.GetInstance<IRepository<Invoice>>();
这听起来是一个不错的功能,因此我着手为这些种类的参数化类型添加支持。对 StructureMap 进行这种更改后来证明是非常困难的,就是因为如下所示的代码:
_PluginFamilies.Add(family.PluginType.FullName,family);
以及如下所示的代码:
MementoSource source = 
  this.getMementoSourceForFamily(pluginType.FullName);
还有如下所示的代码:
private IInstanceFactory this[Type PluginType] {
  get {
    return this[PluginType.FullName];
  }
  set {
    this[PluginType.FullName] = value;
  }
}
您是否已发现了重复?不要过于沉浸在此示例中,我有个毫无疑问的规则表明与 System.Type 相关的对象是通过将 Type.FullName 属性用作 Hashtable 中的键进行存储的。这是个毫不起眼的逻辑,但我已在整个代码库中重复了多次。在实现泛型时,我判定如果按实际类型而不是 Type.FullName 在内部存储对象,这个逻辑会更有效。
这个在行为方面做出的看似微小的变动却花费了我数天的时间,而不是先前假定的数小时,因为我已将这少量数据重复了很多次。我从中得到的教训是,对于系统中的任何规则,无论表面看来多么微不足道,都应只表达一次。
总结
聚合和耦合应用于设计和体系结构的每个级别,但我多数时候是侧重于类和方法级别的细粒度细节。当然,您最好具有较大的体系结构决策权 – 技术选择、项目构造和物理部署都很重要,但这些决策通常都完全限制在多个选择中,而权衡得失利弊之后所做的选择通常能够得到广泛的理解。
我发现您在类和方法级别所做的成千上百个小的决策的累积效应对项目的成功具有着深远的影响,而您也在小的事情上也得到了更多的选择和替代方案。尽管通常在生活中不一定是这样,但在软件设计中请不要忽视这些小事情。
开发人员们共有的看法是,担心所有这些聚合和耦合问题不过是影响工作进度的象牙塔理论。我的感受是,如果您的代码的聚合和耦合质量良好,会随着时间的推移一直保持代码的工作效率。我强烈建议您将对聚合和耦合质量的认识内在化到无需有意识地思考这些质量的程度。此外,我能够推荐的用于改进您的设计技巧的最佳练习之一就是重新回顾以前的编码成果,尝试找到本来可以改进这些旧代码的方法,然后设法回忆过去的设计中使代码易于更改或难以调整的元素。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结