1

Consider an Eg. of Library Management System, we can have a design where in a student can search for a book based on it's title from the list of books using a function, something like below (in c++)

  1. std::vector<std::string> GetBookListBasedOnTitles(std::string title, std::vector<string> books) {
      // iterate through list of books and add a book to the result list if it's title is same as the title to be searched.
      // Return the result list.
    }
    

or

  1. shall we make an interface "Search" that will be implemented by "SearchByTitle" class and designate the responsibility of search functionality to that class.

Are there any advantages of option 2 over option 1? If yes, could you please give some scenarios explaining the same?

Shall we always make interfaces which can then be implemented by other classes to provide functionalities like Search, AddBook, AddMember etc. for the system? The negative which I see in this approach is there would be alot of interfaces for as for every small functionality, we have an interface.

How to decide when to provide an interface for a functionality?

  • Basically, when there are possibilities for you to provide such functionalities in different ways. Instead of thigthly coupling your classes to a concrete dependency that solves a problem,, you leave that dependency open to different and possible implementations.. – Laiv Mar 15 at 23:22
1

In general, create a new interface for an implementing class when:

  1. You need to provide more than one implementation of a capability,
  2. You need to decorate a class with a capability such as Enumerable or Equatable, or
  3. You need multiple inheritance and you're not using C++.
  4. You need to establish an API without having an implementation first (though a prototype class with stub methods would also serve this purpose).

In general, create a new class when:

  1. You have a new area of responsibility that you need to implement,
  2. Your class has become too complicated and you need to break it into smaller classes.
  3. You are implementing an architecture that requires classes in specific places.
3

You are massively misunderstanding the “single responsibility principle”. Create one single class that has the single responsibility of handling book management tasks. This class shouldn’t have the responsibility to pay salaries of the library employees, for example.

0

Philosophy of this problem

One of my favorite quotes is:

Everything should be made as simple as possible, but no simpler. [1]

This quote captures what we want to achieve when designing a good interface: our interface makes things as simple as possible, without making them so simple it causes problems.

Things to consider when using a function

Here are the questions I have looking at your function:

  • Do we have to copy all titles in the library into a vector in order to use it?
  • Does each string contain only the title, or does it contain additional information about the book (like the author or the publication date)?
  • How efficiently can this search be implemented?
  • What if we want to search for something other than a title?

Things to consider when using an interface

  • Do we have multiple separate implementations that serve a similar purpose?
  • Does the interface provide any benefits over a regular function?
  • Is the interface easy to use for the most common cases?

Proposed Architecture

We have a Database interface (for communicating with the library database), and a Query class (which may or may not be an interface - that's up to you). Searching is handled by the Database interface, and the specific method of searching you want to use is a property of the Query class.

struct LibraryBook {
    std::string title;
    std::string author;
    std::string ISBN; 
    std::string publishing_company;
    int publication_year;
};
class Query; //Base class for a search query

class LibraryDatabase {
   public: 
    virtual void add(LibraryBook const& book) = 0;
    virtual void remove(LibraryBook const& book) = 0; 
    virtual std::vector<LibraryBook> search(Query& query) const = 0; 
    virtual ~LibraryDatabase() {}
};
class DefaultDatabase : public LibraryDatabase {
    // Default implementation goes here
};
// We can wrap a concrete class around it:
class Library : public LibraryDatabase {
     std::unique_ptr<Database> database;

    public:
     // Factory function 
     template<class Database, class... Args>
     static Library make(Args&&... database_args) {
         return Library(new Database(std::forward<Args>(database_args)...)); 
     }
     Library() : database(new DefaultDatabase()) {}
     explicit Library(LibraryDatabase* db) : database(db) {}

     //Fields go here;
     std::vector<LibraryBook> search(Query& query) const {
         return database->search(query);
     }
    //Fields go here;
     void add(LibraryBook const& book) const {
         return database->add(book);
     }
     void removeLibraryBook const& book) const {
         return database->remove(book);
     }
};

[1]: This quote is often attributed to Einstein

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.