面向对象范式中,松耦合和紧耦合有什么区别?

346

能否描述面向对象范式中松耦合和紧耦合的确切区别?


7
什么是"松耦合(loose coupling)"?请提供例子。 - Matthew Flaschen
16个回答

428

紧耦合是指一组类高度依赖于彼此。

当一个类承担过多职责或一个关注点分散在多个类中而不是有自己的类时,就会出现这种情况。

松耦合是通过促进单一责任和关注点分离的设计来实现的。

一个松耦合的类可以独立于其他(具体)类进行使用和测试。

接口是用于解耦的强大工具。类可以通过接口而不是其他具体类进行通信,任何类都可以通过实现该接口来成为该通信的另一端。

紧耦合的示例:

class CustomerRepository
{
    private readonly Database database;

    public CustomerRepository(Database database)
    {
        this.database = database;
    }

    public void Add(string CustomerName)
    {
        database.AddRow("Customer", CustomerName);
    }
}

class Database
{
    public void AddRow(string Table, string Value)
    {
    }
}

松散耦合的示例:

class CustomerRepository
{
    private readonly IDatabase database;

    public CustomerRepository(IDatabase database)
    {
        this.database = database;
    }

    public void Add(string CustomerName)
    {
        database.AddRow("Customer", CustomerName);
    }
}

interface IDatabase
{
    void AddRow(string Table, string Value);
}

class Database implements IDatabase
{
    public void AddRow(string Table, string Value)
    {
    }
}

另一个例子在这里


2
观察者模式在这里被描述:http://en.wikipedia.org/wiki/Observer_pattern。由于Subject类可以维护一个从“观察者”继承的类列表,而不实际知道这些类的具体类型,因此这是一种松耦合的实例。主题不依赖于任何观察者或其内部问题。观察者也不依赖于主题或其任何问题。 - Jonathan
6
Java接口是有助于此的工具,但并非必需。"针对接口而非实现编程"的概念意味着针对公共方法/属性(或那些在语言不支持访问修饰符(如C)时应由外部调用者使用的方法/属性)进行编程。接口应该遵守这个隐含的契约,不改变其本身。在没有访问修饰符的语言中(如C),它意味着只使用已发布的接口/函数,并且不使用那些旨在内部使用的接口/函数,因为这些接口可能会根据需要更改以支持已发布的函数。 - Bill Rosmus
5
谢谢你的信任。这两段代码实现了相同的功能,但是它们之间有什么区别呢?也就是说,松耦合有哪些优点? - BenKoshy
1
我之前(基本上)看过这个例子。在我看来,只有当要使用许多类型的数据库时才会有影响。如果您计划只使用一个,则直接使用实现是否那么糟糕,然后稍后如果需要注入不同类型,则重构对数据库的单个使用以使用接口?而不是添加大量可能不需要的样板代码? - Carlos Bribiescas
8
我有点困惑为什么这个答案被接受并得到了这么多赞。一般的解释很基础且抽象,就像维基百科的总结。示例没有展示出任何内容。"紧耦合"的示例似乎没有任何问题,而"松耦合"的示例似乎没有解决任何问题。换句话说,添加接口除了增加复杂性外,还有什么作用? - rkedge
显示剩余3条评论

275

没有任何代码的解释

用简单的类比来解释这个概念。代码可以稍后再来。

松耦合的摘要示例:

The Hat is "loosely coupled" to the body. This means you can easily take then hat off without making any changes to the the person/body. Picture Attribution: https://pixabay.com/en/greeting-cylinder-chapeau-dignity-317250/

