耦合 - 除了更改方法签名或返回类型之外,更改一个模块如何影响另一个模块?

Coupling - How does changing one module effect another OTHER THAN changing method signature or return type?

提问人:lynxx 提问时间:11/12/2019 最后编辑:kaya3lynxx 更新时间:11/24/2019 访问量:1487

问:

在高耦合环境中,更改一个模块会影响另一个模块。好的,但我看不出这怎么可能(除了更改方法签名或返回类型)?

好吧,如果我更改一个类,那么只有在以下情况下,它才能破坏其他类中的代码:

  1. 如果我突然更改方法的返回类型 - 那么我将不得不转到另一个类并修复类型的不匹配。
  2. 如果我更改方法签名 - 那么我将不得不转到所有依赖类,并在调用更改方法的任何地方更改方法参数。

出于同样的原因,最好依赖于抽象(接口),这样我们就可以保证定义的方法将在那里。

除此之外,更改一个类还如何影响另一个依赖类?

OOP 设计模式 接口 依赖 与语言无关

评论

0赞 NathanOliver 11/12/2019
想一想,如果你改变了一个班级的大小,会发生什么。需要更新什么?
0赞 lynxx 11/12/2019
@NathanOliver-恢复莫妮卡我不是很有经验。我不知道。需要更新什么?
0赞 Jarod42 11/12/2019
更改行为也会影响耦合类。;-)
2赞 Öö Tiib 11/12/2019
您想要有关 Java 或 C++ 的列表吗?这些语言具有不同的功能。
1赞 Jarod42 11/12/2019
返回 null 指针而不是 NullObject 可能会导致空指针异常/崩溃/UB。

答:

-1赞 Mark Kerr 11/12/2019 #1

假设您有两个类:和 .我们还假设它与其他字段之间存在依赖关系,并使用了 B 的一些计算:Class AClass BClass AClass B

class A {
  private C myC;
  private B myB;
  ...
  private void someMethod() {
    String foo = myB.bar();
    myC.setField(foo);
  }
}

如果你在 B 中更改了直接(或间接)影响 的结果,那么你可能会在 内部产生各种影响,以及 。这不一定是返回类型的更改,它可能会导致不正确的计算,而这两者都取决于bar()ACAC

请注意,此示例显示了紧密耦合,因此这些类中的任何一个都可能发生变化,这些变化很容易影响其他类,而您不一定意识到。

评论

0赞 lynxx 11/16/2019
您能否更具体地说明副作用和不正确的计算。你的答案看起来像一个更笼统的答案。
3赞 Rishikesh Darandale 11/16/2019 #2

假设您是 A 类的作者,该类按对象的任何属性按升序对对象进行简单排序。

例如,B 类和 C 类使用该功能以某种方式处理列表。B 类是严格的,它只想按升序对对象进行排序。c类,除特定顺序的列表外。

现在,您在类 A 中进行更改,该功能将返回基于对象的相同/其他属性以降序方式排序的对象。

在这里,B类会说你改变了合同,需要调整它的进一步处理。

C 类在这里没问题,因为它不打扰排序。

因此,更改逻辑也会影响使用现有功能的客户端。因此,在编写任何独立的模块/库版本控制时都会完成。

1赞 Matt Timmermans 11/17/2019 #3

你对耦合问题的思考方式并不完全正确。这并不是说必须更改一个模块,因为您更改了另一个模块。这是关于必须更改许多模块才能完成单个任务。

在松散耦合的环境中,每个模块都有一个明确定义的作业。控制它的人的头脑中有一些每个模块负责的事情,所以当有人想要一些小东西(大需求分解成小需求)时,它可以通过改变一个或最多几个模块来完成。

在高度耦合的环境中,每个模块的用途很难定义。当它被记录下来时,文档会使用很多词,比如“帮助”或“允许这个其他模块”等。人们想要的实际东西是通过多个模块之间的协调行为来实现的,甚至要知道一个模块的行为正确,你必须考虑多个模块。

我希望你能理解为什么维护一个高度耦合的系统是一场噩梦,我希望你想写松散耦合的系统。

其中一个重要部分是依赖于抽象。这些抽象通常由接口定义,但重要的不是它们是接口,而是它们是抽象的。很容易构建一个高度耦合的系统,其中一切都依赖于接口。然后,当你必须进行更改时,你会发现除了使用它们的模块之外,还更改了很多接口。

5赞 root 11/17/2019 #4

尽管这被标记为“Java”,但我将给出一个更广泛的答案。

耦合表示为模块 A 在使用模块 B 时所做的假设。模块 A 所做的假设越多,它与模块 B 的耦合就越多。

模块的 API 基本上是其使用者可以做出的一组假设。返回类型或方法签名只是编译器可以验证的假设。

例如,《模拟城市》与 Windows 3.x 内存分配器相结合。内存分配器有一个合约,上面写着你可以假设 X、Y 和 Z,仅此而已。不能假定任何其他行为。但是《模拟城市》假设了这一点,因此,他们无法更改 Windows 95 中的内存分配器(好吧,不是在《模拟城市》执行时)。

