WW.*_*WW. 42 language-agnostic oop dependency-injection loose-coupling
我们一直在使用松散耦合和依赖注入开发代码.
许多"服务"样式类都有一个构造函数和一个实现接口的方法.每个单独的课程都很容易理解.
但是,由于耦合的松散性,查看一个类不会告诉你它周围的类或它在更大的图片中的位置.
使用Eclipse跳转到协作者并不容易,因为你必须通过接口.如果接口是Runnable,那么找到实际插入的类是没有帮助的.真的有必要回到DI容器定义并尝试从那里解决问题.
以下是依赖注入服务类的一行代码: -
// myExpiryCutoffDateService was injected,
Date cutoff = myExpiryCutoffDateService.get();
Run Code Online (Sandbox Code Playgroud)
这里的耦合尽可能宽松.到期日以字面意思以任何方式实施.
这是一个更加耦合的应用程序中的样子.
ExpiryDateService = new ExpiryDateService();
Date cutoff = getCutoffDate( databaseConnection, paymentInstrument );
Run Code Online (Sandbox Code Playgroud)
从紧密耦合的版本,我可以推断截止日期是通过使用数据库连接的支付工具以某种方式确定的.
我发现第一种风格的代码比第二种风格的代码更难理解.
您可能会争辩说,在阅读本课程时,我不需要知道截止日期是如何计算出来的.这是真的,但是如果我正在缩小某个bug或者在增强需要插入的地方工作,那么这是有用的信息.
还有其他人遇到过这个问题吗?你有什么解决方案?这只是要调整的东西吗?是否有任何工具可以显示类连接在一起的方式?我应该让这些课程更大或更多吗?
(故意将这个问题与容器无关,因为我对任何答案感兴趣).
Mar*_*ann 34
虽然我不知道如何在一个段落中回答这个问题,但我试图在博客文章中回答:http://blog.ploeh.dk/2012/02/02/LooseCouplingAndTheBigPicture.aspx
总而言之,我发现最重要的一点是:
Igb*_*man 12
有些工具了解DI框架并知道如何解决依赖关系,允许您以自然的方式导航代码.但是当它不可用时,您只需使用IDE提供的任何功能即可.
我使用Visual Studio和定制框架,所以你描述的问题就是我的生活.在Visual Studio中,SHIFT + F12是我的朋友.它显示了光标下符号的所有引用.过了一段时间,你习惯了代码中必然的非线性导航,并且根据"哪个类实现了这个接口"和"注入/配置站点在哪里"来思考它,这是第二天性,所以我可以看到哪个类用于满足此接口依赖性".
还有可用于VS的扩展,它们提供了UI增强功能以帮助实现此功能,例如Productivity Power Tools.例如,您可以将鼠标悬停在界面上,弹出一个信息框,然后单击"已实施"以查看实施该界面的解决方案中的所有类.您可以双击以跳转到任何这些类的定义.(无论如何我通常只使用SHIFT + F12).
我刚刚对此进行了内部讨论,最后写了这篇文章,我觉得这篇文章太好了,不能分享.我在这里(几乎)未经编辑地复制它,但即使它是更大的内部讨论的一部分,我认为其中大部分都可以独立存在.
讨论是关于引入一个名为的自定义接口IPurchaseReceiptService,以及是否应该使用它来替换它IObserver<T>.
好吧,我不能说我有关于这一点的强大数据点 - 这只是我正在追求的一些理论......然而,我目前关于认知开销的理论是这样的:考虑你的特殊IPurchaseReceiptService:
public interface IPurchaseReceiptService
{
void SendReceipt(string transactionId, string userGuid);
}
Run Code Online (Sandbox Code Playgroud)
如果我们将它保留为当前的Header接口,它只有那个单一的SendReceipt方法.这很酷.
什么不是很酷,你不得不想出一个接口的名称,以及该方法的另一个名称.两者之间有一些重叠:收据一词出现两次.IME,有时重叠可能更加明显.
此外,界面的名称,IPurchaseReceiptService也不是特别有用.该服务后缀基本上是新的经理,并且是国际海事组织,一个设计的气味.
此外,您不仅需要命名接口和方法,还必须在使用时命名变量:
public EvoNotifyController(
ICreditCardService creditCardService,
IPurchaseReceiptService purchaseReceiptService,
EvoCipher cipher
)
Run Code Online (Sandbox Code Playgroud)
在这一点上,你基本上说过同样的事情三次.根据我的理论,这是认知开销,以及设计可以而且应该更简单的气味.
现在,将此与使用众所周知的界面进行对比IObserver<T>:
public EvoNotifyController(
ICreditCardService creditCardService,
IObserver<TransactionInfo> purchaseReceiptService,
EvoCipher cipher
)
Run Code Online (Sandbox Code Playgroud)
这使您能够摆脱官僚主义,减少设计的核心问题.您仍然有意图揭示命名 - 您只需将设计从类型名称角色提示转移到参数名称角色提示.
当涉及到"断开连接"的讨论时,我并没有幻想使用IObserver<T>意志神奇地使这个问题消失,但我有另一个理论.
我的理论是,许多程序员发现编程接口如此困难的原因恰恰是因为他们已经习惯了Visual Studio的Go to definition功能(顺便说一句,这是工具如何腐蚀思维的另一个例子).这些程序员永远处于一种心态,他们需要知道界面的另一面是什么.为什么是这样?可能是因为抽象很差?
这与RAP有关,因为如果你确认程序员相信每个接口背后都有一个特定的实现,那么毫无疑问他们认为接口只是在路上.
但是,如果您应用RAP,我希望慢慢地,程序员将学习在特定接口后面,可能有该接口的任何实现,并且他们的客户端代码必须能够处理该接口的任何实现而不改变正确性系统.如果这个理论成立,我们刚刚将Liskov替换原则引入代码库,而不会吓到任何人有他们不理解的高眉概念:)
但是,由于耦合的松散性,查看一个类不会告诉你它周围的类或它在更大的图片中的位置.
这是不准确的.对于每个类,您确切知道该类所依赖的对象类型,以便能够在运行时提供其功能.
你知道它们,因为你知道预期会注入哪些对象.
您不知道的是将在运行时注入的实际具体类,它将实现您知道类所依赖的接口或基类.
因此,如果您想查看实际注入的类是什么,您只需查看该类的配置文件即可查看注入的具体类.
您还可以使用IDE提供的工具.
既然您引用了Eclipse,那么Spring就有了一个插件,并且还有一个显示您配置的bean的可视选项卡.你检查过了吗?这不是你想要的吗?
另请参阅Spring论坛中的相同讨论
更新:
再次阅读你的问题,我不认为这是一个真正的问题.
我的意思是以下列方式.
像所有东西loose coupling都不是灵丹妙药,本身也有其自身的缺点.
大多数人倾向于关注好处,但作为任何解决方案,它都有其缺点.
你在你的问题中所做的是描述它的一个主要缺点,即它确实不容易看到大局,因为你有任何可配置和插入的东西.
还存在其他缺点,例如,它可能比紧耦合应用更慢并且仍然是真实的.
在任何情况下,重新迭代,您在问题中描述的内容不是您遇到的问题,可以找到标准解决方案(或任何方式).
这是松散耦合的缺点之一,你必须决定这个成本是否高于你实际获得的成本,就像任何设计决策权衡一样.
这就像问:
嘿,我正在使用这个名为的模式Singleton.它工作得很好,但我不能创建新的对象!我怎么能得到这个问题的人呢????
嗯,你不能; 但如果你需要,也许单身不适合你....
帮助我的一件事是在同一个文件中放置多个密切相关的类.我知道这违背了一般建议(每个文件有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);
}
}
Run Code Online (Sandbox Code Playgroud)
现在,消费者只依赖于特定的ICommandHandler<TCommand>,并且没有实际实现的概念(应该如此).然而,尽管Consumer应该对实现一无所知,但在开发过程中,我(作为开发人员)对执行的实际业务逻辑非常感兴趣,因为开发是在垂直切片中完成的; 这意味着我经常处理简单功能的UI和业务逻辑.这意味着我经常在业务逻辑和UI逻辑之间切换.
所以我所做的就是将命令(在这个例子中CustomerMovedCommand和实现中ICommandHandler<CustomerMovedCommand>)放在同一个文件中,首先是命令.因为命令本身是具体的(因为它是DTO没有理由抽象它)跳转到类很容易(在Visual Studio中为F12).通过将处理程序放在命令旁边,跳转到命令意味着跳转到业务逻辑.
当然,只有当命令和处理程序可以存在于同一个程序集中时,这才有效.当您的命令需要单独部署时(例如,在客户端/服务器方案中重用它们时),这将不起作用.
当然,这只是我业务层的45%.然而,另一个大的和平(比如45%)是查询,它们的设计类似,使用查询类和查询处理程序.这两个类也放在同一个文件中,它允许我快速导航到业务逻辑.
由于命令和查询大约是我业务层的90%,因此在大多数情况下,我可以非常快速地从表示层移动到业务层,甚至可以在业务层内轻松导航.
我必须说这是我在同一个文件中放置多个类的唯一两种情况,但使导航变得更容易.
如果你想了解我如何设计这个的更多信息,我写了两篇关于此的文章: