《Effective Ruby:改善Ruby程序的48条建议》一第11条:通过在模块中嵌入代码来创建命名空间

第11条:通过在模块中嵌入代码来创建命名空间

假设你正在做一个订购个性化笔记本(那种过时的纸质笔记本)的应用程序。客户能够在众多装订方式中选择,如使用金属钉针装订或使用传统的胶水装订。你决定创建一个类来表示装订类型,并将其他参数一同放在里面。然而很遗憾,事情并非计划的那样。下面的类定义有什么问题呢?

乍一看,似乎什么都是对的。这是没有语法错误的,但如果你在IRB中运行这个类,你会看到还真有点问题。当你执行这段代码来创建新的对象时,你会发现事实和预期不同。然而如果这段代码不是在定义一个类那它又做了些什么呢?这是个值得思考的问题。
如你所知,Ruby中的类是可变的——你能在任何时间增加和替换方法。创建类的语法和修改类是相同的。在本例中,你想创建的类的名字恰好已经被创建过了。Binding是Ruby核心类库中的一个类。上面的代码不是定义一个新类,而是打开了本就存在的Binding类并修改了它。与你想的显然不同。
任何卓越的软件早晚都会在运行中遇到问题。尤其是对库的作者来说。如果多个库中存在相同名字的类会发生什么呢?你如何同时使用两个这样的库呢?还好,几乎所有现代的通用语言都有解决这个问题的方式,包括Ruby。Ruby通过命名空间的作用域将名字进行分隔。
命名空间是一种保证常量唯一性的方式。其最基本的功能是创建作用域从而定义互不冲突的常量。由于类和模块的名字都是常量,因此命名空间能用来很好地隔离它们。
所有的核心Ruby类都存在于被称为“全局”的命名空间内。我们之后再说它的含义,现在只要知道这些名字代表的每个类都无需使用限定符即可使用就可以了。换句话说,如果你启动IRB并键入Array,意味着“类Array是在全局命名空间里的”。当你不加命名空间地定义类时,这个类就被放在了全局命名空间里。同时存在与已有名字发生冲突的风险。
创建一个新类并将其置于自定义命名空间是非常简单的,只要将类定义嵌入在一个模块中即可。

把类定义嵌入模块中使Binding类和核心类区别开来,就避免了同名造成的困扰。引用新类你需要引用模块名并使用“类路径分隔符”,也就是两个冒号:

使用模块创建命名空间不仅限于保护类,该技术也可以用来把其他常量和模块方法放入命名空间。你还能将模块嵌入其他模块中来创建任意深的命名空间。这在大型应用程序和库中作为代码隔离和模块化的一种方式。
使用命名空间时,常用的实践是在项目的文件系统上使用和命名空间一样的目录结构。比如,上述类的定义应该能在notebooks文件夹下的bindings.rb文件中找到。换句话说,名字Notebooks::Bindings映射着文件notebooks/bindings.rb。
有时在模块中的嵌入类非常烦琐并会导致不必要的缩进。存在一个替代语法可以在命名空间里创建类。但它仅在命名空间已经存在时有效,即如果用来创建命名空间的模块已经在前面定义过,那么你可以在类定义中直接使用模块名和类路径分隔符。

这种语法的类定义的典型应用场景是,你先在入口源文件中定义了命名空间模块,随后加载所有剩下的源文件。但是要小心,在没有预先定义命名空间时就尝试使用这种语法会导致一个NameError异常。Ruby如果找不到引用的常量就会引发这个异常。将常量嵌套在另一个常量中会为你在程序中如何限定常量带来一点复杂性,不过如果明白了Ruby如何搜索它们,就不会有这样的疑虑了。
Ruby使用两种技术寻找常量。第一种是,检查当前词法作用域和所有闭包词法作用域。(我们将在稍后探索词法作用域。)如果无法找到这个常量,Ruby将循着继承体系继续寻找。这就是为什么你可以在子类中使用父类定义的常量。如我们将要看到的,这也是为什么我们可以使用所谓的“全局”常量。
谈到命名空间,我们最关心的就是词法作用域,表示实际定义或引用常量的位置。看代码显然比读这些文字容易些,那么请看:

