拥有一个包含大量虚拟方法的接口?或者有许多接口只有 1 个虚拟方法?

发布于 2024-10-04 01:31:23 字数 1809 浏览 2 评论 0原文

我有一个 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

因为我只有 1 个接口,所以我被迫实现所有书籍的所有内容并返回 0,返回“”或在不相关的实现中不执行任何操作。

另一种方法是将这些接口拆分为多个接口,如下所示:

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;
   };

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

向图书馆添加一本书就变成了这样:

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

拥有不同接口的优点是图书馆很清楚谁支持什么,并且永远不会有调用根本不支持的东西的风险。

然而,由于每本书都可以具有任何可能的特征/功能组合,因此我最终得到了只有一种方法的大量接口。

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

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

有什么想法吗?

I have a C++ module that needs to get information from other classes, without knowing those classes. The obvious approach is to use interfaces.

Let me give you an example. Suppose I have a library that manages books, and all books have their own characteristics and functionalities, and to allow the library to get a characteristic from a book or execute a functionaity, the book needs to implement an interface. Like this:

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;
   };

Unfortunately, it does not make sense to implement all these methods for all kinds of books.

  • Some books don't have images (so I don't want to implement getNofImages())
  • Some books don't have an ISBN code
  • Some books can't be bought, so they don't have a price
  • Some books can't be printed
  • Some books can't be converted to PDF

Because I only have 1 interface, I am forced to implement everything for all books and return 0, return "" or do nothing int he implementation if it is irrelevant.

An alternative could be to split these interfaces in many interfaces, like this:

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;
   };

Book classes then only need to implement the interfaces they really want to support.

Adding a book to the library then becomes something like this:

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

The advantage of having different interfaces is that it is clear for the library who supports what, and it never has the risk of calling something that is simply not supported.

However, since every book can have any possible combination of characteristics/functionalities, I end up with lots of interfaces with only 1 method.

Isn't there a better way to organize the interfaces to obtain something like this?

I was also thinking about using Lambda expressions but behind the screens this is almost the same as having many many interfaces with only 1 method.

Any ideas?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

椵侞 2024-10-11 01:31:23

我会用IBook来实现每一个方法:

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() {}
   };

因为你的替代方案对我来说太混乱了,所以你最终会得到很多令人困惑的界面。另外,您可以检查是否可以在此处应用 访客模式

I'd have IBook to implement every method:

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() {}
   };

since your alternative is too messy for me, you'd end with lots of confusing interfaces. Otherways, you could check if the Visitor pattern could be applied here.

心欲静而疯不止 2024-10-11 01:31:23

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

该中间类的一种常见实现是在每个方法中抛出某种“MethodNotImplemented”异常,以便该类的用户可以根据具体情况捕获这些异常。

考虑到异常对于调用不存在的方法不会是“例外”的情况来说有点昂贵,因此还有一种方法是让这些方法为空或返回默认值,正如 Simone 所发布的。

A solution could be to keep your base interface with pure virtual methods, but have your actual implementations inherit from an intermediate class providing default implementations for the virtual methods.

One common implementation of that intermediate class would be to throw some kind of "MethodNotImplemented" exception in each method, so the user of the class can catch those on a case-by-case basis.

Seeing as exceptions are a bit expensive for such a case where calling non-existing methods would not be "exceptional", there is also the approach of having those methods being empty or returning default values, as Simone posted.

茶色山野 2024-10-11 01:31:23

我想你不需要走极端,而是选择中间道路。
拥有一个接口并不好,另一方面它会破坏接口隔离原则 (ISP)拥有如此多的接口也会破坏你的代码。我会留下一本核心 IBook,然后考虑其余的。例如,IPrintable 和 IConvertible(用于 pdf 转换)可能会进入一个界面。也许 IBuyable 和 IISBNGetter 也会走到一起。

I guess you don't need to get to any extremes but to choose middle way.
Having one interface is not good it breaks Interface Segregation Principle (ISP) on other hand to having so many interfaces will spoil your code as well. I would leave one core IBook and think over the rest. For instance IPrintable and IConvertible(for pdf convert) may go in one interface. Probably IBuyable and IISBNGetter will alse go togehter.

怀里藏娇 2024-10-11 01:31:23

我认为您应该区分实际拥有 ISBN 和实现查询 ISBN 的接口(在本例中为 IBook)。

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

