什么是耦合?
让我们从古老的设计模式:可重复使用的面向对象软件的元素,AKA四人帮(GoF)书中定义。
耦合 - 软件组件相互依赖的程度。

因此,耦合并不总是坏事,是吗?应用程序内部的组件必须相互依赖,或者它只是一组不相关的东西。如果IDE中的代码窗口不能依赖操作系统来打开文件,那么您将无法完成大量工作。
那么,什么时候耦合编程问题呢?
让我们回到GoF。
紧密耦合的类很难单独重用,因为它们彼此依赖...松散耦合增加了类本身可以重用的可能性,并且系统可以更容易地学习,移植,修改和扩展。
紧耦合是我们需要避免的。如果组件共享太多依赖项,则维护会受到影响。因此,他们很难理解,更糟糕的是,修改。
耦合的一个简单例子
紧密耦合的发件人和接收者
让我们看一下打开文本文件并将其内容打印到命令行会话的代码。GOF书的“行为模式”一章中的发送者和接收者示例启发了这个例子。
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Receiver receiver = new Receiver(@\"\\Mac\Home\Documents\test.txt\");
receiver.StartReading();
Console.WriteLine(\"Press any key to exit.\");
Console.ReadKey();
}
}
public class Receiver
{
private readonly Sender _sender;
public Receiver(string filename)
{
_sender = new Sender(filename, this);
}
public void StartReading()
{
_sender.StartSending();
}
public void Receive(string message)
{
System.Console.WriteLine(message);
}
}
public class Sender
{
private readonly System.IO.StreamReader _file;
private readonly Receiver _receiver;
public Sender(string filename, Receiver receiver)
{
_file = new System.IO.StreamReader(filename);
_receiver = receiver;
}
public void StartSending()
{
Task.Factory.StartNew(() =>
{
string line;
while ((line = _file.ReadLine()) != null)
{
_receiver.Receive(line);
}
_file.Close();
});
}
}
}
如果我们查看Program类,代码看起来就像一个简单的工具。我们向Reader传递要读取的文件的名称,然后告诉它开始阅读。有一些更简单的方法可以在不创建两个新类的情况下将文本文件打印到控制台。但是如果我们将这个例子视为一般实用程序的第一次迭代,那就不错了。
但是,当我们想要为不同的来源和目的地添加选项时会发生什么?也许我们想将文件的内容发送到打印机。或者,一次从多个文件中读取。也许我们想打开一个二进制文件并以十六进制显示其内容。有了这个设计,我们必须进行代码更改以更改文件的名称!
使用此设计,接收方接受文件名并创建发件人。发件人接受对阅读器的引用,并将文件内容传回给它。两个类都知道彼此的详细信息,接收方必须知道要读取的文件的名称。只用几句话解释设计听起来很复杂。
松散耦合的发件人和接收者
如果您已经使用C#或任何其他面向对象的语言一段时间,您就会知道接下来会发生什么: 接口。
首先,让我们为发送者和接收者创建接口。
public interface ISender
{
void AddReceiver(IReceiver receive);
void StartSending();
}
public interface IReceiver
{
void Receive(string message);
}
在这里,有更好的方法来做到这一点,但这个实现很短,让我们专注于耦合。
该IReceiver接口是因为它可以很简单。它接受一条消息。一个IReceiver可以做一个消息什么。它可能会将其写入另一个文件,将其打印到控制台,甚至更改数据并将其传递给另一个IReceiver。
ISender有两种方法可以做出一些承诺。它接受对IReceiver的引用,这意味着它将保存并发送消息。它有一个StartSending方法而不是Read,这表明它将处理在自己的线程中获取和传递消息给IReceivers。
现在,让我们重构我们原来的类。
所以,这是我们的新接收器。
public class ConsoleReceiver : IReceiver
{
public void Receive(string message)
{
System.Console.WriteLine(message);
}
}
说它比旧版本更简单是轻描淡写。它唯一关心的是接受一个字符串消息并将其打印到控制台。它不会创建发件人或启动它。它没有自己的状态。
现在,让我们看看新的发件人:
public class TextFileSender : ISender
{
private readonly System.IO.StreamReader _file;
private readonly List<IReceiver> _receivers = new List<IReceiver>();
public TextFileSender(string filename)
{
_file = new System.IO.StreamReader(filename);
}
public void AddReceiver(IReceiver receiver)
{
_receivers.Add(receiver);
}
public void StartSending()
{
Task.Factory.StartNew(() =>
{
string line;
while ((line = _file.ReadLine()) != null)
{
_receivers.ForEach(receiver =>
receiver.Receive(line)
);
}
_file.Close();
});
}
}
新的发送者已经获得了一些额外的功能,但只需花费一些新代码。它现在拥有一个接收器列表,并将向所有接收者发送消息。我们将接收器从构造函数移动到一个新方法,它引入了在没有接收器时开始发送消息的可能性。但是,使用列表可以防止我们崩溃。如果没有项目,发件人会悄悄丢弃该邮件。
但是,缺少某些东西。谁创建发件人和收件人?
添加指挥官
我们已将部分工作委托给Main函数。GoF称这种模式为指挥官。
static void Main(string[] args)
{
ISender sender = new TextFileSender(@\"\\Mac\Home\Documents\test.txt\");
IReceiver receiver = new ConsoleReceiver();
sender.AddReceiver(receiver);
sender.StartSending();
Console.WriteLine(\"Press any key to exit.\");
Console.ReadKey();
}
因此,Main函数创建发送者和接收者,连接它们,并开始处理。如果我们想要从不同的源获取消息或将它们发送到一个或多个新目的地,我们可以重用这些接口并对main进行少量修改。
耦合与凝聚力
我们如何将此示例应用于实际代码?
软件工程师经常将耦合与另一种软件设计概念进行对比:凝聚力。耦合是指组件相互依赖的程度。内聚力衡量一个组件的各个部分属于一起。这两个属性成反比。
紧耦合导致低内聚力。松耦合导致高内聚力。
我们通过分离发送者和接收者之间的关注来降低我们示例中的耦合。接收方不需要知道数据来自何处。因此,它不需要创建发件人并保留对它的引用。发送者需要知道发送数据的位置,但不需要创建接收者或知道有多少接收者。
我们最终将两个类中嵌入的一些逻辑推到了main中。在生产代码中,您将使用称为调度程序或管理器之类的正式类替换main。您甚至可以将接收器列表推入其中。
测量耦合
NDepend可以通过向您展示消除紧密耦合和增加内聚力的机会来帮助您改进代码。
传出耦合显示了严重依赖他人的类型。此度量标准突出了设计问题,因为与大量对象交互的类型有太多问题。
传入耦合显示在程序中大量使用的方法。单独使用它不是衡量代码质量的标准,但它可能指出应该将方法分离到自己的组件中的区域,或者将其分解为多个区域的区域。
不要被烧伤
耦合会影响您修改和维护代码的能力。如果物体紧密耦合,很难推断它是如何工作的。此外,更改往往会波及许多对象。本教程介绍了基本概念,但还有很多东西需要学习。看看你的代码,看看你是否能找到可以降低耦合度并增加内聚力的区域。