在上面的图片中,帽子与身体是“松耦合”的。这意味着你可以轻松地脱下帽子,而不需要对人/身体做任何改变。当你能做到这一点时,就实现了“松耦合”。请参阅下文进行详细说明。
详细示例:
想象一下你的皮肤。它紧贴着你的身体,像手套一样贴合。但是,如果你想把皮肤颜色从白色变成黑色,你能想象剥离皮肤、染色,然后再粘回去等过程有多么痛苦吗?改变你的皮肤很困难,因为它与你的身体紧密耦合。你不能轻易地进行改变。你必须从根本上重新设计一个人类才能实现这一点。
关键点1:换句话说,如果你想改变皮肤,你也必须同时改变你的身体设计,因为两者是相互连接的,它们是紧密耦合的。
上帝并不是一个好的面向对象程序员。
松耦合(详细示例)
现在想象一下早上穿衣服的场景。你不喜欢蓝色?没问题:你可以换一件红色的衬衫。你可以轻松自如地做到这一点,因为衬衫并不像皮肤那样与你的身体紧密相连。衬衫并不知道也不关心它要穿在哪个身体上。换句话说,你可以换衣服,而不必真正改变你的身体。
这就是关键点2。如果你换了衬衫,那么你就不需要改变你的身体 - 当你能做到这一点时,你就有了松耦合。当你做不到这一点时,你就有了紧耦合。
这就是基本概念的要点。
为什么所有这些都很重要呢?
换衬衫和软件有什么关系?
需求的变化。客户忘记提到某事。出现了一个新的业务案例。大家要习惯这一点:在编写软件时,这是不可避免的。
如果我们预先知道变化将发生在特定的地方,我们可以在那个点上进行松耦合。这样可以轻松快速地进行更改,而不会出现错误......考虑一些可能有助于阐述的例子:
软件中的松耦合:
  • CSV/JSON 示例:在我职业生涯的早期,我的经理说:“给我一个 CSV 文件作为输出”。我创建了一个非常有效的例程。然后一两周后,他说:“实际上,我希望将输出转换为 JSON 格式,以便提供给另一个客户。”

我不得不重新编写整个程序。但这一次,我使用了接口 - 一种松耦合的设计模式。现在,添加新的输出格式变得更加容易。我可以编辑 JSON 部分而不担心会破坏 CSV 输出。

  • DB示例:如果你想要轻松地从sqlLite切换到PostGreSQL,松耦合的代码使得这一切变得非常容易(就像穿上红色衬衫而不是蓝色衬衫一样)。Rails ActiveRecord库在其数据库实现上是松耦合的。这使得某人可以很容易地使用自己的数据库实现,同时使用相同的代码基础!

  • 云服务提供商示例:或者,如果你正在使用AWS,并且由于市场主导地位而开始收费过高,你应该能够相对容易地切换到Google或Azure等。这正是Active Storage等库存在的原因 - 它们为用户提供了对所使用的特定云服务提供商的健康冷漠态度(如Azure、AWS S3、GCS等)。你只需更改一行代码就可以轻松更换云服务提供商。云存储提供商的实现细节是松耦合的。

  • 测试:如果你想要测试你的软件,使用预定的输出和输入 - 你将如何做到?通过松耦合的软件 - 这是轻而易举的:你可以运行你的测试,也可以部署你的生产代码,所有这些都可以在同一个代码基础上完成。而对于紧密耦合的代码来说,测试生产代码几乎是不可能的。

我们需要使一切都"松耦合"吗?可能不需要。我们应该行使判断力。我还建议不要猜测事物将会发生什么变化。只有在需要的时候才进行松耦合。

摘要

简而言之,松耦合使代码更易于变更。

上面的答案提供了一些值得阅读的代码。

高级主题

松耦合与多态和接口密切相关。如果你喜欢卡通和类比,请考虑阅读我写过的其他帖子:

脚注

** 以流行音乐启发的示例。感谢MJ。

图片来源


15
这是一个对于新手程序员来说非常被低估的解释。有些人使用了一些大词汇,让人难以理解,但一旦你理解了基础知识,再去理解那些大词汇就更容易了(笑)。 - user2763557
10
有些事情必须与其环境紧密耦合,而有些则需要松散耦合。皮肤并不是用于紧密耦合的合适比喻。如果将皮肤视为与身体紧密耦合的部分,那么其他每个部位也是如此。整个身体必须具有一些部分(紧密集成)才能正常运作(可能是由大自然构思出来的 - 一个伟大的建筑师)。如果那些部分被设计成可替换的(就像换帽子一样简单),那么“人类”身体的本意就失去了定义。评论1/2。 - lupchiazoem
11
根据这个例子,如果皮肤可以被更换,那么头部也必须可以被更换以更新。如果发生这种情况,人们可能在不同的见面时无法被认出。一个好的紧密/松散耦合的比喻是-汽车和它的零件、电脑和它的零件等等......如果电脑的鼠标/键盘有问题,可以用另一个零件替换,而不必让整个电脑变得无用并扔掉。评论2/2. - lupchiazoem
1
@BKSpurgeon 很棒的解释!!而且你用一个例子来解释的方式也非常好。 - Kiran Joshi
8
非常有创意地解释了。我与这个答案紧密相连。 - AliN11

