实用模式--聚合和耦合
发布时间:2020-05-23 03:53:18 所属栏目:程序设计 来源:互联网
导读:实用模式 聚合和耦合 很多软件设计一直都存在一个问题:这段代码应放置在哪里?我一直在寻找编排代码的最佳方法,以便能够更轻松地编写、理解代码,并在以后更方便地进行更改。如果我的代码构造很漂亮,我将可名扬四海,无限荣光。如果构造得很糟糕,那些追
|
实用模式 聚合和耦合 很多软件设计一直都存在一个问题:这段代码应放置在哪里?我一直在寻找编排代码的最佳方法,以便能够更轻松地编写、理解代码,并在以后更方便地进行更改。如果我的代码构造很漂亮,我将可名扬四海,无限荣光。如果构造得很糟糕,那些追随我的开发人员会一直对我埋怨不停。 我特别想在我的代码结构方面实现三个具体目标:
public class BusinessLogicClass {
public void DoSomething() {
// Go get some configuration
int threshold =
int.Parse(ConfigurationManager.AppSettings["threshold"]);
string connectionString =
ConfigurationManager.AppSettings["connectionString"];
string sql =
@"select * from things
size > ";
sql += threshold;
using (SqlConnection connection =
new SqlConnection(connectionString)) {
connection.Open();
SqlCommand command = new SqlCommand(sql,connection);
using (SqlDataReader reader = command.ExecuteReader()) {
while (reader.Read()) {
string name = reader["Name"].ToString();
string destination = reader["destination"].ToString();
// do some business logic in here
doSomeBusinessLogic(name,destination,connection);
}
}
}
}
}
假设我们真正关心的是实际的业务处理,但我们的业务逻辑代码是与数据访问方面的焦点以及配置设置结合在一起的。那么,这种代码有可能出现什么错误呢?
第一个问题是该代码会因侧重点失偏而有些难以理解。我将在有关聚合的下一节中深入讨论这一点。
第二个问题是数据访问策略、数据库结构或配置策略中的任何更改同样也会波及整个业务逻辑代码,因为它们全部都属于同一个代码文件。这种业务逻辑对底层基础结构过于了解。
第三,我们不能独立于特定的数据库结构或在没有 AppSettings 键的情况下重用该业务逻辑代码。我们也不能重用在 BusinessLogicClass 中内嵌的数据访问功能。数据访问与业务逻辑之间的耦合可能不是问题,但如果我们希望改变此业务逻辑的用途,以对照由分析人员直接输入到 Excel 电子表格中的数据来使用它会怎样呢?如果我们要单独测试或调试该业务逻辑又会怎样呢?我们无法实现上述的任何操作,因为该业务逻辑与数据访问代码是紧密耦合的。如果我们能将业务逻辑从其他关注问题中隔离出来,则对它进行更改就会变得轻松得多。
总之,在类与模块之间实现松散耦合的实际目标是为了:
public void Process() {
string connectionString = getConnectionString();
SqlConnection connection = new SqlConnection(connectionString);
DataServer1 server = new DataServer1(connection);
int daysOld = 5;
using (SqlDataReader reader = server.GetWorkItemData(daysOld)) {
while (reader.Read()) {
string name = reader.GetString(0);
string location = reader.GetString(1);
processItem(name,location);
}
}
}
现在让我们重新编写
图 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 所示。
图 3 违背定律
复制代码
public interface DataService {
InsuranceClaim[] FindClaims(Customer customer);
}
public class Repository {
public DataService InnerService { get; set; }
}
public class ClassThatNeedsInsuranceClaim {
private Repository _repository;
public ClassThatNeedsInsuranceClaim(Repository repository) {
_repository = repository;
}
public void TallyAllTheOutstandingClaims(Customer customer) {
// This line of code violates the Law of Demeter
InsuranceClaim[] claims =
_repository.InnerService.FindClaims(customer);
}
}
ClassThatNeedsInsuranceClaim 类需要获取 InsuranceClaim 数据。它有一个对 Repository 类的引用,Repository 类本身包含一个 DataService 对象。ClassThatNeedsInsuranceClaim 到达 Repository 内部以获取内部 DataService 对象,然后调用 Repository.FindClaims 获得其数据。请注意,对 _repository.InnerService.FindClaims(customer) 的调用明显违背了 Demeter 定律,因为 ClassThatNeedsInsuranceClaim 将直接调用其 Repository 字段的属性的方法。现在,请将您注意力转向
图 4,该图显示了同一代码的另一个示例,但这次它遵循了 Demeter 定律。
图 4 较好的解耦
复制代码
public class Repository2 {
private DataService _service;
public Repository2(DataService service) {
_service = service;
}
public InsuranceClaim[] FindClaims(Customer customer) {
// we're simply going to delegate to the inner
// DataService for now,but who knows what
// we want to do in the future?
return _service.FindClaims(customer);
}
}
public class ClassThatNeedsInsuranceClaim2 {
private Repository2 _repository;
public ClassThatNeedsInsuranceClaim2(
Repository2 repository) {
_repository = repository;
}
public void TallyAllTheOutstandingClaims(
Customer customer) {
// This line of code now follows the Law of Demeter
InsuranceClaim[] claims = _repository.FindClaims(customer);
}
}
我们实现了什么?Repository2 比 Repository 更易于使用,因为您有一个直接的方法来调用 InsuranceClaim 信息。在我们违反 Demeter 定律时,Repository 的使用者与 Repository 的实现紧密耦合。在修订过的代码中,我可以更好地更改 Repository 实现以添加更多高速缓存或以完全不同的对象换出基础 DataService。
Demeter 定律是一个功能强大的工具,可帮助您发现潜在的耦合问题,但不可盲目地遵循 Demeter 定律。违背 Demeter 定律的确会使您的系统实现更紧密的耦合,但有时候您可能会认为耦合到代码的某个稳定元素的潜在成本要比编写大量委托代码来避免违背 Demeter 定律的成本低。
只是告知,不要询问
“只是告知,不要询问”设计原则主张您告知对象将要执行什么任务。您不想做的事情是询问某对象其内部状态、对该状态做出决策,然后告知该对象将要执行什么任务。遵守“只是告知,不要询问”的对象交互风格是确保正确安置职责的有效途径。
图 5 说明了违背“只是告知,不要询问”原则的情况。该代码的任务是购买某种商品、确认 $10,000 以上的购买金额是否可能有折扣,最后检查帐户数据来判断是否有充足的资金。先前的 DumbPurchase 和 DumbAccount 类都无此功能。帐户和购买业务规则都是在 ClassThatUsesDumbEntities 中编码的。
图 5 过度询问
复制代码
public class DumbPurchase {
public double SubTotal { get; set; }
public double Discount { get; set; }
public double Total { get; set; }
}
public class DumbAccount {
public double Balance { get; set;}
}
public class ClassThatUsesDumbEntities {
public void MakePurchase(
DumbPurchase purchase,DumbAccount account) {
purchase.Discount = purchase.SubTotal > 10000 ? .10 : 0;
purchase.Total =
purchase.SubTotal*(1 - purchase.Discount);
if (purchase.Total < account.Balance) {
account.Balance -= purchase.Total;
}
else {
rejectPurchase(purchase,"You don't have enough money.");
}
}
}
这种类型的代码在几个方面可能存在问题。在与此类似的系统中,可能会出现重复,因为某个实体的业务规则分散在这些实体之外的程序代码中。您可能会不知不觉地重复逻辑,因为先前编写的业务逻辑所在位置并不明显。
图 6 显示了同一代码,但这次遵循了“只是告知,不要询问”的模式。在此代码中,我将用于购买和帐户的业务规则移动到它们自己的 Purchase 和 Account 类中。当我们打算进行购买时,只需告诉 Account 类从自身扣除购买金额即可。Account 和 Purchase 了解它们自身及其内部规则。Account 的使用者只需要知道去调用 Account.Deduct(Purchase,PurchaseMessenger) 方法就可以了。
图 6 告知您的应用程序要执行什么任务
复制代码
public class Purchase {
private readonly double _subTotal;
public Purchase(double subTotal) {
_subTotal = subTotal;
}
public double Total {
get {
double discount = _subTotal > 10000 ? .10 : 0;
return _subTotal*(1 - discount);
}
}
}
public class Account {
private double _balance;
public void Deduct(
Purchase purchase,PurchaseMessenger messenger) {
if (purchase.Total < _balance) {
_balance -= purchase.Total;
}
else {
messenger.RejectPurchase(purchase,this);
}
}
}
public class ClassThatObeysTellDontAsk {
public void MakePurchase(
Purchase purchase,Account account) {
PurchaseMessenger messenger = new PurchaseMessenger();
account.Deduct(purchase,messenger);
}
}
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 在内部存储对象,这个逻辑会更有效。
这个在行为方面做出的看似微小的变动却花费了我数天的时间,而不是先前假定的数小时,因为我已将这少量数据重复了很多次。我从中得到的教训是,对于系统中的任何规则,无论表面看来多么微不足道,都应只表达一次。
总结
聚合和耦合应用于设计和体系结构的每个级别,但我多数时候是侧重于类和方法级别的细粒度细节。当然,您最好具有较大的体系结构决策权 – 技术选择、项目构造和物理部署都很重要,但这些决策通常都完全限制在多个选择中,而权衡得失利弊之后所做的选择通常能够得到广泛的理解。
我发现您在类和方法级别所做的成千上百个小的决策的累积效应对项目的成功具有着深远的影响,而您也在小的事情上也得到了更多的选择和替代方案。尽管通常在生活中不一定是这样,但在软件设计中请不要忽视这些小事情。
开发人员们共有的看法是,担心所有这些聚合和耦合问题不过是影响工作进度的象牙塔理论。我的感受是,如果您的代码的聚合和耦合质量良好,会随着时间的推移一直保持代码的工作效率。我强烈建议您将对聚合和耦合质量的认识内在化到无需有意识地思考这些质量的程度。此外,我能够推荐的用于改进您的设计技巧的最佳练习之一就是重新回顾以前的编码成果,尝试找到本来可以改进这些旧代码的方法,然后设法回忆过去的设计中使代码易于更改或难以调整的元素。 (编辑:安卓应用网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
