【问题标题】:Does using a C++ namespace increase coupling?使用 C++ 命名空间会增加耦合吗?
【发布时间】:2010-02-04 19:36:20
【问题描述】:

我了解 C++ 库应该使用命名空间来避免名称冲突,但因为我已经必须:

  1. #include 正确的标头(或转发声明我打算使用的类)
  2. 按名称使用这些类

不要这两个参数推断一个命名空间传达的相同信息。使用命名空间现在引入了第三个参数 - 完全限定名称。如果库的实现发生变化,现在有 三个 潜在的事情我需要改变。从定义上讲,这不是增加了库代码和我的代码之间的耦合吗?


例如,看一下 Xerces-C:它在命名空间 XERCES_CPP_NAMESPACE 内定义了一个名为 Parser 的纯虚拟接口。我可以在我的代码中使用Parser 接口,方法是包含适当的头文件,然后导入命名空间using namespace XERCES_CPP_NAMESPACE 或在声明/定义前加上XERCES_CPP_NAMESPACE::

随着代码的发展,可能需要放弃 Xerces 以支持不同的解析器。纯虚拟接口对库实现的更改部分“保护”了(如果我使用工厂构造我的解析器更是如此),但是一旦我从 Xerces 切换到其他东西,我需要梳理我的代码并更改我所有的using namespace XERCES_CPP_NAMESPACEXERCES_CPP_NAMESPACE::Parser 代码。


我最近在重构一个现有的 C++ 项目以将一些现有的有用功能拆分到一个库中时遇到了这个问题:

foo.h

class Useful;  // Forward Declaration

class Foo
{
public:

    Foo(const Useful& u);
    ...snip...

}

foo.cpp

#include "foo.h"
#include "useful.h" // Useful Library

Foo::Foo(const Useful& u)
{
    ... snip ...
}

当时很大程度上出于无知(部分出于懒惰),useful.lib 的所有功能都放在了全局命名空间中。

随着useful.lib 内容的增长(以及更多的客户端开始使用该功能),我们决定将所有代码从useful.lib 移到它自己的名为"useful" 的命名空间中。

客户端.cpp文件很容易修复,只需添加using namespace useful

foo.cpp

#include "foo.h"
#include "useful.h" // Useful Library

using namespace useful;

Foo::Foo(const Useful& u)
{
    ... snip ...
}

.h 文件真的 是劳动密集型的。我没有通过将using namespace useful; 放在头文件中来污染全局命名空间,而是将现有的前向声明包装在命名空间中:

foo.h

namespace useful {
    class Useful;  // Forward Declaration
}

class Foo
{
public:

    Foo(const useful::Useful& u);
    ...snip...
}

有几十个(和几十个)文件,这最终成为一个巨大的痛苦!它不应该那么困难。显然我在设计和/或实现方面做错了。

虽然我知道库代码应该位于其自己的命名空间中,但将库代码保留在全局命名空间中并尝试管理 #includes 是否有利?

【问题讨论】:

  • "(b) 命名空间推断的实现细节" -- 你能更详细地解释一下吗?我不明白这是什么意思。
  • 在我回答这个问题之前我差点睡着了,我还是不太明白你在问什么。您是否考虑过:使用有用的::有用的;
  • 那是很多文字。有没有我可以阅读的精简版?
  • @*:阅读倒数第二段。
  • +1:忽略仇恨者。我喜欢读你的帖子!

标签: c++ namespaces decoupling


【解决方案1】:

在我看来,您的问题主要是由于您 (ab) 使用命名空间的方式,而不是命名空间本身。

  1. 听起来您将许多最小相关的“东西”放入一个命名空间中,主要是因为它们恰好是由同一个人开发的。至少在 IMO,命名空间应该反映代码的逻辑组织,而不仅仅是一堆实用程序碰巧由同一​​个人编写的意外。

  2. 命名空间名称通常应该相当长且具有描述性,以防止最远的冲突可能性。例如,我通常包括我的姓名、写作日期以及对命名空间功能的简短描述。

  3. 大多数客户端代码不需要(通常也不应该)直接使用命名空间的真实名称。相反,它应该定义一个命名空间别名,并且在大多数代码中应该只使用别名。

