【模块化那些事】 拆散的模块化
模块化原则倡导利用集中和分解等手法创建高内聚、低耦合的抽象。
为了理解模块化的含义及其很重要的原因,来看看一本书的极端情况。假设一本书像讲一个长故事一样阐述其中的内容,中间没有任何停顿,也没有章节。试问面对这样的图书,读者将作何反应呢?我估计心中一定有千万只草泥马在崩腾吧。如果这本书根据内容分为不同的章节(模块)进行讲述,情况是不是就完全不一样了呢?同样,设计软件时,遵循模块化原则也很重要。需要指出的是模块化通常是一个系统级考虑因素,指的是如何将抽象组织成逻辑模块。但是我们这里的术语模块指的是类级抽象:具体类、抽象类和接口。模块化的目标是创建高内聚、低耦合的抽象。
应用模块化原则的实现手法
- 将相关的数据和方法集中在一起:每个抽象都必须是内聚的,即抽象应将相关的数据和操作它们的方法集中在一起。
- 将抽象分解为易于管理的规模:将大抽象分解为规模适中(既不太大也不太小)的小抽象。大类不仅难以理解,而且难以修改,因为这种类实现的职责可能交织在一起。
- 创建非循环依赖:抽象之间不应该存在循环依赖。否则修改一个抽象可能引起连锁反应,波及整个设计。
- 限制依赖关系数:创建扇入和扇出低的抽象。扇入指的是有多少个抽象依赖于当前的抽象,因此修改扇入高的抽象时,可能需要修改大量依赖于它的抽象。扇出指的是当前抽象依赖于多少个其它的抽象,高扇出意味着修改很多抽象时都可能影响当前抽象。为避免潜在的修改引发连锁反应,减少设计中抽象之间的依赖关系数很重要。
违反模块化原则导致的坏味
我们这篇博客主要讲解分析拆散的模块化坏味,对于其它模块化坏味将在后面的博客讲解分析。
拆散的模块化
应集中放在一个抽象中的数据和方法分散在多个抽象中,将导致这种坏味。
常见表现形式如下:
-
类被用作数据容器,其中没有任何操作这些数据的方法
-
类的方法更多的被其它类成员调用
为什么不能有拆散的模块化?
如果抽象只包含数据成员,而操作这些数据成员的方法分散在多个抽象中时,原本应属于一个抽象的成员分散在多个抽象中时,将导致这些抽象之间紧密耦合。违反了模块化原则。
拆散的模块化潜在原因
以过程型思维使用面向对象语言
过程型语言倾向于将数据和操作它的函数分开,从而导致这种坏味。
不熟悉既有设计
大型的项目设计很复杂。在这样的项目中,每位开发人员通常只负责系统中很小的一部分,不了解设计的其它部分。这可能导致成员被放置到错误的类中。
示例分析
来看一个设备管理应用程序。在这个应用程序中,与设备相关的数据存储在DeviceData类中,而处理这些设备数据的方法由Device类提供。DeviceData类只有公共数据成员,没有任何方法。而Device类包含一个类型为DeviceData的对象,并提供了访问和操作该数据成员的方法。
这些数据和方法原本应该集中放在一个类中,却分散在了Device和DeviceData类中,显然存在"拆散的模块化"坏味。
代码实现:
public class DeviceData
{
/// <summary>
/// 设备ID
/// </summary>
public string DeviceID { get; set; }
/// <summary>
/// 设备位置
/// </summary>
public string DevicePath { get; set; }
/// <summary>
/// 是否可用
/// </summary>
public bool Enabled { get; set; }
}
public class Device
{
private DeviceData deviceData = new DeviceData();
/// <summary>
/// 获取设备ID
/// </summary>
/// <returns></returns>
public string GetDeviceID()
{
return deviceData.DeviceID;
}
/// <summary>
/// 设置设备ID
/// </summary>
/// <param name="deviceID">设备ID</param>
/// <returns></returns>
public bool SetDeviceID(string deviceID)
{
deviceData.DeviceID = deviceID;
return true;
}
/// <summary>
/// 是否可用
/// </summary>
/// <returns></returns>
public bool IsEnabled()
{
return deviceData.Enabled;
}
}
重构"拆散的模块化"
-
如果一个方法更多地被另一个类(Target类)而不是定义它的类(Source类)调用,就采用“移动方法”,将这个方法从Source类移到Target类中。
-
如果一个字段更多地被另一个类(Target类)而不是定义它的类(Source类)使用,就采用“移动字段”,将这个字段从Source类移到Target类中。
重构后的代码实现:
public class Device
{
/// <summary>
/// 设备ID
/// </summary>
private string DeviceID { get; set; }
/// <summary>
/// 设备位置
/// </summary>
private string DevicePath { get; set; }
/// <summary>
/// 是否可用
/// </summary>
private bool Enabled { get; set; }
/// <summary>
/// 获取设备ID
/// </summary>
/// <returns></returns>
public string GetDeviceID()
{
return DeviceID;
}
/// <summary>
/// 设置设备ID
/// </summary>
/// <param name="deviceID">设备ID</param>
/// <returns></returns>
public bool SetDeviceID(string deviceID)
{
DeviceID = deviceID;
return true;
}
/// <summary>
/// 是否可用
/// </summary>
/// <returns></returns>
public bool IsEnabled()
{
return Enabled;
}
}
现实考虑
数据传输对象
在使用远程接口的情况下,常常使用数据传输对象(DTO)在进程之间传输数据,以减少远程调用数。DTO聚合数据但不包含行为。这是有意为之,为了方便数据同步。