《C++代码设计与重用》——2.6 接口一致性

2.6 接口一致性

C++代码设计与重用
2.6 接口一致性
在类的内部和类与类之间,在程序库的内部和程序库与程序库之间,我们都应该尽可能地保持类接口的一致性。很显然,保持程序库与程序库之间的接口一致性,将比保持单个程序库内部的接口一致性更加困难。

接口一致性的重要性要归因于以下几个方面。首先,具有接口一致性的类易于学习和记忆。例如,假设我们正在设计一个容器类程序库。(容器类是一种用来保存值或者对象的集合的类。)下面就是我们设计的程序库所提供的两个类:

template<class T>
    class Set {
    public:
         void insert(const T& t);
         //...
    };
    template<class T>
    class Bag {
    public:
         void insert(const T& t);
         //...
    };

一个Set是类型为T的值的集合;一个Bag是类型为T的值的汇合。(Bag和Set是有区别的,Bag允许它里面的相同元素值出现多次,而Set就不允许。)尽管在Set里插入一个值和在Bag里插入一个值的区别很小(在Bag里插入一个已经存在的值,将会出现该值的一份拷贝,而Set就不会),我们还是给予这两个类的插入操作相同的句法接口(syntactic interface)。因为如果插入操作拥有不同的句法接口的话,那么对Set类和Bag类的学习、使用、记忆将会显得更加困难。

提供接口一致性的一个稍微次要的原因就是,它使用户可以更加容易地改变程序中对象的类型,假设用户已经完成了下面代码:

Set<int>s;
   //...
   s.insert(7);

如果写完了上面的代码之后,用户发现应该使用Bag而不是Set,那么要是Set和Bag拥有一致性接口的话,这时我们就可以只更改上面的声明语句,而不需要更改调用语句:

Bag<int> s;  //在这里将Set改成Bag...
    //...
    s.insert(7);  //...这一行不需要更改

具有相似接口的类可以从一个公共基类派生而来,譬如下面的代码:

template<class T>
    class Container {
    public:
         virtual void insert(const T& t) = 0;
         //...
    };
    template<class T>
    class Set  : public  Container<T>  {
    public:
         virtual void insert(const T& t);
         //...
    };
//类Bag和类Set类似,继承自类Container...

然而,两个类仅仅具有相似的接口,并不意味着它们就应该派生自某个公共基类。假设我们的用户并不需要在Set和Bag上实现具有多态性的函数,并且如果我们希望避免由于实现为虚函数而给insert函数带来的额外开销,那么我们就可以决定不让类Set和类Bag派生自基类Container。

决定两个接口的一致性程度应该经过深思熟虑。假设我们也提供一个类Queue来描述队列,那么类Queue是否应该提供insert操作呢?

template<class T>
    class Queue {
    public:
         void insert(const T& t);  //应该提供这个函数吗?
         //...
    };

如果我们提供类Queue的insert操作,那么我们将在Queue的什么位置插入t呢?很显然,有两个合理的位置:队头和队尾。然而,这两个选择都是不可取的。假设选择了队头,我们还是必须提供一个在队尾进行插入的操作。另外,因为我们不能把这两个操作都命名为insert,所以我们就必须给其中一个操作命名为别的名字:

template<class T>
    class Queue {
    public:
         void insert(const T& t);
         void insert_tail(const T& t);
         //...
    };

然而,上面的接口就会出现内部的不一致性。许多用户往往记不清楚类Queue究竟是提供insert/insert_tail两个操作,还是提供insert/insert_head两个操作。这样,一些用户就会调用insert函数,并想当然地认为insert执行的是队尾插入操作。

内部一致性的接口应该给这两个操作分别命名为insert_head和insert_tail:

template<class T>
  class Queue {
  public:
       void insert_head(const T& t);
       void insert_tail(const T& t);
       //...
  };

然而,上面这个Queue类的接口将会与Set类和Bag类的接口不一致;为了恢复它们之间的一致性,我们可以提供下面3个操作,并把insert定义为insert_head的等价操作:

template<class T>
    class Queue {
    publiC:
         void insert(const T& t) {insert_head(t); }
         void insert_head(const T& t);
         void  insert_tail(const T& t);
         //...
    };

很遗憾的是,这些接口比前面两种接口都要大,从而难以理解。

要让类Queue的接口与类Set和类Bag的接口保持一致性是很困难的,这主要是由于类Queue具有不同于类Set和类Bag的地方:类Queue逻辑上要实现两个插入操作。让我们考虑另一个像Set和Bag的类吧,它只提供一个插入操作。特别地,考虑描述堆栈的类Stack。堆栈中只有一种插入方式,就是把插入值压入栈顶。我们是应该把这个插入操作命名为insert来和Set和Bag保持一致性呢?还是应该把它命名为push呢?因为push是人们对堆栈插入操作的习惯称呼;或者命名为两个呢?我们的看法是,提供一个push操作将使大多数类Stack的用户能够更容易地学习和记忆—特别地,对那些只使用类Stack,而对我们程序库中其他的容器类并不熟悉的用户而言,就显得更加重要了。另外,并不是所有的用户都会把他们程序中的Stack替换成Set或Bag,或者把Set或Bag替换成Stack。因此,这种只提供push操作但导致不一致性的接口,在这里可能是更可取的。

接口应该尽可能地保持一致性,但也不能用一致性来代替其他的一切因素。如果一致性导致了类的接口产生对这个类不适当或者负面的影响,那么我们就不应该使用一致性。

本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

时间: 2024-07-30 08:09:28

《C++代码设计与重用》——2.6 接口一致性的相关文章

《C++代码设计与重用》导读

