43

我们一直在使用松耦合和依赖注入来开发代码。

许多“服务”风格的类都有一个构造函数和一个实现接口的方法。每个单独的类都非常容易孤立地理解。

然而,由于耦合的松散,查看一个类并不能告诉您它周围的类或它在更大范围内的位置。

使用 Eclipse 跳转到协作者并不容易,因为您必须通过界面。如果接口是Runnable,则对于查找实际插入的类没有帮助。确实有必要回到 DI 容器定义并尝试从那里找出问题。

这是依赖注入服务类中的一行代码:-

  // myExpiryCutoffDateService was injected, 
  Date cutoff = myExpiryCutoffDateService.get();

这里的耦合尽可能松散。到期日可以以任何方式逐字执行。

这是在耦合度更高的应用程序中的样子。

  ExpiryDateService = new ExpiryDateService();
  Date cutoff = getCutoffDate( databaseConnection, paymentInstrument );

从紧密耦合的版本中,我可以推断截止日期是通过使用数据库连接的支付工具以某种方式确定的。

我发现第一种风格的代码比第二种风格的代码更难理解。

你可能会争辩说,在阅读这门课时,我不需要知道截止日期是如何计算出来的。确实如此,但如果我正在缩小错误范围或找出需要增强的地方,那么这是有用的信息。

还有其他人遇到这个问题吗?你有什么解决方案?这只是要适应的东西吗?是否有任何工具可以可视化类的连接方式?我应该让课程更大还是更耦合?

(故意让这个问题与容器无关,因为我对任何答案都感兴趣)。

4

10 回答 10

35

虽然我不知道如何在一个段落中回答这个问题,但我试图在一篇博客文章中回答它:http: //blog.ploeh.dk/2012/02/02/LooseCouplingAndTheBigPicture.aspx

总而言之,我发现最重要的几点是:

  • 了解松散耦合的代码库需要不同的思维方式。虽然“跳到合作者”更难,但它也应该或多或少无关紧要。
  • 松散耦合就是理解一个部分而不理解整体。您应该很少需要同时理解所有内容。
  • 当关注一个错误时,你应该依靠堆栈跟踪而不是代码的静态结构来了解协作者。
  • 编写代码以确保其易于理解是开发人员的责任——而不是开发人员阅读代码的责任。
于 2012-02-02T20:57:01.773 回答
12

一些工具了解 DI 框架并知道如何解决依赖关系,从而允许您以自然的方式导航代码。但是当它不可用时,您只需要尽可能使用您的 IDE 提供的任何功能。

我使用 Visual Studio 和一个定制的框架,所以你描述的问题是我的生活。在 Visual Studio 中,SHIFT+F12 是我的朋友。它显示了对光标下符号的所有引用。一段时间后,您习惯了通过代码进行必然的非线性导航,并且考虑“哪个类实现此接口”和“注入/配置站点在哪里,所以我可以看到哪个类”成为第二天性被用来满足这个接口依赖”。

还有一些可用于 VS 的扩展,它们提供 UI 增强功能来帮助解决此问题,例如Productivity Power Tools。例如,您可以将鼠标悬停在某个接口上,会弹出一个信息框,您可以单击“实现者”以查看解决方案中实现该接口的所有类。您可以双击以跳转到任何这些类的定义。(无论如何,我仍然通常只使用 SHIFT+F12)。

于 2012-01-13T23:16:29.430 回答
8

我刚刚对此进行了内部讨论,最后写了这篇文章,我认为这太好了,不能分享。我在这里复制它(几乎)未经编辑,但即使它是更大的内部讨论的一部分,我认为它的大部分都可以独立存在。

讨论是关于引入一个名为 的自定义接口IPurchaseReceiptService,以及是否应该将其替换为 of IObserver<T>


好吧,我不能说我对此有任何强有力的数据点——这只是我正在追求的一些理论......但是,我目前关于认知开销的理论是这样的:考虑你的特殊IPurchaseReceiptService

public interface IPurchaseReceiptService
{
    void SendReceipt(string transactionId, string userGuid);
}

如果我们将它保留为当前的Header 接口,它只有那个SendReceipt方法。这很酷。

不太酷的是,您必须为接口取一个名称,并为方法取另一个名称。两者之间有一点重叠:Receipt这个词出现了两次。IME,有时这种重叠会更加明显。

此外,接口的名称是IPurchaseReceiptService,这也不是特别有用。Service后缀本质上是新的Manager 并且在 IMO 中是一种设计气味。

此外,您不仅必须命名接口和方法,而且还必须在使用时命名变量:

public EvoNotifyController(
    ICreditCardService creditCardService,
    IPurchaseReceiptService purchaseReceiptService,
    EvoCipher cipher
)

在这一点上,你基本上已经说了三次同样的事情。根据我的理论,这是认知开销,以及设计可以而且应该更简单的气味。

现在,将此与使用众所周知的接口进行对比,例如IObserver<T>

