7

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?

6
  • Solution 1 and solution 2 don't answer the same requirements: In solution 1 there was no need to be able to sell() or open() something. Do you violate the YAGNI principle in solution 2?
    – mouviciel
    May 24, 2013 at 8:00
  • @mouviciel That's the reason I added the "Extended case" part. I wanted to increment the problems and see if Solution 1 is suitable or not, and compare it against Solution 2 May 24, 2013 at 8:24
  • But you can't reject solution 1 on assumptions that are valid only on solution 2. Solution 1 may still be well suited in a specific context where solution 2 would be overengineered.
    – mouviciel
    May 24, 2013 at 8:27
  • @mouviciel You are right, but as we were in Software Design class, we tried to think about good, maintainable solutions preventing too much changes in the future May 24, 2013 at 8:31
  • @mouviciel: Don't invoke YAGNI (which is about implementing features you don't need) to justify inflexible code-design. Perhaps invoke KISS, but I maintain that segregated interfaces are less complex than null methods.
    – pdr
    May 24, 2013 at 9:19

2 Answers 2

11

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.

14
  • Thank you so much for this great explanation. Yeah, you are right indeed, these interfaces don't need to inherit from Drawable. However, is it possible to change slightly this design in order to not break the LSP? May 24, 2013 at 8:22
  • @albertoblaz: Yes, absolutely. LSP, summed up, really says "if Y subclasses/implements X, be sure that Y is really an X". In your case, Y is a Wall. If it implements IMovable, it breaks LSP because it's not movable. You get into more complex debates like whether a Square is a Rectangle (technically yes, programmatically no), but you're still in easily-defined territory here.
    – pdr
    May 24, 2013 at 9:14
  • 1
    ok but if the "App" class wants to manage just IDrawable objects, some of them could be movables and some other don't. Therefore, in some situations you are not able to change Y with X. You solved this issue casting an IDrawable into an ITradable, but doesn't it break the LSP's history constraint? Because you have an extended API in the son (I'm sorry if I am insisting on the same idea every time, it's just I find LSP difficult to understand) May 24, 2013 at 9:45
  • @albertoblaz: It's ok. And no, it doesn't, the History Constraint dictates that you cannot extend the API in a way that causes state changes the parent class wouldn't expect. I'm stretching the analogy a bit here because it applies more to classes than interfaces, but think about an ImmovableObject with a Draw method which assumes the object can't be moved, then deriving from that a Movable object and extending the API by adding a Move method.
    – pdr
    May 24, 2013 at 10:07
  • Checking whether an object which implements Interface1 also implements Interface2, and casting it if it does, is somewhat ugly. Combining the interfaces and having properties that say which members are usable will often create more work on the implementation side (defining stubs for the non-useful methods), but will avoid typecasts on the client side. It may, depending upon class design, also allow for finer-grained reporting over the things that can and cannot be done than would be practical with interfaces alone.
    – supercat
    Feb 24, 2014 at 2:18
1
  1. Solution 1 violates not only Liskov's Substitution Principle but it also violates the Interface Segregation Principle and the Single Responsability Principle. Solution 2 is the correct approach (see correction at the bottom).

  2. Same as above

  3. You can do that and solution 2 at the same time, they are not exclusive.

  4. You should use non-OO solutions and patterns.

CORRECTION: Solution 2 is also wrong. Sellable and Movable must not inherit from Drawable. They should be separate and unrelated interface.

4
  • 1
    A bit more explanation would greatly improve this answer. May 23, 2013 at 23:31
  • Ok thanks but how can I implement the solution given by @pdr using Decorators?. And what non-OO patterns could I use? I'd love to know about that because my experience is a bit limited May 24, 2013 at 8:25
  • @albertoblaz: Oh, I'm sorry, I forgot to answer the Decorators bit. And I really won't have time for a while. That said, it's kind of unrelated to the rest of your questions, and you should stick to one question per post, perhaps you should edit it out of this one and raise a new question about how to implement Decorators (if the site doesn't have such a question yet).
    – pdr
    May 24, 2013 at 9:05
  • @pdr Ok, anyway thank you so much. I will do it later :) May 24, 2013 at 9:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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