目录
Understanding Dependencies
What is a Dependency?
Why are Dependencies Bad?
Dependency Types
Additional Dependency Characteristics
Interface Implementation Dependencies
Compile-time and Runtime Dependencies
Visible and Hidden Dependencies
Direct and Indirect Dependencies
Unnecessarily Extensive Dependencies
Local and Context Dependencies
Standard vs. Custom Class/Interface Dependencies
Summary
当一个类A使用另一个类或接口B时,那么A依赖于B. A 如果没有B就不能工作,也不能被重新利用,在这种情况下,A叫做 "dependant"(受抚养者,依赖他人的人) and B叫做 "dependency"( 被依赖者,从属物). A dependant 依赖于它的dependencies.
像个相互使用的类叫做 "coupled"(耦合). 类之间的耦合可以是松散的,也可以是紧密的,或者介于两者之间.耦合的紧密性不是二元的.它既不“松”也不“紧”. 紧密度是连续的,而不是离散的. 您还可以将依赖关系描述为“强”或“弱”. 紧密耦合导致了强依赖性, 松散耦合导致弱依赖,甚至在某些情况下没有依赖关系.
Dependencies, or couplings, 依赖或者耦合性是单向的. A依赖B,并不意味着B也依赖A
Dependencies不好是因为它们减少了重用reuse. Decreased reuse有很多不好的地方. 重用通常对开发速度、代码质量、代码可读性等都有积极的影响。
一个例子可以很好地说明依赖关系是如何影响重用的:
你有一个 CalendarReader类从 XML 文件里面读取calendar event 事件列表. calendar arreader的实现概述如下:
public class CalendarReader { public List readCalendarEvents(File calendarEventFile){ //open InputStream from File and read calendar events. } }
readCalendarEvents方法有一个 File 对象作为参数. 因此,这个方法依赖于File类. 对File类的这种依赖意味着CalendarReader只能从文件系统中的本地文件中读取日历事件. 它无法从网络连接、数据库或类路径上的资源中读取事件文件. 您可以说,CalendarReader与File类紧密耦合,从而与本地文件系统紧密耦合。
下面是降低耦合的一种方法,把 File参数,改成 InputStream参数:
public class CalendarReader { public List readCalendarEvents(InputStream calendarEventFile){ //read calendar events from InputStream } }
一个输入流 InputStream 可以从 File 对象, a network Socket(网络套接字), a URLConnection 类, a Class object (Class.getResourceAsStream(String name)), (数据库)a column in a database via JDBC etc. 现在,CalendarReader不再耦合到本地文件系统。它可以从许多不同的来源读取事件文件。
参数为InputStream 的readCalendarEvents() 方法变得重用性更好与之前比较,与本地文件的耦合性解除了,变成和 InputStream 类耦合. InputStream 比 File 更灵活,但是也不意味着 CalendarReader 是可以 100% 重用的. 例如,它仍然不能轻松地从NIO通道读取数据。
依赖的类型有:
Class dependencies依赖于类.例如,下面代码框中的方法接受一个字符串作为参数。因此它依赖于String类。
public byte[] readFileContents(String fileName){ //open the file and return the contents as a byte array. }
Interface dependencies 依赖于接口. 例如,下面代码框中的方法接受一个CharSequence作为参数。CharSequence是一个标准的Java接口(in the java.lang package). CharBuffer、String、StringBuffer和StringBuilder都实现了CharSequence接口,所以这些类的任何实例都可以用作该方法的参数。
public byte[] readFileContents(CharSequence fileName){ //open the file and return the contents as a byte array. }
Method or field dependencies 依赖于对象的具体方法或字段. 不管对象的类是什么,或者它实现了什么接口,只要它有一个所需类型的方法或字段. 下面的方法演示了一个方法依赖项 method dependency. 该方法依赖于一个作为参数给出的对象的类(fileNameContainer)中"getFileName" 方法.注意,依赖项在方法声明中是不可见的:
public byte[] readFileContents(Object fileNameContainer){ Method method = fileNameContainer .getClass() .getMethod("getFileName", null); String fileName = method.invoke(fileNameContainer, null); //open the file and return the contents as a byte array. }
Method or field dependencies 通常使用反射或者目标方法或者字段.比如, Butterfly Persistence 使用反射检测类的getters 和 setters . 没有 getters and setters, Butterfly Persistence就不能读写数据库中的类,这样 Butterfly Persistence 依赖于 getters and setters. Hibernate (a similar ORM API) 可以直接使用 getters and setters, 或者访问字段fields ,也是通过反射.这样Hibernate要么有方法依赖项,要么有字段依赖项。
Method (or "function") dependencies 也可以在支持函数指针或方法指针作为参数传递给其他方法的语言中看到。例如,c#委托。
Dependencies can be compile-time, runtime, visible, hidden, direct, indirect, contextual etc.依赖项可以是编译时、运行时、可见的、隐藏的、直接的、间接的、上下文的等等
如果 class A 依赖于接口 I, 那么A不依赖于I的具体实现. 但是,A依赖于I的某个实现. 如果A不实现I中的方法,就不能工作. 因此,每当一个类依赖于一个接口时,这个类也依赖于一个实现。
接口的方法越多,开发人员为接口提供自己的实现的机会就越少,除非需要这样做.因此,一个接口拥有的方法越多,开发人员就越有可能坚持使用该接口的默认实现. 换句话说,一个接口变得越大、越复杂,它与默认实现的耦合就越紧密!
因为interface implementation dependencies, 您不应该盲目地向接口添加功能.如果功能可以封装在它自己的脚本中,在它自己的接口之后,尽量这样做:
下面就是一个例子。代码示例显示了一个分层树结构的树节点,The code example shows a tree node for a hierarchical tree structure.
public interface ITreeNode { public void addChild(ITreeNode node); public ListgetChildren(); public ITreeNode getParent(); }
假设您希望能够计算给定节点的后代. 首先,您可能想要向ITreeNode接口添加countents()方法. 但是,如果您这样做,任何想要实现ITreeNode接口的人也必须实现countent()方法。
相反,您可以实现一个派生计数器类,该类可以遍历一个ITreeNode实例,计算该实例的所有派生。这个派生计数器可以在ITreeNode接口的不同实现中重用. 您刚刚为用户省去了实现countents()方法的麻烦,即使他们需要实现ITreeNode接口!
可以在编译时解析的依赖项是编译时依赖项. 直到运行时才能解析的依赖项是运行时依赖项。与运行时依赖项相比,开发人员更容易看到编译时依赖项, 但有时运行时依赖关系可能更灵活.比如, Butterfly Persistence 在运行时检测类的getter和setter,并自动将它们映射到该类的数据库表. 这是将类映射到数据库表的一种非常简单的方法.
visible dependency 是开发人员可以从类接口看到的依赖项. 如果不能从类的接口看到依赖项,则它是一个隐藏的依赖项。 hidden dependency.
在之前的例子中,String 和 CharSequence是 readFileContents()方法的依赖项,是可见的依赖项.它们在方法声明中是可见的,方法声明是类接口的一部分. method dependencies 方法依赖类型的 readFileContents() 方法需要一个对象作为参数,是不可见invisible.您无法从接口查看readFileContents()方法是否调用了fileNameContainer.toString()来获取文件名,或者它实际上调用了getFileName()方法。
另一个隐藏依赖项的例子是对静态单例的依赖,或者来自方法内部的静态方法. 您无法从接口看到一个类是否依赖于静态方法或静态单例对象。
可以想象,隐藏的依赖关系是不好的。 对于使用具有隐藏依赖项的类的开发人员来说,很难检测到它们。他们只能通过检查代码来查看它们。
这并不等于说永远不要使用隐藏的依赖项.隐藏的依赖关系通常是提供合理的默认值 providing sensible defaults.例如,在这个例子中,它可能不是一个问题:
public class MyComponent{ protected MyDependency dependency = null; public MyComponent(){ this.dependency = new MyDefaultImpl(); } public MyComponent(MyDependency dependency){ this.dependency = dependency; } }
MyComponent
有一个隐藏的依赖 MyDefaultImpl
,. 但如果MyDefaultImpl没有任何危险的副作用, 那么这个隐藏的依赖就不是危险的
依赖项可以是直接依赖项,也可以是间接依赖项.如果a类使用B类,那么a直接依赖于B. 如果A依赖于B, B依赖于C,那么A间接依赖于C. 如果你不能在没有B的情况下使用A,也不能在没有C的情况下使用B,那么你也不能在没有C的情况下使用A。
间接依赖关系也称为链式依赖关系, or "transitive dependencies" (in "Better, Faster, Lighter Java" by Bruce A. Tate and Justin Gehtland)
有时组件依赖的信息比它们执行工作所需的信息还要多.例如,设想一个web应用程序的登录组件. 登录组件只需要一个用户名和一个密码,并将返回与这些匹配的user对象(如果有的话)。界面可以是这样的:
public class LoginManager{ public User login(HttpServletRequest request){ String user = request.getParameter("user"); String password = request.getParameter("password"); //read user and return it. } }
Calling the component would look like this:
LoginManager loginManager = new LoginManager(); User user = loginManager.login(request);
看起来很简单,对吧?即使登录方法需要更多的参数,也不需要更改调用代码。
但是登陆方法对 HttpServletRequest
接口有我称之为 "unnecessarily extensive dependency. . LoginManager只需要一个用户名和一个密码来查找一个用户,但是使用HttpServletRequest作为登录方法的参数. HttpServletRequest
包含许多 LoginManager
不需要的内容
依赖于 HttpServletRequest
接口造成两种问题:
HttpServletRequest
实例的情况下重用. 这造成测试 LoginManager
很困难. 你需要模拟一个HttpServletRequest
实例A much better interface for the LoginManager's login method would be:
public User login(String user, String password){ //read user and return it. }
But look what happens to the calling code now:
LoginManager loginManager = new LoginManager(); User user = loginManager.login( request.getParameter("user"), request.getParameter("password"));
它变得更加复杂。对于一个需要5个请求参数来完成其工作的组件,情况会更糟. 这是开发人员创建不必要的大量依赖项的主要原因。以简化调用代码。
在开发应用程序时,通常将应用程序拆分成小的组件. 其中一些组件是通用组件,在其他应用程序中也很有用. 其他组件是特定于应用程序的,在应用程序之外没有任何用途。
对于通用组件,属于组件(或API)的任何类都是“本地的”. 应用程序的其余部分是“上下文”。 "context". 应用程序的其余部分是“上下文”叫做 "context dependency". Context dependencies 是不好的,因为它使通用组件在应用程序之外也不可用。
. Context dependencies经常会出现在开发人员试图简化其应用程序的设计时,这方面的一个很好的例子是请求处理应用程序,比如消息队列连接的应用程序或web应用程序。
假设有一个应用程序以XML形式接收请求、处理请求并以XML形式发回结果. 在处理过程中,XML请求由几个单独的组件处理. 每个组件都需要不同的信息,其中一些信息是由早期组件生成的。每个组件都需要不同的信息,其中一些信息是由早期组件生成的。.然后,处理组件可以从该请求对象读取信息,并附加更多信息供以后的处理组件使用. 通过将此请求对象作为参数,每个请求处理组件都依赖于此请求对象。请求对象是特定于应用程序的,因此导致每个请求处理组件具有上下文依赖关系。
在许多情况下,组件最好依赖于标准Java(或c#等)包中的类或接口.任何人都可以使用这些类和接口,从而更容易地满足这些组件依赖关系。此外,这些类不太可能更改并导致应用程序编译失败。
但在某些情况下,依赖于JDK类并不是最好的做法.例如,假设一个方法需要4个字符串来进行配置. 然后你的方法接受4个字符串作为参数。例如,驱动程序名、数据库url、用户名和数据库连接所需的密码. 如果所有这4个字符串总是一起使用,那么对于该方法的用户来说,如果将这4个字符串分组到一个类中,可能会更清楚, 并传递类的实例,而不是4个字符串。
. 一般来说,接口依赖比类依赖更可取. 方法和字段依赖关系可能非常有用,但请记住,它们通常也是隐藏的依赖关系,而隐藏的依赖关系使组件的用户更难检测到它
Interface implementation dependencies比你想象的更普遍.通过让接口尽量简单来限制他们
Hidden dependencies 可能是危险的,但是由于运行时依赖项有时也是隐藏的依赖项,所以您不一定总能做出选择。
请记住,即使一个组件不直接依赖于另一个组件,它仍然可能间接依赖于另一个组件。虽然间接依赖关系的限制通常较少,但它们仍然是依赖关系。
尽量避免不必要的广泛依赖。但是请记住,当您将多个参数分组到一个类中时,经常会出现不必要的大量依赖项。这是一种常见的重构,通常是为了使代码看起来更简单,但是正如您现在看到的,这也会导致不必要的大量依赖。