What is more important: following the SOLID principles, or having a simpler interface?
Interface vis-a-vis SOLID
These are not mutually exclusive. The interface should express the nature of your business model ideally in business model terms. The SOLID principles is a Koan for maximizing Object Oriented code maintainability (and I mean "maintainability" in the broadest sense). The former supports the use and manipulation of your business model and the latter optimizes code maintenance.
Open/Closed Principle
"don't touch that!" is too simplistic an interpretation. And assuming we mean "the class" is arbitrary, and not necessarily right. Rather, OCP means that you have designed your code such that modifying it's behavior does not (should not) require you to directly modify existing, working code. Further, not touching the code in the first place is the ideal way to preserve the integrity of existing interfaces; this is a significant corollary of OCP in my view.
Finally I see OCP as an indicator of existing design quality. If I find myself cracking open classes (or methods) too often, and/or without really solid (ha, ha) reasons for doing so then this may be telling me I've got some bad design (and/or I don't know how to code OO).
Give it your best shot, we have a team of doctors standing by
If your requirements analysis tells you that you need to express the Product-Review relationship from both perspectives, then do so.
Therefore, Wolfgang, you may have a good reason for modifying those existing classes. Given the new requirements, if a Review is now a fundamental part of a Product, if every extension of Product needs Review, if doing so makes client code appropriately expressive, then integrate it into the Product.
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.
Best Answer
Design principles always have to be balanced against each other. You can't predict the future, and most programmers do it horribly when they try. That's why we have the rule of three, which is primarily about duplication, but applies to refactoring for any other design principles as well.
When you only have one implementation of an interface, you don't need to care much about OCP, unless it's crystal clear where any extensions would take place. In fact, you often lose clarity when trying to over-design in this situation. When you extend it once, you refactor to make it OCP friendly if that's the easiest and clearest way to do it. When you extend it to a third implementation, you make sure to refactor it taking OCP into account, even if it requires a little more effort.
In practice, when you only have two implementations, refactoring when you add a third is usually not too difficult. It's when you let it grow past that point that it becomes troublesome to maintain.