Cohésion & Couplage (1/2)

A mes tous débuts en programmation orientée objet il y a quelques années, je me demandais pourquoi il était si nécessaire d'écrire des classes "courtes" et ayant un rôle "spécifique". Certes, oui, il faut respecter la philosophie objet. Mais franchement, c'est tellement plus simple et plus rapide de mettre tout son code dans une seule classe, surtout quand on n'a pas beaucoup de temps... Erreur fatale! Heureusement très vite, après avoir été confronté à de multiples problèmes, j'ai compris l'importance du respect de principes orientés objet comme la cohésion et le couplage.

Dans cette série de billets, je me propose de vous présenter brièvement ces 2 concepts fondamentaux de la programmation orientée objet, en précisant les avantages d'une architecture à "forte cohésion et faible couplage".

LA COHESION

Si je devais donner une définition, je dirais que la cohésion représente le degré avec lequel une classe a une responsabilité unique et spécifique. Une classe faiblement cohésive est donc une classe qui effectue à elle toute seule des actions diverses et variées.

Bon, un petit exemple (écrit en Java) pour illustrer tout ceci.

package fr.netapsys.training;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class A {

    public String manageUser(final String login, final String password) 
        throws ClassNotFoundException, SQLException {

        String uiMessage = "";
        final User user = new User();

        // first check user parameters
        final boolean isParametersValid = !StringUtils.isEmpty(login) && !StringUtils.isEmpty(password)
                && ( login.length() > 3 ) ;
        // then retrieve everything from database
        if (isParametersValid) {
            Class.forName("org.postgresql.Driver");
            final String url = "jdbc:postgresql://localhost:5433/CBDB";
            final String dbUser = "cashbird";
            final String dbPassword = "cashbird";
            final Connection connection = DriverManager.getConnection(url, dbUser, dbPassword);
            final Statement statement = connection.createStatement();
            final ResultSet resultSet = statement.executeQuery(
                    "SELECT * FROM utilisateur WHERE login='" + login + "' AND password = '" + password+"'");
            while(resultSet.next()){
                user.setFirstname(resultSet.getString("FIRSTNAME"));
                user.setLastname(resultSet.getString("LASTNAME"));
                user.setAuthenticate(true);
            }
            statement.close();
            connection.close();

            // finally format GUI  message
            if (user.isAuthenticate()) {
                uiMessage = "M. " + user.getFirstname() + " " +  user.getLastname() + ", bienvenue chez NETAPSYS !";
            }else {
                uiMessage = "login ou mot de passe incorrect !";
            }
            return uiMessage;
        }else {
            uiMessage = "Vous devez renseignez tous les champs !";
        }
        return uiMessage;
    }
}

Que fait ce code? Il permet à un utilisateur de récupérer ses informations (nom,prénom) d'une base de données, à partir de son login et mot de passe. On commence par valider le login et le mot de passe, ensuite on récupère les informations de la base de données, et enfin on les formatte pour retourner à notre utilisateur un message user-friendly.

Cette classe A n'est pas cohésive : elle effectue à elle seule des opérations qui n'ont pas de lien direct entre elles : validation, récupération de la base, formattage.

Dans la pratique, on aspirera à avoir des classes aussi cohésives que possible. Une méthode amusante qui permet de savoir si une classe est fortement cohésive, est d'essayer de lui donner un nom évoquateur. Si l'on y arrive difficilement, c'est qu'il y a de fortes chances qu'elle ne le soit pas.
En essayant ce petit exercice sur notre classe A, on se rend compte qu'il n'est pas si aisé de lui trouver un nom évoquateur... à moins de forcer et d'obtenir quelque chose du style UserValidatorRetrieverFormator... :).

Bon, appelons-la UserManager. On peut rendre une classe fortement cohésive en la découpant en plusieurs classes qui ont un rôle bien spécifique.
Voici ce que donne la classe UserManager (ex classe A) après ce petit coup de refactoring.

package fr.netapsys.training;
import java.sql.SQLException;
public class UserManager {

    private static UserManager instance;

    private UserManager() {
    }

    public void manageUser(final String login, final String password) 
            throws ClassNotFoundException, SQLException {

        // first check user parameters
        if (UserCredentialChecker.getInstance().check(login, password)) {
            // then retrieve everything from database
            final User user = UserDao.getInstance().retrieve(login, password);
            // finally format GUI  message
            GuiMessageFormater.getInstance().formatUIMessage(user);
        }				
    }

    public static UserManager getInstance(){
        if (instance == null) {
            instance = new UserManager();
        }
        return instance;
    }
}

Plus lisible, n'est ce pas? Voici à quoi ressemble la classe UserCredentialChecker.

package fr.netapsys.training;

public class UserCredentialChecker {

    private static UserCredentialChecker instance;

    public boolean check(final String login, final String password)
    {
        final boolean isAuthenticate = !StringUtils.isEmpty(login) 
                && !StringUtils.isEmpty(password) && (login.length() > 3);
        return isAuthenticate;
    }

    public static UserCredentialChecker getInstance(){
        if (instance == null) {
            instance = new UserCredentialChecker();
        }
        return instance;
    }
}

Et la classe UserDao.