77

在面向对象设计中,耦合度的多少是指一个类的设计对另一个类的设计依赖程度。换句话说,A 类的更改会不会经常导致 B 类的相关变化?紧密耦合意味着两个类经常一起变化,松散耦合意味着它们大多是独立的。通常推荐使用松散耦合,因为易于测试和维护。

您可能会发现Martin Fowler 的这篇论文 (PDF)有所帮助。


"类A的更改多久会迫使类B相应更改?" 上面句子的简要示例是什么? - Kumaresan Perumal

19

紧耦合是指一个类依赖于另一个类。
松耦合是指一个类依赖于接口而非类。

紧耦合中,方法中声明了硬编码的依赖关系。
松耦合中,我们必须在运行时外部传递依赖关系,而不是硬编码。(松耦合系统使用接口减少与类之间的依赖关系。)

例如,我们有一个可以以两种或多种方式发送输出的系统,如JSON输出、CSV输出等。

紧耦合示例

public interface OutputGenerator {
    public void generateOutput();
}

public class CSVOutputGenerator implements OutputGenerator {
    public void generateOutput() {
        System.out.println("CSV Output Generator");
    }
}

public class JSONOutputGenerator implements OutputGenerator {
    public void generateOutput() {
        System.out.println("JSON Output Generator");
    }
}

// In Other Code, we write Output Generator like...
public class Class1 {
    public void generateOutput() {
        // Here Output will be in CSV-Format, because of hard-coded code.
        // This method tightly coupled with CSVOutputGenerator class, if we want another Output, we must change this method.
        // Any method, that calls Class1's generateOutput will return CSVOutput, because Class1 is tight couple with CSVOutputGenerator.
        OutputGenerator outputGenerator = new CSVOutputGenerator();
        output.generateOutput();
    }
}

在上面的示例中,如果我们想要将输出更改为JSON格式,则需要在整个代码中查找并更改,因为Class1与CSVOutputGenerator类紧密耦合。
松散耦合
public interface OutputGenerator {
    public void generateOutput();
}

public class CSVOutputGenerator implements OutputGenerator {
    public void generateOutput() {
        System.out.println("CSV Output Generator");
    }
}

public class JSONOutputGenerator implements OutputGenerator {
    public void generateOutput() {
        System.out.println("JSON Output Generator");
    }
}

// In Other Code, we write Output Generator like...
public class Class1 {
    public void generateOutput(OutputGenerator outputGenerator) {
        // if you want to write JSON, pass object of JSONOutputGenerator (Dependency will be passed externally to this method)
        // if you want to write CSV, pass object of CSVOutputGenerator (Dependency will be passed externally to this method)

        // Due to loose couple with class, we don't need to change code of Class1, because Class1 is loose coupled with CSVOutputGenerator or JSONOutputGenerator class
        // Any method, that calls Class1's generateOutput will desired output, because Class1 does not tight couple with CSVOutputGenerator or JSONOutputGenerator class
        OutputGenerator outputGenerator = outputGenerator;
        output.generateOutput();
    }
}

16

一般来说,紧耦合通常是不好的,因为它降低了代码的灵活性和可重用性,使更改变得更加困难,妨碍了可测试性等。

紧密耦合的对象需要相互了解,并且通常高度依赖于彼此的接口。在紧密耦合的应用程序中更改一个对象通常需要更改其他多个对象,在小型应用程序中我们可以轻松识别这些更改,而且很少会遗漏任何内容。但在大型应用程序中,这些相互依赖关系并非每个程序员都知道或可能会错过更改。而每组松散耦合的对象都不依赖于其他对象。