前言 C++代码设计与重用 一切事物都将得到检验并因此被称为问题. Edith Hamilton 这本书的主要目的在于:展示如何以C++编程语言编写可重用代码-就是说,根据不同的需要,在不经过修改,或者经过很少修改的前提下,可重用代码可以很容易地应用到5个.50个甚至500个程序当中,而且这些程序往往是不同程序员编写的,可能运行在不同的系统上.在整个阐述的过程中,我们的目的并不在于争论是否所有的代码都是可重用的,也不在于说明可重用代码能够解决所有的程序问题.显然,不论是对程序员而言,还是对可重用

《C++代码设计与重用》——2.7 转型

2.7 转型 C++代码设计与重用2.7 转型程序库设计者必须充分重视隐式转型(implicit conversion).在C++中,有两种方法可以用来定义从类型From到类型To的隐式转型.第一种,我们可以在类To中定义一个只含一个参数的构造函数(并且没有其他的缺省参数): class To { public: To(const From&); //或者是To(From) //... }; 或者,我们可以在类From中定义一个转型操作: class From { public: operato

《C++代码设计与重用》——2.9 总结

2.9 总结 C++代码设计与重用2.9 总结正规函数-拷贝构造函奴.析构函数.基本赋值运算符.相等运算符和不等运算符-在所有的类中都应该实现相同的语义. 尽管没有最小标准接口,但是nice函数-缺省构造函数.拷贝构造函数.赋值运算符和相等运算符-应该是大多数类都提供的函数.没有任何函数是所有的类都应该提供的函数:而且,绝大多数类都不应该提供浅拷贝和深拷贝操作. 对程序库中类的接口一致性,我们应该给予充分的重视.但是当一致性使类的接口变得很不适当或者不直观时,我们就不能一味顽固地坚持这种一致性.

《C++代码设计与重用》——2.8 const关键字的使用

2.8 const关键字的使用 C++代码设计与重用2.8 const关键字的使用在程序库中,const关键字的正确使用是很重要的.使用const的最大障碍就是用户往往未能正确理解const的意义.接下来,我们将讨论如何解释const,如何使用const,和当我们要改变const的时候,const为什么不能够被重新解释. 2.8.1 抽象const对比位元const 我们可以用好几种方式来解释关键字const.先考虑函数sqrt,它用于计算Rational对象的平方根(这里的Rational指2

《C++代码设计与重用》——1.3 重用的障碍

1.3 重用的障碍 C++代码设计与重用1.3 重用的障碍事实上重用是很难实现的-存在着非技术上和技术上的障碍.非技术上的障碍是指诸如组织结构.社会结构.程序设计文化等阻碍重用的事物.技术障碍是指程序设计本身阻碍重用的各种因素. 1.3.1 非技术障碍 为了理解重用的非技术障碍,我们来考虑重用对C++程序员自身的影响.例如,程序员在一个函数库里设计一个名为Widget的类. 类Widget的设计者应该怀疑设计可重用的Widget类是否会有用处 因为使代码能够被重用是需要时间和精力的,所以大多数程

《C++代码设计与重用》——1.4 希望是否尚存

1.4 希望是否尚存 C++代码设计与重用1.4 希望是否尚存读了这么多关于重用的障碍之后,你可能会怀疑重用性是否还有存在的希望?毫无疑问,希望是存在的.首先,可重用程序库(既有商用的,也有个人使用的)的大量存在和普及就说明了软件重用是完全有可能的.现今就有几个高质量多用途的程序库存在.例如实现链表.集合和字符串等数据结构的程序库:支持开发用户图形接口和窗口应用程序的程序库:另外还有很多专业程序库,例如数据库程序库.远程通信程序库.股票行情和金融分析程序库,物理数字处理程序库,实际上还有很多.相

《C++代码设计与重用》——1.6 练习

1.6 练习 C++代码设计与重用1.6 练习在整本书里,困难的题目将会在题号后面标示(),特别困难的题目会用(*)标示. 1.1 假设你在实现一个函数,这个函数在一个给定的数组中查找某个给定的值. a.在什么情况使用函数时,线性查找的实现比二分查找的实现具有更高的效率. b.为了能在任何环境下,你的查找算法都可以更加高效地执行,你应该如何(使用什么算法)实现这个函数呢? c.假设在这里,我们并不是查找用户给定的值,而是查找第一个0出现的位置,那么需要在什么样的条件下,线性查找才能比二叉查找更有

《C++代码设计与重用》——2.10 练习

2.10 练习 C++代码设计与重用2.10 练习2.1 给出下面被建议为最小标准接口函数的反例: a.输入函数: b.输出函数: c.用字符串返回外层类类名的函数. 2.2 考虑类WORM_Pool,它和2.4.1节的Pool类很相似,但这一点除外,它在只能写一次但可读多次的内存区域分配内存块.那么,类WORD_Pool是析构函数的反例吗?请说明是或不是的原因. 2.3 假设我们为用户提供一个类Buf,它描述一个缓冲区: class Buf { public: Buf(size_t sz);

《C++代码设计与重用》——2.3 Nice类

2.3 Nice类 C++代码设计与重用 2.3 Nice类 我们都知道类会提供某些函数,这些函数要么是在类的代码中被显式声明为公共的(public)或保护的(protected),要么是由编译器在程序需要这些代码时隐式生成的.例如,下面这个类: class X{ public: X(); void f(); }; 它提供了一个缺省构造函数.函数f.一个拷贝构造函数.一个赋值运算符和一个析构函数.而且最后3个函数会在程序需要它们的时候由编译器自动生成. 请考虑下面这个通常有用的函数: templ