public EvoNotifyController(
    ICreditCardService creditCardService,
    IObserver<TransactionInfo> purchaseReceiptService,
    EvoCipher cipher
)

这使您能够摆脱官僚主义并减少设计问题的核心。您仍然有意图显示的命名 - 您只需将设计从Type Name Role Hint转换为Argument Name Role Hint


当谈到关于“断开连接”的讨论时,我并不幻想使用IObserver<T>会神奇地解决这个问题,但我对此有另一种理论。

我的理论是,许多程序员发现接口编程如此困难的原因正是因为他们习惯了 Visual Studio 的Go to definition功能(顺便说一句,这是工具如何让头脑变得糟糕的另一个例子)。这些程序员永远处于一种需要知道“接口的另一端”是什么的心态。为什么是这样?可能是因为抽象很差吗?

这与RAP相关,因为如果您确认程序员相信每个接口背后都有一个单一的、特定的实现,难怪他们认为接口只是阻碍。

但是,如果你应用 RAP,我希望慢慢地,程序员会了解到,在一个特定的接口背后,可能有那个接口的任何实现,并且他们的客户端代码必须能够处理该接口的任何实现而不改变其正确性系统。如果这个理论成立,我们刚刚将Liskov 替换原则引入了代码库,而不会用他们不理解的高级概念吓到任何人:)

于 2014-09-04T07:15:27.170 回答
7

然而,由于耦合的松散,查看一个类并不能告诉您它周围的类或它在更大范围内的位置。

这是不准确的。对于每个类,您确切地知道该类所依赖的对象类型,以便能够在运行时提供其功能。
您了解它们是因为您知道预计将注入哪些对象。

您不知道的是将在运行时注入的实际具体类,它将实现您知道您的类所依赖的接口或基类。

因此,如果您想查看实际注入的类是什么,您只需查看该类的配置文件即可查看注入的具体类。

您还可以使用 IDE 提供的功能。
既然您指的是 Eclipse,那么 Spring 有一个插件,并且还有一个显示您配置的 bean 的可视选项卡。你检查了吗?这不是你要找的吗?

还可以查看Spring 论坛中的相同讨论

更新:
再次阅读您的问题,我认为这不是一个真正的问题。
我的意思是以下方式。
就像所有的东西loose coupling都不是灵丹妙药,它本身也有它的缺点。
大多数人倾向于关注好处,但作为任何解决方案,它都有其缺点。

您在问题中所做的是描述它的主要缺点之一,即确实不容易看到大局,因为您可以配置所有东西并插入任何东西
还有其他一些可以抱怨的缺点,例如它比紧密耦合的应用程序慢,但仍然是正确的。

无论如何,重申一下,您在问题中描述的内容不是您踩到的问题,并且可以找到标准解决方案(或任何这种方式)。

这是松散耦合的缺点之一,必须确定此成本是否高于您实际获得的成本,就像在任何设计决策权衡中一样。

这就像在问:
嘿,我正在使用这个名为Singleton. 它工作得很好,但我不能创建新对象!我怎样才能解决这个问题呢????
好吧,你不能;但如果你需要,也许单身不适合你....

于 2012-01-13T23:31:14.430 回答
5

帮助我的一件事是将多个密切相关的类放在同一个文件中。我知道这违背了一般建议(每个文件有 1 个类),我通常同意这一点,但在我的应用程序架构中它工作得很好。下面我将尝试解释这种情况。

我的业务层的架构是围绕业务命令的概念设计的。定义了命令类(只有数据而没有行为的简单 DTO),并且对于每个命令都有一个“命令处理程序”,其中包含执行该命令的业务逻辑。每个命令处理程序都实现通用ICommandHandler<TCommand>接口,其中 TCommand 是实际的业务命令。

消费者依赖于ICommandHandler<TCommand>并创建新的命令实例并使用注入的处理程序来执行这些命令。这看起来像这样:

public class Consumer
{
    private ICommandHandler<CustomerMovedCommand> handler;

    public Consumer(ICommandHandler<CustomerMovedCommand> h)
    {
        this.handler = h;
    }

    public void MoveCustomer(int customerId, Address address)
    {
        var command = new CustomerMovedCommand();

        command.CustomerId = customerId;
        command.NewAddress = address;

        this.handler.Handle(command);
    }
}

现在消费者只依赖于一个特定的ICommandHandler<TCommand>并且没有实际实现的概念(应该是)。然而,虽然Consumer应该对实现一无所知,但在开发过程中,我(作为开发人员)对实际执行的业务逻辑非常感兴趣,因为开发是在垂直切片中完成的;这意味着我经常同时处理一个简单功能的 UI 和业务逻辑。这意味着我经常在业务逻辑和 UI 逻辑之间切换。

所以我所做的就是把命令(在这个例子中CustomerMovedCommand和 的实现ICommandHandler<CustomerMovedCommand>)放在同一个文件中,首先是命令。因为命令本身是具体的(因为它是 DTO,所以没有理由抽象它)跳转到类很容易(Visual Studio 中的 F12)。通过将处理程序放在命令旁边,跳转到命令也意味着跳转到业务逻辑。

