如何理解松散耦合应用程序中的大局?

时间:2012-01-13 22:18:24

标签: oop language-agnostic dependency-injection loose-coupling

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

许多“服务”样式类都有一个构造函数和一个实现接口的方法。每个单独的课程都很容易理解。

然而,由于耦合的松散,看一个类不会告诉你它周围的类或它在更大的图片中的位置。

使用Eclipse跳转到协作者并不容易,因为你必须通过接口。如果接口是Runnable,则无法找到实际插入的类。真的有必要回到DI容器定义并尝试从那里解决问题。

以下是依赖注入服务类的一行代码: -

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

这里的耦合尽可能宽松。到期日以字面意思实施。

这是一个更加耦合的应用程序中的样子。

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

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

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

您可能会争辩说,在阅读本课程时,我不会需要知道截止日期是如何计算出来的。这是真的,但是如果我正在缩小某个bug或者想要增强需要插入的地方,那么这是有用的信息。

还有其他人遇到过这个问题吗?你有什么解决方案?这只是要调整的东西吗?是否有任何工具可以显示类连接在一起的方式?我应该让这些课程更大或更多吗?

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

10 个答案:

答案 0 :(得分:34)

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

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

  • 了解松散耦合的代码库需要不同的思维模式。虽然“跳到合作者”更难,但它也应该或多或少地无关紧要。
  • 松散耦合是关于理解一部分而不理解整体。你应该很少需要同时理解它。
  • 在处理错误时,您应该依赖堆栈跟踪而不是代码的静态结构来了解协作者。
  • 开发人员编写代码的责任是确保它易于理解 - 开发人员阅读代码并不负责。

答案 1 :(得分:12)

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

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

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

答案 2 :(得分:8)

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

讨论是关于引入名为IPurchaseReceiptService的自定义界面,以及是否应该使用IObserver<T>替换它。


好吧,我不能说我有关于这一点的强大数据点 - 这只是我正在追求的一些理论......然而,我目前关于认知开销的理论是这样的:考虑你的特殊IPurchaseReceiptService

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

如果我们将其保留为当前的Header Interface,则它只有SendReceipt个单一方法。那很酷。

你不得不想出一个接口名称,以及该方法的另一个名称,这不是很酷。两者之间有一些重叠:单词 Receipt 出现两次。 IME,有时重叠可能更加明显。

此外,界面的名称为IPurchaseReceiptService,这也不是特别有用。 服务后缀本质上是新的 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的转到定义功能(顺便说一句,这是{{3}的另一个例子。 }})。这些程序员永远处于一种心态,他们需要知道界面的另一面是什么。为什么是这样?可能是因为抽象很差?

这与tooling rots the mind有关,因为如果你确认程序员相信每个接口背后都有一个特定的实现,那么毫无疑问他们认为接口只是在路上。

但是,如果你应用RAP,我希望慢慢地,程序员将学习在特定接口后面,可能有该接口的任何实现,并且他们的客户端代码必须能够处理该接口的任何实现,而不改变系统的正确性。如果这个理论成立,我们只是将RAP引入代码库,而不会让任何人不敢理解他们不理解的高概念:)

答案 3 :(得分:7)

  

然而,由于耦合的松散,看着一堂课   没有告诉你它周围的类或它适合的地方   更大的图片。

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

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

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

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

另请参阅Spring Forum

中的相同讨论

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

你在问题​​中所做的是描述其主要缺点之一,因为你有一切可配置并插入任何东西<确实不容易看到大局/ strong>即可。
还存在其他缺点,例如它比紧耦合应用程序慢,但仍然是真的。

在任何情况下,重复迭代,您在问题中描述的内容不是您遇到的问题,并且可以找到标准解决方案(或任何方式)。

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

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

答案 4 :(得分: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逻辑之间切换。

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

当然,只有当命令和处理程序可以生活在同一个程序集中时,这才有效。当您的命令需要单独部署时(例如,在客户端/服务器方案中重用它们时),这将无效。

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

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

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

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

答案 5 :(得分:4)

在我看来,松散耦合的代码可以帮助你很多,但我同意你的可读性。 真正的问题是方法的名称也应该传达有价值的信息。

这是意图 - 揭示界面原则,如下所述 域驱动设计http://domaindrivendesign.org/node/113)。

您可以重命名 get 方法:

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

我建议您仔细阅读有关DDD原则的内容,您的代码可以变得更易读,也更容易管理。

答案 6 :(得分:2)

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

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

答案 7 :(得分:2)

根据项目开发人员的数量以及是否要在不同项目中重用其中的某些部分,松散耦合可以帮助您。如果您的团队很大并且项目需要跨越几年,那么松散耦合可以提供帮助,因为可以更轻松地将工作分配给不同的开发人员组。我使用带有大量DI的Spring / Java,Eclipse提供了一些显示依赖关系的图表。使用F3在光标下打开类有很大帮助。如前面的帖子所述,了解您的工具的快捷方式将对您有所帮助。

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

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

答案 8 :(得分:2)

文档!

是的,您将松散耦合代码的主要缺点命名为。如果你可能已经意识到最终它会得到回报,那么找到“在哪里”进行修改总是会更长,并且在找到“正确的位置”之前你可能需要打开几个文件。 ..

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

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

“大图”文档
我认为有一个维基解释大局是件好事。鲍勃已经建立了代理系统?它是如何运作的?它处理身份验证吗?什么样的组件会用它?不是一个完整的教程,而是一个可以阅读5分钟的地方,找出涉及哪些组件以及它们如何链接在一起。

我同意马克·西曼回答的所有要点,但是当你第一次参加一个项目时,即使你很清楚这些原则正在解决,你也需要大量的猜测,或某种帮助,以找出实现您想要开发的特定功能的位置。

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

答案 9 :(得分:0)

令我震惊的是,没有人写过松耦合代码的可测试性(当然是单元测试)和紧耦合设计的不可测试性(用相同的术语)!你应该选择哪种设计是没有道理的。今天,所有模拟和覆盖框架都很明显,至少对我而言。

除非你不对你的代码进行单元测试或者你认为你做了它们,但实际上你没有...... 通过紧耦合几乎无法实现隔离测试。

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

阿门。

...评论后更新......

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

The hierarchy view in Eclipse after pressing F4