It is not difficult to state the good principles of design but identifying the right abstractions that really support low coupling and high cohesion is not easy and may take a long time. For instance, although most programming languages use the same abstractions to implement their collections libraries, the identification of the right abstraction took several years and experiments in different programming languages.
Additionally, an abstraction is not an absolute characteristic of an entity. For instance, what is the abstraction of animal? A living entity? Not necessarily, note that animal means different thing for the butcher and for the zoo. So, the right abstraction of animal depends on where it is going to be used. A software system to support the management of a zoo will use a different abstraction of animal than one which goal is to manage a butcher shop.
Using the wrong abstractions does not reduce the propagation of changes, because, some time in the future, the abstraction will have to change. The right abstractions depend on their use and this is an argument to define abstractions bottom-up, by refactoring the code. Instead of trying to find the right abstraction, abstractions are identified and evolve as more functionalities are being implemented. This will avoid the problem known as the analysis paralysis, where developers try to find the right abstraction before starting implementing the functionality. However, if the abstractions are already known it does not make sense to find them by refactoring the code. For instance, it is not necessary to rediscover the abstractions for collections whenever we develop a new system.
The interface hides the implementation but exposes the abstraction. So, it is relevant how the abstraction is exposed. Usually an interface defines a set of methods, or services, that can be used by the caller. There are several techniques of implementing interfaces:
- The interface contains complex entities as parameters – In this case the caller depends on the structure of the parameter and needs to change whenever it changes.
- The interface contains only simple data types of a particular programming language – In this case the caller only depends on the programming language of the interface.
- The interface is programming language independent – It further reduces coupling because the caller does not depend on the programming language that implements the interface. For instance, this is the case of WSDL interfaces.
- Contains the meta-model – The interface is self-contained because it describes itself. It reduces coupling because the caller knows at runtime what is the structure of the data being passed. Note, that complex entities can be passed but the interface contains the schema that defines the complex entities and expects the caller to interpret them and adapt its behavior accordingly. Service discovery is an example where interface coupling is syntactically reduced to a minimum, but the client needs to be able to interpret that data that it receives.