Open / Close: It means that you have to make a design so that, when adding new functionality, you do not have to touch the existing code.
Imagine that you have a billing application and several types of clients, defined in an enum, and to invoice you do something like:
switch (cliente.getTipoCliente) {
case TipoClienteA:
facturarClienteA(cliente);
break;
case TipoClienteB:
facturarClienteB(cliente);
break;
If you add a type of client, you have to go to all client-type switches and ifs and add the logic, modifying code that already works.
As an alternative, imagine that you define an interface Facturador
, and that in the enum of TypeClient you have a method that, for each instance, gives you an implementation of Facturador
appropriate to the type of client. Then the above code looks like:
Facturador facturador = cliente.getTipoCliente.getFacturador();
facturador.facturarCliente(cliente);
If you add a new client, you only need to implement the invoice and add the element to the enum.
Other options are plugin systems and so on.
Having said that, I have always found that it is the most complicated criterion to implement, and I apply it only for well-defined blocks (remember that SOLID are recommendations, not absolute rules).
The substitution principle is that an instance of a subclass must always function consistently as an instance of the superclass would. For example, imagine
public class A {
public int valorAbsoluto(int val) {
return val < 0 ? -val : val;
}
}
public class B extends A {
@Override
public int valorAbsoluto(int val) {
return 0;
}
}
It does not make much sense, right? If you pass an instance of B
to a method x that accepts a A
, you may end up having a DivideByCeroError
even though x has made the correct checks.
The problem is that the instance of B
does not act as an instance of A
, even though it is also an instance of A
(because it is an instance of a subclass of A
).
Liskov's principle adds that, in addition, history must be taken into account. Imagine that each B
method complies with the A
implementation, with the exception that after calling each B method you must call a reset()
method, or that the B
methods should be called in a certain order. A code that expects to use instances of A will not take those restrictions into account, which will again result in unexpected / erroneous results.