显然,这不是有意的耦合——任何关于模块超出其接口的假设都应该是无意的,但这仍然是一个假设。

其他示例包括:

  • 一个密码哈希 API,用于告知其使用者哈希算法是什么。然后,使用者可以将哈希密码的长度硬编码到数据库架构中,这意味着只有在数据库迁移后才能将其更改为具有更长哈希值的更安全的算法。
  • 每个使用 protobuf 的人不仅要与它的 API 相结合,还要与它的连线格式相结合。如果 Google 想要更改连线格式,它将破坏生产者或消费者尚未升级的每个部署。
  • 如果你使用 GCC 的 GNU 编译器扩展,那么你就与 GCC 耦合了。如果你想(或必须)使用另一个编译器,你最好希望它们也碰巧实现相同的扩展。
  • 每个程序都与其编程语言耦合。例如,如果 Oracle 想要更改 Java,以便函数重载是不可能的,例如,由于一些每 100 年才出现一次的致命设计错误,所有 Java 程序都将停止编译。
  • 如果你编写一个 POSIX 程序,你依赖于 POSIX 的实现。如果你的 POSIX 程序使用了 POSIX basename(3),但你的实现破坏了它的语义,那么你的程序就不能在这个实现上工作(GNU 引入了一个兼容性宏来避免这种情况)。
1赞 Ori Marko 11/20/2019 #5

即使接口没有改变,实现也很容易打破你的期望,例如:

旧的实现方法

public void doStuff(){
    //real doing stuuf...
}

新的实现方法

public void doStuff(){
    throw new UnsupportedOperationException("Stop supporting doing stuff");
}

这就是为什么在升级依赖项模块时需要进行回归测试的原因

此外,您的依赖项模块还有其他可能中断的依赖项,例如,在龙目岛更改中使用杰克逊依赖项中断

1赞 Honza Zidek 11/22/2019 #6

一个特别危险的依赖关系是,如果一个类的内部实现在内部调用同一类的公共方法。

作为一个现实生活中的例子(这个问题确实发生在 Collection 框架中,vs. implementation),想象一个具有 s 内部存储的类,具有两个公共方法: 和 :HashSet.addAll()ArrayList.addAll()ContainerIntegeraddMultiple()addSingle()

实现 1

class public Container {
    private Integer[] storage = new Integer[0];

    public void addMultiple(List<Integer> numbers) {
        // Simple, but slow implementation
        for (Integer number : numbers) {
            // !!! Here is the hidden problem !!!
            addSingle(number);
        }
    }

    public void addSingle(Integer number) {
        storage = copyOf​(storage, storage.length + 1)
        storage[storage.length - 1] = number;
    }
}

现在,您想要添加一些功能,例如在将每个数字添加到容器之前将每个数字乘以 2。你决定像这样用愚蠢的方式扩展类:Container

class public DoubleContainer extends Container {
    @Override
    public void addSingle(Integer number) {
        super addSingle(number * 2);
    }
}

你实现你的 while 有实现 1。它的工作方式正如您所期望的那样,每个数字都加了一倍的天气,带有或带有。DoubleContainerContaineraddSingle()addMultiple()

然后突然间,他们变成了实现 2。

实现 2

class public Container {
    private Integer[] storage = new Integer[0];

    public void addMultiple(List<Integer> numbers) {
        // New, improved, faster implementation
        int position = storage.length;
        storage = copyOf​(storage, storage.length + numbers.getSize())
        for (Integer number : numbers) {
            storage[position++] = number;
        }
    }

    public void addSingle(Integer number) {
        storage = copyOf​(storage, storage.length + 1)
        storage[storage.length - 1] = number;
    }
}

:你的代码坏了。通过添加的数字不会加倍。所以你修复它:你还覆盖了方法:addAll()addAll()

class public DoubleContainer extends Container {
    @Override
    public void addMultiple(List<Integer> numbers) {
        List<Integer> doubledNumbers = numbers.stream()
            .map(number -> number * 2)
            .collect(Collectors.toList());
        super addMultiple(doubledNumbers);
    }

    @Override
    public void addSingle(Integer number) {
        super addSingle(number * 2);
    }
}

但是你也不能安然入睡——如果他们再次恢复到旧的实现怎么办?然后数字将翻两番......

如您所见,实现 2 没有更改公共方法。甚至可以有一个接口,它不会改变大小写。

郑重声明:我并不建议以这种方式使用继承(有关更多信息,请阅读 Joshua BlochEffective Java第 16 项:支持组合而不是继承)!但是,您的问题是“更改一个类还如何影响另一个相关类?”这是可能的答案之一。

0赞 hooknc 11/24/2019 #7

其他答案中没有提出的东西......

如果 a 直接调用另一个 .更改时,必须重新编译,并且由于与 B 直接耦合,因此也必须重新编译。Class Aclass BClass BClass AClass A

这降低了两个类的独立部署能力。

这意味着,如果 B 被更改,则必须重新编译 A 才能正常工作。

这来自 Uncle Bob,你可以在他的一篇博文 Microservices 和 Jars 中读到它。