将第二点和第三点放在一起,我们可以得到如下代码:

#include "jdate.h"

namespace dt = Jerry_Coffin_Julian_Date_Dec_21_1999;

int main() {

    dt::Date date;

    std::cout << "Please enter a date: " << std::flush;
    std::cin>>date;

    dt::Julian jdate(date);
    std::cout   << date << " is " 
                << jdate << " days after " 
                << dt::Julian::base_date()
                << std::endl;
    return 0;
}

这消除(或至少大大减少)客户端代码与日期/时间类的特定实现之间的耦合。例如,如果我想重新实现相同的日期/时间类,我可以将它们放在不同的命名空间中,只需更改别名并重新编译即可在一个命名空间和另一个命名空间之间切换。

事实上,我有时将其用作一种编译时多态机制。例如,我编写了几个版本的小型“显示”类,一个在 Windows 列表框中显示输出,另一个通过 iostreams 显示输出。然后代码使用类似的别名:

#ifdef WINDOWED
namespace display = Windowed_Display
#else
namespace display = Console_Display
#endif

其余的代码只使用display::whatever,所以只要两个命名空间都实现了整个接口,我可以使用其中一个,而完全不改变其余代码,使用指针/引用到具有实现的虚函数的基类的任何运行时开销。

【讨论】:

  • “作为一种运行时多态机制”——应该是“编译时”
  • @gf:哎呀,是的。谢谢。我会解决的。
  • 上帝,我绝对不会用我的姓氏和日期来命名我的命名空间......我们有数百个组件在工作,并且名称通常是组件的名称,以区分它们:)
  • @Matthieu:关键是想出一个你可以遵循的模式,它可以合理地保证它是独一无二的。我所提倡的重点是,您唯一一次直接使用该名称是作为别名的目标。
  • 我明白,但是您的每个客户都需要用别名包装您的标头或在每次包含时重新定义别名,这很麻烦。此外,作为命名约定,我使用命名空间作为我的文件的路径>>#include "name1/name2/class.h" 表示name1::name2::Class,这样一目了然地识别一个类属于哪个模块(子模块...)以及哪个标头把它带进来。
【解决方案2】:

命名空间与耦合无关。无论您将其称为useful::UsefulClass 还是仅称为UsefulClass,都存在相同的耦合。现在,您需要进行所有重构工作这一事实只会告诉您代码在多大程度上依赖于您的库。

为了简化转发,您可以编写一个 forward 标头(在 STL 中有一对,您肯定可以在库中找到它),例如 usefulfwd.h,它只转发定义了库接口(或实现类或其他)你需要)。但这与耦合无关。

不过,耦合和命名空间是无关的。一朵玫瑰的任何其他名字都会闻起来一样甜美,并且您的类在任何其他命名空间中都是耦合的。

【讨论】:

  • “现在你需要做所有重构工作的事实只是告诉你你的代码在多大程度上依赖于你的库。”这是一个很好的观点。我将代码制成了一个库,以便其他人可以使用它,但我的原始代码在很大程度上依赖于该功能。
【解决方案3】:

(a) 库中的接口/类/函数

不会比你已经拥有的更多。使用namespace-ed 库组件可以帮助您避免命名空间污染。

(b) 命名空间推断的实现细节?

为什么?您应该包含的只是标题useful.h。该实现应该被隐藏(并位于useful.cpp 中,并且可能以动态库的形式)。

您可以通过 using useful::Useful 声明选择性地仅包含 useful.h 中您需要的那些类。

