Perfecting OO's Small Classes and Short Methods

In The ThoughtWorks Anthology a new book from the Pragmatic Programmers, there is a fascinating essay called "Object Calisthenics" by Jeff Bay. It’s a detailed exercise for perfecting the writing of the small routines that demonstrate characterize good OO implementations. If you have developers who need to improve their ability to write OO routines, I suggest you have a look-see at this essay. I will try to summarize Bay’s approach here.

He suggests writing a 1000-line program with the constraints listed below. These constraints are intended to be excessively restrictive, so as to force developers out of the procedural groove. I guarantee if you apply this technique, their code will move markedly towards object orientation. The restrictions (which should be mercilessly enforced in this exercise) are:

1. Un seul niveau d'indentation

Use only one level of indentation per method. If you need more than one level, you need to create a second method and call it from the first. This is one of the most important constraints in the exercise.

Mauvais exemple

void afficherLesElements(CollectionElement elements) {
    for (Element element : elements) {
        if (element.isCompleted()) {
            fluxDeSortie.write(element.toString()) ;
        }
    }
}

Meilleur exemple

void afficherLesElements(CollectionElement elements) {
    for (Element element : elements) {
        afficherUnElement(element) ;
    }
}

void afficherUnElement(Element element) {
    if (element.isCompleted()) {
        fluxDeSortie.write(element.toString()) ;
    }
}

2. Ne pas utiliser de else

Don’t use the ``else`` keyword. Test for a condition with an if-statement and exit the routine if it’s not met. This prevents if-else chaining; and every routine does just one thing. You’re getting the idea.

Mauvais exemple

void presenterCode(int code) {
    if (code == this.code) {
        this.ratification = 0 ;
        this.etat = VALIDE ;
    } else {
        this.ratification ++ ;
        if (this.ratification == 3) this.etat = INUTILISABLE ;
    }
}

Meilleur exemple

void evaluerCode(int code) throw MauvaisCodeException {
     if (code != this.code)
         throw new MauvaisCodeException() ;
     this.etat = VALIDE ;
}

3. Encapsuler les types primitifs et les chaines

Wrap all primitives and strings. This directly addresses "primitive obsession." If you want to use an integer, you first have to create a class (even an inner class) to identify it’s true role. So zip codes are an object not an integer, for example. This makes for far clearer and more testable code.

Mauvais exemple

private String ville ;
private int codePostal ;

Meilleur exemple

private Ville ville ;
private CodePostal codePostal ;

4. Utilier un seul point par ligne

Use only one dot per line. This step prevents you from reaching deeply into other objects to get at fields or methods, and thereby conceptually breaking encapsulation.

Mauvais exemple

void assertTrue(String message, boolean condition) {
    rapport.current().add(msg + (res ? " OK" : " FAILED")) ;
}

Meilleur exemple

void assertTrue(String message, boolean condition) {
    String result = msg + (res ? " OK" : " FAILED") ;
    rapport.addResult(result) ;
}

5. Ne pas abréger les noms

Don’t abbreviate names. This constraint avoids the procedural verbosity that is created by certain forms of redundancy—if you have to type the full name of a method or variable, you’re likely to spend more time thinking about its name. And you’ll avoid having objects called Order with methods entitled shipOrder(). Instead, your code will have more calls such as Order.ship().

Mauvais exemple

class OpBin {
    private Node g, d ;
    void affOpBin() { ... }
    ...
}

Meilleur exemple

class OperationBinaire {
    private Node filsDroit ;
    private Node filsGauche ;
    void afficher() { ... }
    ...
}

6. Ecrire des petites entités

Keep entities small. This means no more than 50 lines per class and no more than 10 classes per package. The 50 lines per class constraint is crucial. Not only does it force concision and keep classes focused, but it means most classes can fit on a single screen in any editor/IDE.

Mauvais exemple

Rechercher une classe écrite il y a plus de six mois et faisant plus
de 50 lignes. Comprendre ce qu'elle fait, combien a-t-elle de
responsabilités ? Combien a-t-elle de raison de changer ?

Meilleur exemple

public class ServiceEmpruntsImpl implements ServiceEmprunts {
    private FabriqueEmprunts fabriqueEmprunts ;
    public ServiceEmpruntsImpl(FabriqueEmprunts fabrique) {
        fabriqueEmprunts = fabrique ;
    }
    public Emprunt emprunter(Membre membre, Livre livre)
        throws QuotaAtteintException, LivreNonEmpruntableException {
        if (membre.aAtteintSonQuota())
            throw new QuotaAtteintException() ;
        if (livre.estEnConsultationUniquement())
            throw new LivreNonEmpruntableException() ;
        return fabriqueEmprunts.create(membre, livre) ;
    }
}

7. Pas plus de deux attributs par classe

Don’t use any classes with more than two instance variables. This is perhaps the hardest constraint. Bay’s point is that with more than two instance variables, there is almost certainly a reason to subgroup some variables into a separate class.

Mauvais exemple

class Client {
    String prenom ;
    String nom ;
    Rue rue ;
    Ville ville ;
    CodePostal codePostal ;
    Telephone telephone ;
    Courriel courriel ;
}

Meilleur exemple

class Client {
    Identite identite ;
    Adresse adresse ;
}

8. Utiliser des collections de premier ordre

Use first-class collections. In other words, any class that contains a collection should contain no other member variables. The idea is an extension of primitive obsession. If you need a class that’s a subsumes the collection, then write it that way.

Mauvais exemple

// l'usage de la liste se limite à ajouter un élement et parcourir
// la liste

void mesTraitements(List<Element> elements) {
    ...
}

Meilleur exemple

class ElementCollection {
    private List<Element> elements ;
    public void add(Element element) { ... }
    public Iterator<Element> iterator() { ... }
}

void mesTraitements(ElementCollection elements) {
    ...
}

9. Ne pas utiliser d'accesseurs

Don’t use setters, getters, or properties. This is a radical approach to enforcing encapsulation. It also requires implementation of dependency injection approaches and adherence to the maxim "tell, don’t ask."

Mauvais exemple

interface Carrefour {
    Feu getFeuVoieA() ;
    Feu getFeuVoieB() ;
    void changer() ;
}

// dans vehicule

carrefour.getFeuVoieA().getCouleur() ;

Meilleur Exemple

interface Carrefour {
    void abonnerSurVoieA(Vehicule vehicule) ;
}

// vehicule implémente une interface pour être notifié du changement
// de couleur

Taken together, these rules impose a restrictive encapsulation on developers and force thinking along OO lines. I assert than anyone writing a 1000-line project without violating these rules will rapidly become much better at OO. They can then, if they want, relax the restrictions somewhat. But as Bay points out, there’s no reason to do so. His team has just finished a 100,000-line project within these strictures.

Source