Object-Oriented Design – Should an Interface Have Many Methods to Preserve Liskov’s Principle?

liskov-substitutionobject-orientedobject-oriented-designsingle-responsibilitysolid

I'm currently studying a course based on Software Design and I had a discussion in class with my professor and some classmates about a problem represented by the next scenario:

Scenario

Imagine we have a graphic application which lets us plan the interior design of our future house using a perspective from top. We are able
to add or remove some elements like furniture and change their
position, but obviously, we can't move the walls of the house because
it was already built.

Solution 1

To solve this problem, some of my classmates proposed a solution that could be expressed using the following UML diagram:

UML diagram for Solution 1

As you can see, they agreed on the use of a common interface called "Drawable" which represents the graphical objects displayed on the UI. The general class "App" manages a list of Drawables, and each of them has a set of methods. This interface is implemented by different classes such as Furniture, Wall or Window. The thing is that a Furniture object could be moved, but not the Walls nor the Windows. So, a Furniture would implement the 'move' method defined in Drawable. In contrast, Wall and Window simply will write it empty.

At this point, other classmates and I complained about this decission because the design does not require to fulfill with the constraints (like moving or not Drawable objects, depending on their nature). This way, the design will allow some new objects could be moved, although they probably shouldn't be. However, the professor said this design is good because it's flexible and transparent for the App class, because this class doesn't know what kind of instance is managing.

Extended case

In addition, we could think on an extreme case where we add several methods to the Drawable interface, such as 'sell', 'open' and 'lend'. Using the same approach described above, we will have an interface which could be able to do anything, like Superman. Therefore, I believe this is a bad design solution due to we are mixing behaviors which belong to different concepts (Movable, Sellable, Openable, and so on). Also, we are allowing a Wall object could implement the 'sell' method, which completely does not make sense… but my professor still insisted on his point of view and he didn't see the problem.

Solution 2 (based on the extended case)

Other people suggested we could add those interfaces (Movable, Sellable, Openable) and each of them would inherit from the Drawable interface. That way:

  • A Wall would implement Drawable directly
  • A Furniture would implement Movable and Sellable
  • A Window would implement just Openable

The next diagram summarizes this approach:

UML Diagram for Solution 2

[Edit: The Drawable's move method should be removed, this is a mistake because it is now placed into the Movable interface]

Questions and Doubts

Finally, once you can figure out the problem, could you help me and answer these questions, please?

  1. Aren't we breaking the Single Responsibility Principle with this super-mega-god-interface?

  2. Are both approaches valid against the Liskov's Substitution Principle? I think it's broken in the first case because the post-conditions are broken due to some methods don't do anything. Anyway, in the second approach I'm not sure as well because of the interfaces inheritance tree

  3. Maybe, another alternative could be the definition of basic Drawable objects (with 'click' and 'draw' methods). Thus, we may take advantage of some kind of mechanism like the Decorator pattern in order to add behaviors dynamically. But how?

  4. If we use a language without interfaces such as JavaScript or Python, how can we deal with this problem?

Best Answer

Aren't we breaking the Single Responsibility Principle with this super-mega-god-interface?

Not exactly, because the interface isn't doing anything. It has no responsibilities.

You are breaking Liskov Substitution Principle and Interface Segregation Principle though.

LSP because you're writing methods that do nothing, just to make it fit the interface (ie. What is true of IDrawable -- that you can move it -- is not true of Wall).

ISP because you'll have situations like a trade method, which should have access to Buy and Sell methods, but will also have access to Draw.

Are both approaches valid against the Liskov's Substitution Principle? I think it's broken in the first case because the post-conditions are broken due to some methods don't do anything. Anyway, in the second approach I'm not sure as well because of the interfaces inheritance tree

It's just as broken in the second case as the first.

Edit: On second thoughts, it isn't. But it is still broken. It doesn't seem automatically true that everything that could have a Move method will also have a Draw method. It might be true in your case but, if you later find yourself wanting to pass an object with a Move method into a method that receives an IMovable, you're going to have to implement Draw on that object, even if you don't need it.

And it gains you nothing over a third solution ...

Maybe, another alternative could be the definition of basic Drawable objects (with 'click' and 'draw' methods). Thus, we may take advantage of some kind of mechanism like the Decorator pattern in order to add behaviors dynamically. But how?

The best alternative is to segregate your interfaces fully. You can still have objects with lots of methods (but Dependency-Inversion Principle and Single Responsibility Principle dictate you must push the logic down into services), but they only have to implement the interfaces they need. Wall shouldn't implement IMovable, if it can't be moved.

public class Wall : IDrawable
public class Furniture : IDrawable, IMovable, ISellable
public class Window : IDrawable, IOpenable

Nothing here dictates that any of those interfaces must derive from another.

public interface IDrawable
{
    void Draw(Canvas canvas);
}

public interface IMovable
{
    void MoveTo(int x, int y);
}

etc

You should then manage your list of IDrawables, which you can only be sure implement Draw(), and cast to see if other methods are available.

For example,

ITradable tradable = drawable as ITradable;
if (tradable != null)
{
    tradable.Trade(buyer, seller);
}

You're going to have to do the above in your option 2 anyway, so why tie everything to IDrawable?

If we use a language without interfaces such as JavaScript or Python, how can we deal with this problem?

OO languages tend to support at least one of three things:

Prototypical languages, such as Javascript, support ... well, prototypes, which allow for a slightly modified version of duck-typing (in prototypes, you generally ask if a method exists before calling it).

Other paradigms generally handle these issues in their own way.