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:
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.
To solve this problem, some of my classmates proposed a solution that could be expressed using the following UML diagram:
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.
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:
[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?
Aren't we breaking the Single Responsibility Principle with this super-mega-god-interface?
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
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?