模块定义中创建了一个词法作用域。由于常量KEY和类Encrypt都定义在同一个词法作用域中,所以initialize方法能够在不加限定符(使用常量全路径)的情况下就使用常量KEY。明白词法作用域不同于模块创建的命名空间这一点非常重要。它与常量定义和使用的物理位置有关。如果我们稍做改变,你就可以看出其区别:

这一次命名空间和词法作用域都使用模块来定义,不过它们都在创建完常量KEY之后立即关闭了。类定义在正确的命名空间里,只是它并没有和KEY共享同一个词法作用域,因此不能不加限定符就使用它。因为Ruby在词法作用域或其继承体系中无法找到常量KEY,所以这段代码会引发NameError异常。修复这个问题很简单,只要引用常量时加上限定符即可:

这看起来有点古怪,不过它就是这么运行的。还有更奇怪的呢,既然常量已经加上了限定符,它却是在继承体系中被找到的而非词法作用域。常量SuperDumbCrypto可以被看作全局常量,但当它出现时,Ruby还没有全局命名空间。这时所有顶级常量都被存在Object类中。由于Ruby中几乎所有的东西都继承自Object,因此可以通过继承体系找到所有顶级常量。这就解释了为什么Ruby会在两个地方寻找它们:当前的词法作用域以及继承体系。
当使用命名空间时还有最后一处可能遇到的坑,如下代码所示:

上面的代码定义了Cluster::Array类,它需要用到顶级类Array。根据常量的检索规则,当前上下文中没有限定符的常量Array表示类Cluster::Array,而这并不是我们期望的。解决方法是:为常量Array加上限定符。由于这是顶级常量,我们知道它们被存储在类Object中,因此限定符常量的名字将是Object::Array。这看起来有点怪,因此Ruby允许我们简写为::Array。下面的代码就是我们想要的:

命名空间虽然增加了些许复杂性(对于不加限定符的常量)但这个特性是值得的。任何卓越的Ruby项目都必然会使用它,尤其是被打成gem包的库。我们预期这类库会把它们的常量放在和库名相匹配的命名空间里。通过创建和使用命名空间,能够和其他程序友好相处。
要点回顾
通过在模块中嵌入代码来创建命名空间。
让你的命名空间结构和目录结构相同。
如果使用时可能出现歧义,可使用“::”来限定顶级常量(比如,::Array)。

时间: 2024-09-19 20:33:56

《Effective Ruby:改善Ruby程序的48条建议》一第11条:通过在模块中嵌入代码来创建命名空间的相关文章

编写高质量代码改善C#程序的157个建议[优先考虑泛型、避免在泛型中声明静态成员、为泛型参数设定约束]

原文:编写高质量代码改善C#程序的157个建议[优先考虑泛型.避免在泛型中声明静态成员.为泛型参数设定约束] 前言 泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能.基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用.同时,它减少了泛型类及泛型方法中的转型,确保了类型安全.委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用.事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分.一旦我们开始编写

编写高质量代码改善C#程序的157个建议[IEnumerable<T>和IQueryable<T>、LINQ避免迭代、LINQ替代迭代]

原文:编写高质量代码改善C#程序的157个建议[IEnumerable<T>和IQueryable<T>.LINQ避免迭代.LINQ替代迭代] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议29.区别LINQ查询中的IEnumerable<T>和IQueryable<T> 建议30.使用LINQ取代集合中的比较器和迭代器 建议31.在LINQ查询中避免不必要的迭代

编写高质量代码改善C#程序的157个建议[为泛型指定初始值、使用委托声明、使用Lambda替代方法和匿名方法]

