如何理解松耦合应用程序中的大局?
我们一直在使用松耦合和dependency injection开发代码。
很多“服务”风格的类有一个构造函数和一个实现接口的方法。 每个单独的课程都很容易理解。
但是,由于耦合的松散,查看课程并不能告诉你周围的课程或者它适合放大图片的地方。
使用Eclipse跳转到协作者并不容易,因为您必须通过接口进行操作。 如果界面是可运行的,那么对于find哪个类实际插入没有任何帮助。实际上,有必要回到DI容器定义,并尝试从那里解决问题。
以下是一个从dependency injection服务类的代码行:
// myExpiryCutoffDateService was injected, Date cutoff = myExpiryCutoffDateService.get();
这里的耦合尽可能松散。 到期date以任何方式字面上实施。
以下是在一个更加耦合的应用程序中可能看起来像什么。
ExpiryDateService = new ExpiryDateService(); Date cutoff = getCutoffDate( databaseConnection, paymentInstrument );
从紧密耦合的版本,我可以推断,截止date是由支付工具使用数据库连接以某种方式确定。
我发现第一种风格的代码比第二种风格的代码更难理解。
你可能会争辩说,在阅读这门课时,我不需要知道截止date是如何计算出来的。 确实如此,但是如果我正在缩小一个bug或者确定增强需要插入的位置,那么这是非常有用的信息。
有谁遇到这个问题? 你有什么解决scheme? 这只是一些适应? 是否有任何工具可以让class级连线的方式可视化? 我应该让课程更大还是更融合?
(有意留下这个问题容器不可知的,因为我有兴趣在任何答案)。
虽然我不知道如何在一个段落中回答这个问题,但我试图在博客文章中回答这个问题: http : //blog.ploeh.dk/2012/02/02/LooseCouplingAndTheBigPicture.aspx
总而言之,我发现最重要的一点是:
- 理解松散耦合的代码库需要不同的思维模式 。 虽然很难“跳入合作者”,但也应该或多或less地不相关。
- 松耦合就是不了解整体就理解一个部分 。 你应该很less需要同时理解这一切。
- 在对一个bug进行调整时,应该依靠堆栈跟踪而不是代码的静态结构来了解协作者。
- 编写代码的开发人员有责任确保它易于理解 – 这不是开发人员阅读代码的责任。
一些工具知道DI框架,并知道如何解决依赖关系,让您以自然的方式导航您的代码。 但是,如果没有这个function,只需要尽可能使用IDE提供的任何function即可。
我使用Visual Studio和一个定制的框架,所以你描述的问题是我的生活。 在Visual Studio中,SHIFT + F12是我的朋友。 它显示对光标下的符号的所有引用。 过了一段时间,你已经习惯了通过你的代码进行必要的非线性导航,并且根据“哪个类实现这个接口”和“注入/configuration站点在哪里”这个术语来考虑是第二性质的,所以我可以看到哪个类正被用来满足这个接口依赖“。
也有VS的扩展提供UI增强来帮助,比如生产力动力工具 。 例如,您可以将鼠标hover在界面上,会popup一个信息框,您可以单击“实施者”来查看实现该界面的解决scheme中的所有类。 你可以双击跳转到任何这些类的定义。 (我仍然通常只是使用SHIFT + F12)。
我刚刚就此事进行了内部讨论,最后写了这篇文章,我认为这太好了,不能分享。 我几乎是在未经编辑的情况下复制它,但即使这是一个更大的内部讨论的一部分,我认为它大部分可以独立。
讨论的内容是介绍一个名为IPurchaseReceiptService
的自定义接口,以及是否应该使用IObserver<T>
来replace它。
那么,我不能说我有强大的数据点关于这一点 – 这只是我正在追求的一些理论…然而,我现在对于认知开销的理论是这样的:考虑你的特殊IPurchaseReceiptService
:
public interface IPurchaseReceiptService { void SendReceipt(string transactionId, string userGuid); }
如果我们将它保留为当前的Header接口 ,它只有一个SendReceipt
方法。 这很酷。
有什么不是很酷的,你必须提出一个界面的名称,另一个名称的方法。 两者之间有一些重叠:单词Receipt出现两次。 IME,有时候这种重叠可能会更加明显。
此外,接口的名称是IPurchaseReceiptService
,这也不是特别有用。 服务后缀本质上是新的经理 ,是IMO,一个devise的气味。
另外,您不仅需要命名接口和方法,还必须在使用该variables时命名该variables:
public EvoNotifyController( ICreditCardService creditCardService, IPurchaseReceiptService purchaseReceiptService, EvoCipher cipher )
在这一点上,你基本上说过三次同样的事情。 根据我的理论,这就是认知开销,以及devise可以并且应该更简单的气味。
现在,将其与使用像IObserver<T>
这样的着名接口进行对比:
public EvoNotifyController( ICreditCardService creditCardService, IObserver<TransactionInfo> purchaseReceiptService, EvoCipher cipher )
这可以让你摆脱官僚作风,减lessdevise的核心问题。 您仍然有意向 – 揭示命名 – 您只将devise从“ types名称”angular色提示转换为“ 参数名称angular色提示” 。
当谈到关于“不连贯性”的讨论时,我毫不IObserver<T>
使用IObserver<T>
会神奇地使这个问题消失,但是我还有另外一个理论。
我的理论是,许多程序员发现编程接口非常困难的原因正是因为他们习惯于Visual Studio的定义function(顺便说一句,这是工具如何腐蚀头脑的又一个例子)。 这些程序员永远处于一种心境,他们需要知道接口的另一端是什么。 为什么是这样? 难道是因为抽象很差?
这与RAP相关联 ,因为如果你确认程序员的信念,即在每个接口背后都有一个特定的实现,那么他们就认为接口是唯一的。
但是,如果你应用了RAP,我希望程序员能够慢慢地学习,在特定的接口背后,可能会有这个接口的任何实现,并且它们的客户代码必须能够处理该接口的任何实现而不改变其正确性系统。 如果这个理论成立,我们刚刚将Liskov替代原则引入到代码库中,而不会吓到任何人都不懂的高眉概念:)
但是,由于耦合的松散,查看课程并不能告诉你周围的课程或者它适合放大图片的地方。
这是不准确的。对于每个类,您都知道该类依赖于哪种types的对象,以便能够在运行时提供其function。
你知道他们,因为你知道什么对象是预计注入。
你不知道的是将在运行时注入的实际的具体类,它将实现你知道你的类依赖的接口或基类。
所以,如果你想看看注入的实际类是什么,你只需要查看该类的configuration文件就可以看到注入的具体类。
您也可以使用IDE提供的工具。
既然你指的是Eclipse,那么Spring就有一个插件,还有一个可视化选项卡来显示你configuration的bean。 你检查了吗? 不是你在找什么?
另外请参阅Spring论坛上的相同讨论
更新:
再次读你的问题,我不认为这是一个真正的问题。
我的意思是这样的。
像所有的事情一样, loose coupling
不是万能的,本身就有其缺点。
大多数倾向于集中于好处,但作为任何解决scheme它有它的缺点。
在你的问题中你所做的是描述它的一个主要缺点,那就是它确实不容易看到大局,因为你有一切可configuration和插入的东西 。
还有其他的缺点,比如说,它可能比紧密耦合的应用慢,并且仍然是真实的。
在任何情况下,重复迭代,您在问题中描述的内容都不是您所遇到的问题,并且可以find一个标准解决scheme(或者任何此类方法)。
这是松散耦合的缺点之一, 你必须决定这个成本是否高于你实际获得的成本,就像在任何devise决策中一样。
这就像问:
嘿,我正在使用这种名为Singleton
模式。 它工作得很好,但我不能创build新的对象!我怎么能得到这个问题arround这家伙?
那么你不能; 但如果你需要,也许单身不适合你….
有一件事帮助我把多个密切相关的类放在同一个文件中。 我知道这违背了一般的build议(每个文件有一个类),我一般同意这一点,但在我的应用程序体系结构中,它工作得很好。 下面我会试着解释在这种情况下。
我的业务层架构是围绕业务命令的概念而devise的。 命令类(简单的DTO只有数据,没有行为)被定义,每个命令都有一个“命令处理程序”,它包含执行这个命令的业务逻辑。 每个命令处理程序都实现通用ICommandHandler<TCommand>
接口,其中TCommand是实际的业务命令。
消费者依赖ICommandHandler<TCommand>
并创build新的命令实例,并使用注入的处理程序来执行这些命令。 这看起来像这样:
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
应该对实现一无所知,但是在开发过程中,我(作为开发人员)对于执行的实际业务逻辑非常感兴趣,仅仅是因为开发是在垂直切片中完成的; 这意味着我经常在简单function的UI和业务逻辑上工作。 这意味着我经常在业务逻辑和UI逻辑之间切换。
所以我所做的就是把命令(在这个例子中是CustomerMovedCommand
和ICommandHandler<CustomerMovedCommand>
的实现)放在同一个文件中,首先使用命令。 因为命令本身是具体的(因为它是一个DTO,所以没有理由将其抽象出来)跳到类是很容易的(在Visual Studio中是F12)。 通过在命令旁边放置处理程序,跳转到命令意味着也跳转到业务逻辑。
当然这只有在命令和处理程序可以在同一个程序集中时才有效。 当你的命令需要单独部署时(例如在客户机/服务器场景中重新使用它们时),这将不起作用。
当然,这只是我业务层的45%。 然而,另一个大的和平(比如说45%)是查询,而且它们的devise也是类似的,使用查询类和查询处理程序。 这两个类也被放置在相同的文件中,使我能够快速导航到业务逻辑。
由于命令和查询大约是我业务层的90%,因此在大多数情况下,我可以从表示层到业务层快速移动,甚至可以在业务层中轻松导航。
我必须说,这是我在同一个文件中放置多个类的唯一两种情况,但是使得导航更容易。
如果你想更多地了解我如何devise这个,我写了两篇关于这个的文章:
- 同时…在我的架构的命令方面
- 同时…在我的架构的查询方面
在我看来,松耦合的代码可以帮助你很多,但我同意你的可读性。 真正的问题是方法的名称也应该传达有价值的信息。
这就是领域驱动devise ( http://domaindrivendesign.org/node/113 )所述的意图 – 揭示界面原则。
你可以重命名get方法:
// intention revealing name Date cutoff = myExpiryCutoffDateService.calculateFromPayment();
我build议你仔细阅读DDD原则,你的代码可以变得更可读,更易于pipe理。
我发现大脑在开发中作为一个节点映射工具是有用的。 如果你写一些脚本来parsing你的源代码,Brain接受,你可以很容易地浏览你的系统。
秘诀就是在你想要跟踪的每个元素的代码注释中joinguid,然后可以点击The Brain中的节点将你引导到你的IDE中。
取决于有多less开发人员在项目上工作,以及是否要在不同的项目中重用其中的一些部分,松耦合可以帮助您很多。 如果你的团队很大,项目需要跨越几年,松耦合可以帮助你更轻松地将工作分配给不同的开发团队。 我使用Spring / Java与大量的DI和Eclipse提供了一些图来显示依赖关系。 使用F3打开光标下的类有很多帮助。 正如以前的文章所述,知道你的工具快捷方式将帮助你。
另一件要考虑的事情是创build自定义类或包装,因为它们比您已经拥有的常见类更容易跟踪(如Date)。
如果您使用多个模块或应用程序层,那么了解项目stream是什么是一个挑战,因此您可能需要创build/使用一些自定义工具来查看每个事物是如何相互关联的。 我为自己创造了这个 ,它帮助我更容易理解项目结构。
文档!
是的,你指定了松耦合代码的主要缺点。 如果您已经意识到最终会得到回报,确实find修改的“地点”总是比较长,而您在find“正确的位置”之前可能需要打开几个文件。 ..
但那是一件非常重要的事情:文档。 很奇怪,没有一个答案明确提到,这是所有大型发展的一个主要要求。
API文档
具有良好searchfunction的APIDoc。 每个文件和 – 每个方法都有一个明确的描述。
“大图”文档
我认为有一个wiki可以解释这个大局面是很好的。 鲍勃做了一个代理系统? 如何运作? 它处理身份validation? 什么样的组件将使用它? 不是一个完整的教程,但只是一个地方,当你可以阅读5分钟,弄清楚什么组件是如何连接在一起的。
我同意Mark Seemann的所有答案,但是当你第一次参加一个项目的时候,即使你理解了解决原则的方法,你也许需要很多的猜测或者某种帮助找出在哪里实现你想要开发的特定function。
…再一次:APIDoc和一个小发展的维基。
令人震惊的是,没有人写过关于松散耦合代码的可testing性(当然是unit testing)以及紧耦合devise的不可testing性(相同的条件下)! 你应该select哪种devise是没有道理的。 今天,所有的模拟和覆盖范围的框架,至less对我来说是显而易见的。
除非你没有对你的代码进行unit testing,或者你认为你做了这些testing,但是事实上你并不这么做……隔离testing几乎不能通过紧密耦合来实现。
您认为您必须浏览IDE中的所有依赖关系? 忘掉它! 这与编译和运行时的情况是一样的。 在编译过程中几乎找不到任何错误,除非testing它,否则不能确定它是否正常工作,也就是说执行它。 想知道界面背后是什么? 放一个断点,运行该死的应用程序。
阿门。
…评论后更新…
不知道它是否会为您服务,但在Eclipse中有一些称为层次结构视图。 它向您展示了项目中所有接口的实现(不确定工作区是否也是如此)。 您可以导航到界面,然后按F4键。 然后它将向您展示实现该接口的所有具体和抽象类。