如果您不喜欢返回空值来表示“无”,那也很公平。在某些域中,空字符串是有效值,因此这是不可能的。你可以有:

bool hasISBNcode();
string getISBNcode();

或:

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

或类似的。

否则,在调用 IBook 的任何函数之前,您会发现自己到处都是 dynamic_cast,而且您还会发现自己有 2^n 个不同的具体对象不同类型书籍的课程。或者,在您的示例代码中,您让图书馆参与一本书是否有 ISBN 的事务(这对我来说似乎是错误的 - 它与图书馆无关,它只是这本书的属性)。这些都不是特别有趣的工作,而且它们在这里似乎没有必要。

如果这些事情看起来有必要,你也许可以使用策略。将ConcreteBook 定义为能够使用某些辅助对象来查找 ISBN,而图书类不知道如何执行搜索。然后插入不同的对象来进行查找,具体取决于特定的书是否确实有。不过,对于数据库中某处可能只是一个可为空的列来说,这似乎有点矫枉过正。

I think you should draw a distinction between actually having an ISBN, and implementing an interface that queries ISBN (in this case, IBook).

There's no reason you can't say, as a part of the definition of "book", that a book is something of which "it is possible to find whether it has an ISBN, and if so what".

If you don't like returning empty values to indicate "none", that's fair enough. In some domains, an empty string is a valid value, so that wouldn't even be possible. You could have:

bool hasISBNcode();
string getISBNcode();

or:

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

or similar.

Otherwise, you'll find yourself with dynamic_cast all over the place before you call any of the functions of IBook, and you'll also find yourself with 2^n different concrete classes for different types of books. Or, in your example code, you involve the library in the business of whether a book has an ISBN (which seems wrong to me - it's nothing to do with the library, it's a property of the book alone). None of those is particularly fun to work with, and they don't seem necessary here.

If those kinds of things looked necessary, you could perhaps use strategies. Define ConcreteBook as able to look for an ISBN using some helper object, without the book class knowing how the search is performed. Then plug in different objects to do that looking, depending whether a particular book actually has one or not. Seems a bit overkill, though, for what is probably just a nullable column in a database somewhere.

洋洋洒洒 2024-10-11 01:31:23

另一个解决方案是保留接口,但使用 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;
   };

Another solution would be to keep the interface, but to use boost::optional for return values that can be empty.

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;
   };
江南月 2024-10-11 01:31:23

您可以为每本书提供一个指向基接口单例对象的指针容器,例如 std::map。然后,您需要按名称获取接口,获取指向它的指针(或 null),然后在其上调用一些 doDefault() (或将指针向上转换为 IDerived,如果必须的话)。每个接口函数都必须将指向 Book 的指针作为其第一个(甚至唯一)参数:doDefault(const Book*)

You could have for each book a container of pointers to base-interface singleton objects, something like std::map<std::string, IBase>. Then you would require the interface by name, get the pointer to it (or null), and just call some doDefault() on it (or upcast the pointer to IDerived, if must). Each interface function would have to have pointer to Book as its first (or even only) parameter: doDefault(const Book*).

感悟人生的甜 2024-10-11 01:31:23

有两个稍微(非常轻微!)相关的问题:

  • 逻辑部分层次结构的正确抽象。
  • 空值的可能性。

其他人已经为每个问题提供了解决方案。也就是说,关于第一个,不要追求一切接口,不要追求每个接口单一方法,而是尝试对那里的层次结构进行建模。对于后者,boost::可选的,可能通过单独的查询方法来增强数据项的存在。

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

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

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

在 C++ 中没有意义,只需写 sin 即可。 :-)

干杯&呵呵,,

There are two just slightly (ever so slightly!) related problems:

  • Proper abstraction of logical parts hierarchy.
  • The possibility of null-values.

Others have provided solutions to each problem. Namely, regarding the first, don't go for an everything interface, don't go for single-method-per-interface, but try to model the hierarchy that's there. And regarding the latter, boost::optional, possibly augmented with separate query methods for existence of data item.

I just want to emphasize, which may not be apparent from the answers present as I'm writing this, that they really are two separate problems.

Regarding style (another aspect of clarity), what's all this getSin Javaism stuff?

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

Doesn't make sense in C++, just write sin. :-)

Cheers & hth.,

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文