原文:编写高质量代码改善C#程序的157个建议[为泛型指定初始值.使用委托声明.使用Lambda替代方法和匿名方法] 前言 泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能.基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用.同时,它减少了泛型类及泛型方法中的转型,确保了类型安全.委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用.事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分.一旦我们

编写高质量代码改善C#程序的157个建议[用抛异常替代返回错误、不要在不恰当的场合下引发异常、重新引发异常时使用inner Exception]

原文:编写高质量代码改善C#程序的157个建议[用抛异常替代返回错误.不要在不恰当的场合下引发异常.重新引发异常时使用inner Exception] 前言 自从.NET出现后,关于CLR异常机制的讨论就几乎从未停止过.迄今为止,CLR异常机制让人关注最多的一点就是"效率"问题.其实,这里存在认识上的误区,因为正常控制流程下的代码运行并不会出现问题,只有引发异常时才会带来效率问题.基于这一点,很多开发者已经达成共识:不应将异常机制用于正常控制流中.达成的另一个共识是:CLR异常机制带来

编写高质量代码改善C#程序的157个建议[4-9]

原文:编写高质量代码改善C#程序的157个建议[4-9] 前言 本文首先亦同步到http://www.cnblogs.com/aehyok/p/3624579.html.本文主要来学习记录一下内容: 建议4.TryParse比Parse好 建议5.使用int?来确保值类型也可以为null 建议6.区别readonly和const的使用方法 建议7.将0值设为枚举的默认值 建议8.避免给枚举类型的元素提供显式的值 建议9.习惯重载运算符 建议4.TryParse比Parse好 如果注意观察,除st

编写高质量代码改善C#程序的157个建议[C#闭包的陷阱、委托、事件、事件模型]

原文:编写高质量代码改善C#程序的157个建议[C#闭包的陷阱.委托.事件.事件模型] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议38.小心闭包中的陷阱 建议39.了解委托的实质 建议40.使用event关键字对委托施加保护 建议41.实现标准的事件模型 建议38.小心闭包中的陷阱 首先我们先来看一段代码: class Program { static void Main(string[] arg

编写高质量代码改善C#程序的157个建议[为类型输出格式化字符串、实现浅拷贝和深拷贝、用dynamic来优化反射]

原文:编写高质量代码改善C#程序的157个建议[为类型输出格式化字符串.实现浅拷贝和深拷贝.用dynamic来优化反射] 前言 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议13.为类型输出格式化字符串 建议14.正确实现浅拷贝和深拷贝 建议15.使用dynamic来简化反射实现 建议13.为类型输出格式化字符串   有两种方法可以为类型提供格式化的字符串输出. 一种是意识到类型会产生格式化字符串输出,于是

编写高质量代码改善C#程序的157个建议[匿名类型、Lambda、延迟求值和主动求值]

原文:编写高质量代码改善C#程序的157个建议[匿名类型.Lambda.延迟求值和主动求值] 前言 从.NET3.0开始,C#开始一直支持一个新特性:匿名类型.匿名类型由var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性: 1.既支持简单类型也支持复杂类型.简单类型必须是一个非空初始值,复杂类型则是一个以new开头的初始化项. 2.匿名类型的属性是只读的,没有属性设置器,它一旦倍初始化就不可更改. 3.如果两个匿名类型的属性值相同,那么就任务这两个匿名类型

编写高质量代码改善C#程序的157个建议[动态数组、循环遍历、对象集合初始化]

原文:编写高质量代码改善C#程序的157个建议[动态数组.循环遍历.对象集合初始化] 前言   软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类.不管是数组还是集合类,它们都有各自的优缺点.如何使用好集合是我们在开发过程中必须掌握的技巧.不要小看这些技巧,一旦在开发中使用了错误的集合或针对集合的方法,应用程序将会背离你的预想而运行. 本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html .本文主要学习记录以下内容: 建议16.