【讨论】:

    【解决方案4】:

    我想扩展 David Rodríguez 的第二段 - dribeas 的回答(已投票):

    为了简化转发,您可以编写一个转发头(在 STL 中有几个,您肯定可以在库中找到它),例如仅转发定义库接口(或实现类或任何您需要的东西)的有用的fwd.h )。但这与耦合无关。

    我认为这指向了您问题的核心。命名空间在这里是一个红鲱鱼,你被低估了包含语法依赖的需要而被咬伤。

    我能理解你的“懒惰”:过度工程(企业 HelloWorld.java)不对的,但是如果你在一开始就保持你的代码低调(这不一定是错误的)并且代码证明是成功的,成功将把它拖到它的联盟之上。诀窍是把握合适的时机切换到(或从需要出现的第一刻开始使用)一种能够以向前兼容的方式抚平你的痒的技术。

    对一个项目进行闪闪发光的前瞻性声明只是在乞求第二轮及后续轮次。您不需要成为 C++ 程序员就可以阅读“不要前向声明标准流,改用 &lt;iosfwd&gt;”的建议(尽管这已经是几年前的事了;1999 年?VC6 时代,绝对)。如果您稍作停顿,您会听到许多没有听从建议的程序员发出的痛苦尖叫。

    可以理解保持低调的冲动,但你必须承认#include &lt;usefulfwd.h&gt; 并不比class Useful 更痛苦,并且规模。只需这个简单的委派,您就可以将N-1class Useful 更改为class useful::Useful

    当然,它不会帮助您处理客户端代码中的所有用途。简单的帮助:事实上,如果您在大型应用程序中使用库,您应该将库提供的转发标头包装在特定于应用程序的标头中。这一点的重要性随着依赖的范围和库的波动性而增加。

    src/libuseful/usefulfwd.h

    #ifndef GUARD
    #define GUARD
    namespace useful {
        class Useful;
    } // namespace useful
    #endif
    

    src/myapp/myapp-usefulfwd.h

    #ifndef GUARD
    #define GUARD
    #include <usefulfwd.h>
    using useful::Useful;
    #endif
    

    基本上,这是保持代码干燥的问题。你可能不喜欢朗朗上口的 TLA,但这个 TLA 描述了一个真正的核心编程原则。

    【讨论】:

      【解决方案5】:

      如果您有多个“有用”库的实现,那么它们是否同样可能(如果不受您控制)使用相同的命名空间,无论是 global 命名空间还是有用的命名空间?

      换句话说,使用命名命名空间与全局命名空间与您与库/实现的“耦合”程度无关。

      任何一致的库演变策略都应该为 API 维护相同的命名空间。该实现可以利用对您隐藏的不同名称空间,并且这些名称空间可能会在不同的实现中发生变化。不确定这是否是您所说的“命名空间推断的实现细节”。

      【讨论】:

        【解决方案6】:

        不,你没有增加耦合。正如其他人所说 - 我没有看到命名空间使用如何泄漏实现

        消费者可以选择做

         using useful;
         using useful::Foo;
         useful::Foo = new useful::Foo();
        

        我的投票总是最后一票 - 它是污染最少的

        应该强烈劝阻第一个(行刑队)

        【讨论】:

        • 大概是你想要的第一个using namespace useful;?
        【解决方案7】:

        好吧,事实是没有办法轻松避免 C++ 中的代码纠缠。但是,使用全局命名空间是最糟糕的想法,因为这样您就无法在实现之间进行选择。你以对象而不是对象结束。这在内部工作正常,因为您可以随时编辑源代码,但如果有人将这样的代码发送给客户,他们不应该期望它们会持续很长时间。

        一旦你使用了 using 语句,你也可以在全局中,但在 cpp 文件中使用它会很好。所以我想说你应该把所有东西都放在一个命名空间中,但是对于内部来说,它应该都是同一个命名空间。这样其他人仍然可以使用您的代码而不会发生灾难。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-05-31
          • 2014-07-17
          • 1970-01-01
          相关资源
          最近更新 更多