package fr.netapsys.training;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class UserDao {

    private static UserDao instance;

    public User retrieve(final String login, final String password)
        throws ClassNotFoundException, SQLException{		  

        final User user = new User();
        Class.forName("org.postgresql.Driver");
        final String url = "jdbc:postgresql://localhost:5433/CBDB";
        final String dbUser = "cashbird";
        final String dbPassword = "cashbird";
        final Connection connection = DriverManager.getConnection(
                url, dbUser, dbPassword);
        final Statement statement = connection.createStatement();
        final ResultSet resultSet = statement.executeQuery(
                "SELECT * FROM utilisateur WHERE login='" +
                login + "' AND password = '" + password+"'");

        while(resultSet.next()){
            user.setFirstname(resultSet.getString("FIRSTNAME"));
            user.setLastname(resultSet.getString("LASTNAME"));
            user.setAuthenticate(true);
        }
        statement.close();
        connection.close();
        return user;
    }

    public static UserDao getInstance(){
        if (instance == null) {
            instance = new UserDao();
        }
        return instance;
    }
}
Et enfin, la classe GuiMessageFormater.
package fr.netapsys.training;

public class GuiMessageFormater {

    private static GuiMessageFormater instance;

    public String formatUIMessage(final User user){
        final StringBuilder uiMessage = new StringBuilder();	
        if (user.isAuthenticate()) {
            uiMessage.append("Mr " + user.getFirstname() +
                    " " + user.getLastname() + " ,bienvenue chez NETAPSYS !");
        }else {
            uiMessage.append("login ou mot de passe incorrect !");
        }
        return uiMessage.toString();
    }

    public static GuiMessageFormater getInstance(){
        if (instance == null) {
            instance = new GuiMessageFormater();
        }
        return instance;
    }
}

La classe UserManager délègue désormais les 3 traitements à des classes spécialisées :
- UserCredentialChecker pour la validation des paramètres de l'utilisateur.
- UserDao pour la récupération de données de la base
- GuiMessageFormater pour le formattage des données

Cette 2e architecture est plus cohésive que la 1e, car chacune de ces 3 classes a un rôle bien spécifique ( NB: la classe UserDao peut être rendue encore plus cohésive en séparant le code réalisant la connexion à la base du code effectuant la récupération des données; mais l'exemple a été laissé tel quel pour des soucis de simplicité.)

On peut distinguer plusieurs avantages d'une architecture à forte cohésion :

- clarté : Si ma classe a un rôle bien spécifique, alors elle aura relativement moins de lignes de code, et sera donc plus lisible. Au lieu de passer plusieurs minutes à la lire et essayer de la comprendre, on peut en quelques secondes se faire une idée de ce qu'elle fait. Avoir un code clair est particulièrement important dans le monde de l'entreprise, où plusieurs développeurs peuvent être amenés à travailler sur le même code.

- maintenabilité : Avec une architecture forte cohésion, il est plus facile de maintenir le code. D'une part, parce que si l'on veut effectuer une modification, on sait exactement dans quelle partie du code aller pour le faire. Pour reprendre notre exemple de tout à l'heure, on saura qu'il faut aller directement dans la classe GuiMessageFormater pour regarder le formattage des données au lieu de se perdre dans la classe A...

D'autre part, parce qu'il est plus facile de travailler sur une architecture cohésive à plusieurs. Supposons par exemple, qu'un développeur doive modifier le code de récupération des données, et un autre doive retravailler le formattage. Dans le 1e cas, il pourrait y avoir rapidement des conflits au moment du merge; alors que dans le second, chacun peut apporter les modifications dans la classe correspondante, sans gêner l'autre.

- extensibilité : Un des inconvénients de l'architecture n°1 est qu'elle n'est pas extensible. Si à l'avenir on doit rajouter d'autres traitements de gestion d'utilisateur, notre classe va très vite déborder et devenir complètement illisible. Par contre, dans la 2e architecture, il suffira tout simplement de déléguer le traitement à la nouvelle classe spécialisée et le tour est joué.

  public void manageUser(final String login, final String password) 
            throws ClassNotFoundException, SQLException {

        // first check user parameters
        if (UserCredentialChecker.getInstance().check(login, password)) {

            // then retrieve everything from database
            final User user = UserDao.getInstance().retrieve(login, password);

            // finally format GUI  message
            GuiMessageFormater.getInstance().formatUIMessage(user);

            // rajout d'une nouvelle fonctionnalité
	    UserAdditionnalTreator.getInstance().treat(user);
        }				
    }

- testabilité : L'écriture des tests unitaires en est aussi facilitée. L'on peut voir clairement que dans le 1e cas,on peut difficilement tester la méthode manageUser(). Les méthodes check(), retrieve() et formatUIMessage(), sont par contre beaucoup plus simples à tester.

- réutilisabilité : Puisque chaque classe a un rôle unique, elle peut être utilisée dans un autre contexte. Supposons par exemple que l'on veuille ,dans un autre module, effectuer à nouveau le formattage du message utilisateur.

Dans le 1e cas, l'on serait bien obligé de dupliquer ce code; ce qui n'est pas une très bonne pratique; car à l'avenir,si l'on doit changer l'implémentation du formattage, on devra le faire dans toutes les classes où l'on a dupliqué ce code.

Par contre, avec une architecture cohésive, il suffira de faire un appel à la méthode formatUIMessage(user) ( de GuiMessageFormater) pour réaliser le formattage. Ainsi on ne duplique pas le code et si une modification s'impose par la suite, elle se fera uniquement dans la classe GuiMessageFormater !


En somme, écrire du code cohésif n'est pas seulement une "bonne pratique OO", mais un passage nécessaire et indispensable afin de pouvoir obtenir un code efficace et robuste. Toutesfois, il faut trouver le bon équilibre entre granularité et simplicité, car en voulant faire trop cohésif parfois, on pourrait en arriver à créer une usine à gaz avec des classes d'1 ligne...

Un commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.