我刚刚遇到了设计松耦合软件架构的Inversion of Control方法(使用依赖注入实现)。根据我的理解,IOC方法旨在通过在另一个类中实例化一个类的对象来解决与类之间紧密耦合相关的问题,理想情况下不会发生这种情况(根据模式)。我的理解在这里是否正确?
如果以上是真的,那么关于组成或有关系(OO的非常基本的重要方面)。举个例子,我使用已定义的链表类编写我的堆栈类,所以我在堆栈类中实例化了一个链表类。但是根据IOC,这将导致紧密耦合,从而导致糟糕的设计。这是真的?我在组合或有关系和IOC之间有点困惑。
答案 0 :(得分:4)
根据我的理解,IOC方法旨在解决与问题相关的问题 通过实例化一个对象来实现类之间的紧密耦合 另一个类中的类,理想情况下不应该发生(按照 图案)。我的理解在这里是否正确?
关闭,但你稍微偏了。当您在类(Java中的接口)之间定义契约时,会解决紧耦合问题。由于您需要实现合同(接口),因此必须在某些时候提供这些实现。 IoC是一种提供实现的方式,但不是唯一的方式。所以紧耦合实际上与控制反转正交(意味着它没有直接关联)。
更具体地说,您可以使用松散耦合但不能使用IoC。 IoC部分是实现来自组件外部。考虑您定义使用接口实现的类的情况。当您测试该类时,您可能会提供模拟。当您将模拟传递给被测试的类时,您没有使用IoC。但是,当您启动应用程序时,IoC容器决定将哪些内容传递给您的班级,就是IoC 。
举个例子,我使用链表类编写我的堆栈类 已定义,所以我在我的堆栈中实例化链接列表类 类。但是根据IOC,这将导致紧密耦合,因此a 糟糕的设计。这是真的?我在构图之间有点困惑 或者有一个关系和IOC。
是和否。从一般意义上讲,您不需要完全抽象应用中的所有功能。你可以和纯粹主义者一样,但这可能是乏味和过度的。
在这种情况下,您可以将堆栈视为黑盒子,而不是使用IoC管理它。请记住,Stack本身是松散耦合的,因为Stack的行为可以被抽象掉。另外,请考虑以下两个定义
class StackImpl implements Stack {
private List backingList
VS
class StackImpl implements Stack {
private LinkedList backingList
第一个优于第二个,正是因为它更容易更改List实现;即你已经提供松耦合。
就我所说的而言。此外,如果您正在使用合成,您当然可以配置大多数IoC容器(如果不是全部)将内容传递给构造函数或调用setter,这样您仍然可以拥有 has-A 关系。
答案 1 :(得分:3)
IoC的良好实现可以实现“有一个”模式,但只是抽象了孩子的实现。
例如,根据您的设计,每个业务层类都可以“拥有”异常处理程序;使用IoC,您可以定义它,以便在运行时实际获得实例化的异常处理程序在不同的环境中是不同的。
IoC的最大价值在于您是否进行了大量的自动化测试;在这些场景中,您可以在测试环境中实例化模拟数据访问组件,但在生产中实例化实际数据访问组件,这样可以保持测试的清洁。 IoC的缺点是调试起来比较困难,因为一切都比较抽象。
答案 2 :(得分:1)
堆栈示例中的紧耦合来自于对特定列表类型进行实例化的堆栈。 IOC允许堆栈类型的创建者提供要使用的精确列表实现(例如,用于性能或测试目的),实现堆栈不(至少不应该)关心列表的确切类型是什么,只要它有一个特定的接口(堆栈想要使用的方法),而concetere实现提供了所需的语义(例如,遍历列表将按照添加顺序访问添加到列表中的所有元素。)
答案 3 :(得分:1)
我也怀疑我对控制反转的理解。 (似乎是一个很好的OO设计原则的应用给出了一个奇特的名字)所以,让我假设你是一个初学者,分析你的例子并澄清我在路径上的想法。
我们应该首先定义一个接口IStack
。
interface IStack<T>
{
bool IsEmpty();
T Pop();
void Push(T item);
}
在某种程度上我们已经完成了;其余的代码可能不关心我们是用链表,数组还是其他方法实现的。 StackWithLinkedList : IStack
和StackWithArray : IStack
的行为相同。
class StackWithLinkedList<T> : IStack<T>
{
private LinkedList<T> list;
public StackWithLinkedList<T>()
{
list = new LinkedList<T>();
}
}
所以StackWithLinkedList
完全拥有list
;它不需要外部任何帮助来构建它,它不需要任何灵活性(该行永远不会改变)并且StackWithLinkedList
的客户端可能不在乎(他们无法访问列表)。简而言之,这不是讨论控制反转的好例子:我们不需要任何。
让我们讨论一个类似的例子PriorityQueue<T>
:
interface IPriorityQueue<T>
{
bool IsEmpty();
T Dequeue();
void Enqueue(T item);
}
现在我们遇到了一个问题:我们需要比较T
类型的项目,以提供IPriorityQueue
的实现。客户端仍然不关心我们是使用数组,堆还是其他内容,但他们确实关心我们如何比较项目。我们可能需要T
来实施IComparable<T>
,但这将是一项不必要的限制。我们需要的是一些功能,可以根据我们的请求比较T
个项目:
class PriorityQueue<T> : IPriorityQueue<T>
{
private Func<T,T,int> CompareTo;
private LinkedList<T> list;
//bla bla.
}
这样:
如果CompareTo(left,right) < 0
则离开&lt;对(在某种意义上)
如果CompareTo(left,right) > 0
则离开&gt;对(在某种意义上)
如果CompareTo(left,right) = 0
然后左=右(在某种意义上)
(我们还要求CompareTo
保持一致,等等,但这是另一个主题)
问题是如何初始化CompareTo
。
一个选项可能是,-let假设在某处有一个通用的比较创建者 - 使用比较创建者。 (我同意,这个例子变得有点傻了)
public PriorityQueue()
{
this.CompareTo = ComparisonCreator<T>.CreateComparison();
this.list = new LinkedList<T>();
}
或者甚至可能是:ServiceLocator.Instance.ComparisonCreator<T>.CreateComparison();
由于以下原因,这不是理想的解决方案:
PriorityQueue
现在(非常不必要地)依赖于ComparisonCreator
。如果它在不同的组件上,则必须引用它。如果有人更改ComparisonCreator
,则必须确保PriorityQueue
不受影响。
客户将很难使用PriorityQueue
。他们首先需要确保构造并初始化ComparisonCreator
。
客户将很难更改默认行为。假设某个客户端需要不同的CompareTo
函数。没有简单的解决方案。例如,如果它更改了ComparisonCreator<T>
的行为,则可能会影响其他客户端。如果有其他线程怎么办?即使在单线程环境中,客户端也可能需要撤消构造上的更改。要使其发挥作用,需要付出太多努力。
出于同样的原因,很难对PriorityQueue
进行单元测试。人们需要建立整个环境。
当然, - 当然你一直都知道这一点 - 在这个具体问题上有一个更简单的方法。只需在构造函数中提供CompareTo
函数:
public PriorityQueue(Func<T,T,int> CompareTo)
{
this.CompareTo = CompareTo;
this.list = new LinkedList<T>();
}
我们来看看:
PriorityQueue
独立于ComparisonCreator
。
对于客户来说,使用PriorityQueue
可能要容易得多。他们可能需要提供CompareTo
函数,但在最坏的情况下,他们总是可以询问ServiceLocator
,所以至少它不会更难。
更改默认行为非常简单。只需提供不同的CompareTo
功能。一个客户做什么,不会影响其他客户。
单元测试PriorityQueue
非常容易。没有复杂的环境可供设置。我们可以使用不同的CompareTo
函数等轻松测试它。
我们所做的是“构造函数注入”,因为我们在构造函数中注入了一个依赖项。通过在构造中提供所需的依赖性,我们能够将PriorityQueue
更改为“自给自足”类。我们仍然在LinkedList<T>
示例中创建了Stack
,这是构造中的具体类,原因相同:它不是真正的依赖。
答案 4 :(得分:1)
根据我的理解,IOC方法旨在解决与问题相关的问题 通过实例化一个对象来实现类之间的紧密耦合 另一个类中的类,理想情况下不应该发生(按照 图案)。我的理解在这里是否正确?
IoC实际上是quite a broad concept,所以让我们将字段限制为您所指的依赖注入方法。是的,依赖注入做了你说的。
我认为hvgotcodes
认为你稍微偏离的原因是紧耦合的概念可以被认为是具有多个层次。对接口进行编程是从特定实现中抽象出来的方法,它可以保留一些客户端代码与之交互的代码片段的使用,并使其实现松散耦合。
必须在某处创建(实例化)实现:即使您编程到接口,如果在客户端代码中创建实现,您也会绑定到该特定实现。
因此,我们可以从界面中抽象出实现,但我们也可以抽象出要使用哪种实现的选择。
一旦明确这个细节,你就必须问自己什么时候抽象实现的选择是有意义的,这基本上是软件工程的基本问题之一:什么时候应该抽象什么?这个问题的答案当然取决于背景。
但是根据国际奥委会的说法,这将导致紧密耦合,从而导致不良 设计。这是真的吗?
如果紧耦合设计不好,为什么还要依赖标准的Java类?我们实际上需要区分stable and volatile dependencies。
引用您的示例,如果您使用列表的标准实现,您可能不希望将此依赖项注入您的类。这样做你会得到什么?您是否希望列表的标准实现能够很快改变,或者您是否希望能够注入标准列表的不同实现?
另一方面,假设您有一个带有某种更改跟踪机制的自定义列表,以便您可以对其执行撤消和重做操作。现在注入它是有意义的,因为您可能希望能够单独测试客户端类,而不会产生自定义列表实现的潜在错误。
如你所见,紧密耦合并不总是坏事,有时它是有意义的,有时候应该避免:最终归结为依赖类型。