代码之家  ›  专栏  ›  技术社区  ›  Patrick

一个接口有很多虚拟方法?或者只有一个虚拟方法有很多接口?

  •  5
  • Patrick  · 技术社区  · 14 年前

    我有一个C++模块,需要从其他类中获取信息,而不知道这些类。显而易见的方法是使用接口。

    我给你举个例子。假设我有一个管理图书的图书馆,所有的图书都有自己的特点和功能,为了让图书馆从一本书中获得一个特点或执行一个功能,图书需要实现一个接口。这样地:

    class Library
       {
       public:
          void addBook(IBook &book);
       };
    
    class IBook
       {
       public:
          string getAuthor()    = 0;
          string getTitle()     = 0;
          string getISBNCode()  = 0;
          size_t getNofPages()  = 0;
          size_t getNofImages() = 0;
          double getPrice()     = 0;
          void   printBook()    = 0;
          void   convertToPdf() = 0;
       };
    

    不幸的是,对所有种类的书实现所有这些方法是没有意义的。

    • 有些书没有图像(所以我不想实现getNofImages())
    • 有些书没有ISBN代码
    • 有些书买不到,所以没有价格
    • 有些书不能印刷
    • 有些书不能转换成PDF格式

    因为我只有一个接口,所以我必须为所有书籍实现所有内容,如果不相关,则返回0、return“”或在实现中不执行任何操作。

    另一种选择是将这些接口拆分为许多接口,如:

    class IBook
       {
       public:
          string getAuthor()    = 0;
          string getTitle()     = 0;
          size_t getNofPages()  = 0;
       };
    
    class IISBNGetter
       {
       public:
          string getISBNCode()  = 0;
       };
    
    class IImagesGetter
       {
       public:
          size_t getNofImages() = 0;
       };
    
    class IBuyable
       {
       public:
          double getPrice()     = 0;
       };
    
    class IPrintable
       {
       public:
          void   printBook()    = 0;
       };
    
    class IConvertible
       {
       public:
          void   convertToPdf() = 0;
       };
    

    书籍类只需要实现它们真正想要支持的接口。

    将一本书添加到库中,然后会变成这样:

    bookid = myLibrary->addBook (myBook);
    myLibrary->setISBNGetter  (bookid, myBook);
    myLibrary->setImageGetter (bookid, myBook);
    myLibrary->setBuyable     (bookid, myBook);
    

    拥有不同接口的好处是,对于支持什么的库来说是很清楚的,并且它永远不会有调用不支持的东西的风险。

    然而,由于每本书都可以有任何可能的特性/功能组合,因此我最终只使用了一个方法就得到了许多接口。

    有没有更好的方法来组织接口来获得这样的东西?

    我也在考虑使用Lambda表达式,但在屏幕后面,这几乎与许多接口只有一个方法是一样的。

    有什么想法吗?

    7 回复  |  直到 14 年前
        1
  •  8
  •   Simone    14 年前

    我会的 伊布克 要实现每种方法:

    class IBook
       {
       public:
          virtual ~IBook() {}
    
          virtual string getAuthor() { return ""; } // or some other meaningful default value
          virtual string getTitle() { return ""; }
          virtual string getISBNCode() { return ""; }
          virtual size_t getNofPages() { return 0; }
          virtual size_t getNofImages() { return 0; }
          virtual double getPrice() { return .0; }
          virtual void   printBook() {}
          virtual void   convertToPdf() {}
       };
    

    因为你的选择对我来说太混乱了,你会以许多混乱的界面结束。在其他方面,你可以检查 Visitor pattern 可以在这里申请。

        2
  •  2
  •   SirDarius    14 年前

    一个解决方案可以是使用纯虚拟方法保留您的基本接口,但是让您的实际实现继承自为虚拟方法提供默认实现的中间类。

    中间类的一个常见实现是在每个方法中抛出某种“MethodNotImplemented”异常,这样类的用户就可以逐个捕获这些异常。

    在这种情况下,对于调用不存在的方法不会是“例外”的情况来说,有点贵,也有方法使这些方法为空或返回默认值,如Simone所示。

        3
  •  2
  •   Arseny    14 年前

    我想你不需要走极端,只要选择中间路线。 只有一个接口是不好的 Interface Segregation Principle (ISP) 另一方面,拥有这么多接口也会破坏代码。我会留下一本核心的IBook,然后再考虑其他的。例如,IPrintable和IConvertible(用于pdf转换)可以放在一个接口中。也许我和伊斯宾杰也会走到一起。

        4
  •  2
  •   Steve Jessop    14 年前

    我觉得你应该区分一下 一个ISBN,并实现一个查询ISBN的接口(在本例中是IBook)。

    作为“书”定义的一部分,你没有理由不说一本书是“有可能发现它是否有ISBN,如果有,又是什么”的东西。

    如果您不喜欢返回空值来表示“无”,那就足够了。在某些域中,空字符串是一个有效值,因此这甚至是不可能的。你可以:

    bool hasISBNcode();
    string getISBNcode();
    

    或:

    std::pair<bool, string> getISBNcode();
    

    或类似的。

    否则,你会发现自己 dynamic_cast 在你调用 IBook ,你也会发现自己有2^n个不同的具体类,用于不同类型的书籍。或者,在您的示例代码中,您让图书馆参与图书是否有ISBN的事务(这在我看来是错误的——这与图书馆无关,它只是图书的一个属性)。这些都不是特别有趣的工作,他们似乎没有必要在这里。

    如果这些事情看起来是必要的,你也许可以使用策略。定义 ConcreteBook 尽可能 对于使用某个helper对象的ISBN,book类不知道如何执行搜索。然后插入不同的对象进行查找,这取决于一本特定的书是否真的有一本。不过,这似乎有点过头了,因为数据库中可能只有一个可以为空的列。

        5
  •  1
  •   BЈовић    14 年前

    另一个解决方案是保留接口,但对可以为空的返回值使用boost::optional。

    class IBook
       {
       public:
          virtual ~Ibook(){}
    
          virtual string getAuthor()    = 0;
          virtual string getTitle()     = 0;
          virtual string getISBNCode()  = 0;
          virtual size_t getNofPages()  = 0;
          virtual size_t getNofImages() = 0;
          virtual boost::optional< double > getPrice()     = 0;   // some have no price
          virtual void   printBook()    = 0;
          virtual void   convertToPdf() = 0;
       };
    
        6
  •  1
  •   Dialecticus    14 年前

    对于每本书,都可以有一个指向基本接口singleton对象的指针容器,比如 std::map<std::string, IBase> . 然后按名称要求接口,获取指向它的指针(或null),然后对它调用doDefault()(如果必须,也可以将指针上移到IDerived)。每个接口函数都必须将指向Book的指针作为其第一个(甚至是唯一的)参数: doDefault(const Book*) .

        7
  •  1
  •   Cheers and hth. - Alf    14 年前

    有两个只是轻微的(从来没有如此轻微!)相关问题:

    • 逻辑部件层次结构的适当抽象。
    • 空值的可能性。

    其他人则为每一个问题提供了解决方案。也就是说,对于第一个接口,不要使用everything接口,不要使用每个接口的单个方法,而是尝试对那里的层次结构进行建模。关于后者, boost::optional ,可能使用单独的查询方法来扩充数据项的存在。

    我只想强调,在我写这篇文章的时候,从现在的答案中可能看不出来,它们确实是两个独立的问题。

    关于风格(清晰度的另一个方面),这是什么 getSin 爪哇主义的东西?

    x = 2*getSin(v)/computeCos(v)

    在C++中没有意义,只是写 sin . :-)

    干杯。,