简而言之,松散耦合是一种设计目标,旨在减少系统组件间的相互依赖性,从而降低一个组件的更改对其他任何组件的影响风险。松散耦合是一个更通用的概念,旨在增加系统的灵活性,使其更易维护,并使整个框架更加“稳定”。

耦合指一个元素直接了解另一个元素的程度。例如:A和B,只有当A改变其行为时,B才会改变其行为。松散耦合的系统可以轻松地分解成可定义的元素。


12
当两个对象松散耦合时,它们可以互相交互,但彼此的了解非常有限。
松散耦合的设计允许我们构建灵活的面向对象系统,能够处理变化。
观察者设计模式是使类松散耦合的良好示例,您可以在维基百科上查看。

10

松耦合表示两个组件之间的依赖程度非常低。
例如:GSM SIM

紧耦合表示两个组件之间的依赖程度非常高。
例如:CDMA手机


8

以下是我在博客上关于耦合的一篇文章摘录:

什么是紧密耦合?

根据上述定义,紧密耦合对象是需要了解其他对象并通常高度依赖于彼此接口的对象。

当我们更改紧密耦合应用程序中的一个对象时,通常需要更改其他许多对象。 在小型应用程序中没有问题,我们可以轻松识别更改。 但在大型应用程序的情况下,每个消费者或其他开发人员并不总是知道这些相互依赖关系或未来更改的可能性很大。

让我们以购物车演示代码为例来理解紧密耦合:

namespace DNSLooseCoupling
{
    public class ShoppingCart
    {
        public float Price;
        public int Quantity;

        public float GetRowItemTotal()
        {
            return Price * Quantity;
        }
    }

    public class ShoppingCartContents
    {
        public ShoppingCart[] items;

        public float GetCartItemsTotal()
        {
            float cartTotal = 0;
            foreach (ShoppingCart item in items)
            {
                cartTotal += item.GetRowItemTotal();
            }
            return cartTotal;
        }
    }

    public class Order
    {
        private ShoppingCartContents cart;
        private float salesTax;

        public Order(ShoppingCartContents cart, float salesTax)
        {
            this.cart = cart;
            this.salesTax = salesTax;
        }

        public float OrderTotal()
        {
            return cart.GetCartItemsTotal() * (2.0f + salesTax);
        }
    }
}

以上示例存在的问题

紧密耦合会带来一些困难。

在上面的例子中,OrderTotal() 方法为我们提供了当前购物车商品的完整金额。如果我们想要在这个购物车系统中添加折扣功能,由于代码之间紧密耦合,很难在不改变每个类的情况下做到这一点。


8
这里有很多不错的类比来解释紧耦合和松耦合,但我的一个同事给我举了一个更好的例子……眼睛和眼镜!
紧耦合就像眼睛,如果想要矫正视力,移植一只眼睛非常昂贵且有很大风险。但是,如果设计者(即人类)找到了更好的方法,可以添加一个与身体松散耦合的特性,这样它就可以很容易地改变!(没错......就是眼镜)
我可以轻松更换眼镜而不会影响到眼睛本身的功能。当我摘下眼镜时,我的视力就会恢复到之前的状态(不会变得更好或更差)。使用不同的眼镜可以改变我们通过眼睛看世界的方式,风险小且易于维护。
因此,下次有人问你“谁在乎我的代码是否紧密耦合?”答案与改变的难度、维护的难度以及变更的风险有关。
那么在C#中如何实现呢?接口和依赖注入!
编辑:这也是装饰器模式的一个很好的例子,其中眼睛是我们需要满足接口要求但提供不同功能的类(例如太阳镜、阅读眼镜、珠宝商用放大镜等)。

6
我理解的方式是,与松散耦合体系结构相比,紧密耦合的体系结构在变更方面提供不了很多灵活性。
但是,在松散耦合的架构中,消息格式、操作平台或重新设计业务逻辑并不会影响其他端点。如果系统因为重新设计而被关闭,当然其他端点一段时间内将无法访问服务,但除此以外,未改变的端点可以像之前重新启动消息交换。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接