当然,这仅在命令和处理程序可以位于同一个程序集中时才有效。当您的命令需要单独部署时(例如在客户端/服务器场景中重用它们时),这将不起作用。

当然,这只是我业务层的 45%。然而,另一个大的和平(比如 45%)是查询,它们的设计类似,使用查询类和查询处理程序。这两个类也放在同一个文件中,这再次允许我快速导航到业务逻辑。

因为命令和查询约占我业务层的 90%,所以在大多数情况下,我可以非常快速地从表示层移动到业务层,甚至可以在业务层内轻松导航。

我必须说这是我将多个类放在同一个文件中的唯一两种情况,但使导航更容易。

如果你想了解更多关于我是如何设计这个的,我写了两篇关于这个的文章:

于 2012-01-14T00:53:53.080 回答
4

In my opinion, loosely coupled code can help you much but I agree with you about the readability of it. The real problem is that name of methods also should convey valuable information.

That is the Intention-Revealing Interface principle as stated by Domain Driven Design ( http://domaindrivendesign.org/node/113 ).

You could rename get method:

// intention revealing name
Date cutoff = myExpiryCutoffDateService.calculateFromPayment();

I suggest you to read thoroughly about DDD principles and your code could turn much more readable and thus manageable.

于 2012-02-05T08:16:26.793 回答
2

我发现The Brain作为节点映射工具在开发中很有用。如果您编写一些脚本将您的源代码解析为 The Brain 接受的 XML,您可以轻松浏览您的系统。

秘诀是在您要跟踪的每个元素的代码注释中添加 guid,然后可以单击 The Brain 中的节点以将您带到 IDE 中的该 guid。

于 2012-01-14T01:57:13.927 回答
2

文档!

是的,您提到了松耦合代码的主要缺点。如果您最终可能已经意识到,它会得到回报,确实,找到“哪里”进行修改总是会更长,而且您可能需要打开几个文件才能找到“正确的位置”。 ..

但那是真正重要的事情:文档。奇怪的是,没有答案明确提到这一点,这是所有大型开发项目的主要要求。

API 文档
具有良好搜索功能的 APIDoc。每个文件和——几乎——每个方法都有清晰的描述。

“大图”文档
我认为有一个解释大图的维基是件好事。Bob做了一个代理系统?它是如何工作的?它是否处理身份验证?什么样的组件会用到它?不是一个完整的教程,只是一个你可以阅读 5 分钟的地方,弄清楚涉及哪些组件以及它们如何链接在一起。

我确实同意 Mark Seemann 回答的所有观点,但是当你第一次进入一个项目时,即使你很好地理解了解耦的原则,你也需要大量的猜测,或者某种帮助找出在哪里实现您想要开发的特定功能。

...再次:APIDoc 和一个小开发者 Wiki。

于 2012-02-06T20:02:34.290 回答
2

取决于有多少开发人员正在从事项目以及您是否想在不同的项目中重用其中的某些部分,松散耦合可以为您提供很多帮助。如果您的团队很大并且项目需要跨越数年,松散耦合会有所帮助,因为可以更轻松地将工作分配给不同的开发人员组。我使用带有大量 DI 的 Spring/Java,Eclipse 提供了一些图表来显示依赖关系。使用 F3 在光标下打开课程有很大帮​​助。如前文所述,了解工具的快捷方式将对您有所帮助。

要考虑的另一件事是创建自定义类或包装器,因为它们比您已经拥有的常见类(如 Date)更容易跟踪。

如果您使用多个模块或应用程序层,那么了解项目流程到底是什么可能是一个挑战,因此您可能需要创建/使用一些自定义工具来查看所有内容如何相互关联。我为自己创建了这个,它帮助我更容易地理解项目结构。

于 2012-02-01T20:38:12.003 回答
0

我很惊讶没有人写过松耦合代码的可测试性(当然是单元测试)和紧耦合设计的不可测试性(同样的)!您应该选择哪种设计是显而易见的。今天有了所有的 Mock 和 Coverage 框架,这很明显,至少对我来说是这样。

除非您不对代码进行单元测试,或者您认为自己进行了测试,但实际上您并没有……通过紧密耦合几乎无法实现孤立的测试。

您认为您必须浏览 IDE 中的所有依赖项吗?忘掉它!这与编译和运行时的情况相同。在编译过程中几乎找不到任何错误,除非您测试它,否则您无法确定它是否有效,即执行它。想知道界面背后是什么?放置一个断点并运行该死的应用程序。

阿门。

...评论后更新...

不确定它是否会为您服务,但在 Eclipse 中有一种叫做层次视图的东西。它向您展示了项目中接口的所有实现(不确定工作区是否也是如此)。您只需导航到界面并按 F4。然后它将向您展示实现该接口的所有具体和抽象类。

按 F4 后 Eclipse 中的层次结构视图

于 2012-02-06T20:05:32.130 回答