Version without JavaScript, read-only: workshops, quizzes and running code are not available here. Full version · Catalogue · Sign in
Lecture notes · chapter 13
The limits of OOP and other paradigms
The weaknesses of object-oriented programming, other paradigms, and a few questions to ask before adding an abstraction. A chapter to read.
The weaknesses of OOP
The rigidity of hierarchies
It is dealt with in chapter 4. A class hierarchy fixes, at the start of a project, choices whose consequences only become clear much later. The usual recommendation is therefore to use composition by default, and to use inheritance only when substitutability is real.
Verbosity
Before Java 8, sorting a list required an anonymous class:
Collections.sort(canards, new Comparator<Canard>() {
public int compare(Canard a, Canard b) {
return Integer.compare(a.getPoids(), b.getPoids());
}
});
Filtering then sorting required two, plus a loop:
List<Canard> legers = new ArrayList<Canard>();
for (Canard c : canards) {
if (c.estEnPlastique() && c.getPoids() <= 200) legers.add(c);
}
Collections.sort(legers, new Comparator<Canard>() {
public int compare(Canard a, Canard b) { return a.getCouleur().compareTo(b.getCouleur()); }
});
That is a lot of code for two simple operations. Java 8 introduced lambdas and streams, which make it possible to write:
List<String> couleurs = canards.stream()
.filter(c -> c.estEnPlastique() && c.getPoids() <= 200)
.map(c -> c.getCouleur().toUpperCase())
.collect(Collectors.toList());
The code is shorter and its intention can be read directly. It is still Java, with a lighter syntax.
Over-engineering
Suppose the only need is to create, read, update and delete a duck. For this need, one sometimes finds six interfaces (ServiceCanard, DepotCanard, UniteDeTravail, MappeurCanard, FabriqueDepot, FabriqueMappeur), four factories, DTOs, mappers and transactions.
public void creer(CanardDto dto) {
UniteDeTravail u = FabriqueUnite.creer();
DepotCanard depot = depots.creerDepotCanard(u);
MappeurCanard map = mappeurs.creer();
depot.sauver(map.depuisDto(dto));
u.valider();
}
The same need, without unnecessary abstraction:
public final class ServiceCanard {
private final DepotCanard depot;
public ServiceCanard(DepotCanard depot) { this.depot = depot; }
public void creer(Canard c) { depot.sauver(c); }
}
Other paradigms
Functional programming
final class Canard {
private final String nom;
private final int poids;
Canard(String nom, int poids) { this.nom = nom; this.poids = poids; }
String nom() { return nom; }
int poids() { return poids; }
}
static int rationAliment(Canard c) {
int base = c.estEnPlastique() ? 0 : 50;
return base + Math.max(0, (c.poids() - 100) / 10);
}
The data is immutable and the function has neither state nor side effects: its test fits in two lines and needs no mock object.
A common way of organising code is called functional core, imperative shell: the business rules are written as pure functions, and OOP is used around them for input/output, orchestration and long-lived state.
Data-oriented programming
The shape of the data is described first, and the behaviour afterwards. This is the usual approach for APIs and storage.
{ "id": "D42", "type": "plastique", "poids": 180 }
Recent versions of Java make it possible to write this in one line with a record. The site compiles in Java 8, where a complete immutable class is still needed: this is the verbosity described above, and records were created to avoid it.
The ECS model and the actor model
The ECS model (entity, component, system) pushes composition to the extreme: the entity is only an identifier, components are data without logic, and systems go through the entities that have a given component. It is used mainly in video games and simulation.
In the actor model (Akka), each actor has an isolated state and communicates only through messages. Since no state is shared, locks are unnecessary. This model is used for concurrent and fault-tolerant systems.
Neither model is on the syllabus; they are mentioned so that you know their names.
The decision rules
The checklist
- Have I imposed useless methods through inheritance?
- Can I replace this inheritance with a role (an interface) and composition?
- Is this code verbose without bringing anything?
- Does this pattern solve a real problem, present in the current code?
- Does the abstraction I have just created simplify the code, or does it only serve to reassure me?
- Is the unit test easier to write after this choice than before?