web-dev-qa-db-fra.com

Nettoyer le code pour supprimer la condition de commutateur (à l'aide de polymorphisme)

Comme le disent les principes SOLID, il est préférable de supprimer les conditions de commutation en les convertissant en classes et en interfaces. Je veux le faire avec ce code:

Remarque: Ce code n'est pas un code réel et je viens d'y mettre mon idée.

MessageModel message = getMessageFromAnAPI();
manageMessage(message);
...
void manageMessage(MessageModel message){        
    switch(message.typeId) {
        case 1: justSave(message); break;
        case 2: notifyAll(message); break;
        case 3: notify(message); break;
    }
}

Maintenant, je veux supprimer l'instruction switch. Donc, je crée des classes pour cela et j'essaye d'implémenter un polymorphisme ici:

interface Message{
    void manageMessage(MessageModel message);
}
class StorableMessage implements Message{

    @Override
    public void manageMessage(MessageModel message) {
        justSave(message);
    }
}
class PublicMessage implements Message{

    @Override
    public void manageMessage(MessageModel message) {
        notifyAll(message);
    }
}
class PrivateMessage implements Message{

    @Override
    public void manageMessage(MessageModel message) {
        notify(message);
    }
}

puis j'appelle mon API pour obtenir ma MessageModel:

MessageModel message = getMessageFromAnAPI();

Maintenant mon problème est ici. J'ai mon modèle et je veux le gérer en utilisant mes cours. Comme exemples SOLID, je devrais faire quelque chose comme ceci:

PublicMessage message = new Message();
message.manageMessage(message);

Mais comment puis-je savoir quel type est lié à ce message pour en faire une instance (PublicMessage ou StorableMessage ou PrivateMessage)?! Devrais-je mettre le commutateur ici encore pour le faire ou quoi?

13
Siamak Ferdos

Dans ce cas, vous pouvez utiliser une fabrique pour obtenir l'instance de Message. L'usine aurait toutes les instances de Message et renverrait l'instance appropriée en fonction du typeId du MessageModel.

class MessageFactory {
    private StorableMessage storableMessage;
    private PrivateMessage privateMessage;
    private PublicMessage publicMessage;
    //You can either create the above using new operator or inject it using some Dependency injection framework.

    public getMessage(MessageModel message) {
        switch(message.typeId) {
            case 1: return storableMessage; 
            case 2: return publicMessage;
            case 3: return privateMessage
            default: //Handle appropriately
        }
    }
}

Le code d'appel ressemblerait à

MessageFactory messageFactory; //Injected 
...
MessageModel messageModel = getMessageFromAnAPI();

Message message = messageFactory.getMessage(messageModel);
message.manageMessage(messageModel);

Comme vous pouvez le constater, cela n’a pas complètement éliminé la switch (et vous n’aurez pas besoin, car utiliser switch n’est pas mauvais en soi). Ce que SOLID essaie de dire, c'est de garder votre code propre en suivant SRP (Principe de responsabilité unique) et OCP (Principe d'ouverture/fermeture) ici. Cela signifie que votre code ne devrait pas avoir la logique de traitement réelle à gérer pour chaque typeId à un endroit.

Avec la fabrique, vous avez déplacé la logique de création vers un emplacement séparé et vous avez déjà déplacé la logique de traitement réelle vers les classes respectives.

EDIT: Juste pour répéter - ma réponse est centrée sur l’aspect SOLID du PO. En disposant de classes de gestionnaire distinctes (une instance de Message à partir de l'OP), vous obtenez le SRP. Si l'une des classes de gestionnaire change ou que vous ajoutez un nouveau message typeId (message.typeId) (c'est-à-dire, ajoutez une nouvelle implémentation Message), vous ne devez pas modifier l'original et vous devez donc atteindre OCP. (En supposant que chacun de ceux-ci ne contient pas de code trivial). Celles-ci sont déjà effectuées dans le PO.

Le vrai point de ma réponse ici est d’utiliser une Factory pour obtenir une Message. L'idée est de garder le code de l'application principale propre et de limiter l'utilisation des commutateurs, si/else et des nouveaux opérateurs au code d'instanciation. (Similaire aux classes @Configuration/aux classes qui instancient des Beans lors de l’utilisation de modules Spring ou Abstract dans Guice). Les OO principes ne disent pas que l’utilisation de commutateurs est mauvaise. Cela dépend de vous l'utilisez. Son utilisation dans le code de l'application enfreint les principes SOLID et c'est ce que je voulais faire ressortir. 

J'aime aussi l'idée de daniu @ d'utiliser une manière fonctionnelle et la même chose peut même être utilisée dans le code d'usine ci-dessus (ou peut même utiliser une simple carte pour se débarrasser du commutateur).

10
user7

Tu peux le faire:

static final Map<Integer,Consumer<MessageModel>> handlers = new HashMap<>();
static {
    handlers.put(1, m -> justSave(m));
    handlers.put(2, m -> notifyAll(m));
    handlers.put(3, m -> notify(m));
}

Cela supprimera votre commutateur à

Consumer<Message> consumer = handlers.get(message.typeId);
if (consumer != null) { consumer.accept(message); }

Principe de séparation des opérations d'intégration

Vous devriez bien sûr encapsuler ceci:

class MessageHandlingService implements Consumer<MessageModel> {
    static final Map<Integer,Consumer<MessageModel>> handlers = new HashMap<>();
    static {
        handlers.put(1, m -> justSave(m));
        handlers.put(2, m -> notifyAll(m));
        handlers.put(3, m -> notify(m));
    }
    public void accept(MessageModel message) {
        Consumer<Message> consumer = handlers.getOrDefault(message.typeId, 
                m -> throw new MessageNotSupportedException());
        consumer.accept(message);
    }
}

avec votre code client

message = getMessageFromApi();
messageHandlingService.accept(message);

Ce service constitue la partie "intégration" (par opposition à la "mise en oeuvre": principe de séparation des opérations d’intégration cfg).

Avec un framework CDI

Pour un environnement de production avec un framework CDI, ceci ressemblerait à ceci:

interface MessageHandler extends Consumer<MessageModel> {}
@Component
class MessageHandlingService implements MessageHandler {
    Map<Integer,MessageHandler> handlers = new ConcurrentHashMap<>();

    @Autowired
    private SavingService saveService;
    @Autowired
    private NotificationService notificationService;

    @PostConstruct
    public void init() {
        handlers.put(1, saveService::save);
        handlers.put(2, notificationService::notifyAll);
        handlers.put(3, notificationService::notify);
    }

    public void accept(MessageModel m) {  // as above }
}

Le comportement peut être modifié à l'exécution

L'un des avantages de ce paramètre par rapport au commutateur de la réponse de @ user7 est que le comportement peut être ajusté à l'exécution . Vous pouvez imaginer des méthodes comme

public MessageHandler setMessageHandler(Integer id, MessageHandler newHandler);

qui installerait le MessageHandler donné et renverrait l'ancien; cela vous permettrait d'ajouter des décorateurs, par exemple.

Un exemple de cette utilité est si vous avez un service Web peu fiable fournissant la manipulation; s'il est accessible, il peut être installé comme un handlelr; sinon, un gestionnaire par défaut est utilisé.

16
daniu

Le point principal ici est que vous séparez instanciation et configuration de execution

Même avec OOP, nous ne pouvons pas éviter de distinguer différents cas en utilisant des instructions if/else cascades ou switch. Après tout, nous devons créer des instances de classes concrètes spécifiques.
Mais cela devrait être en code d'initialisation ou une sorte de factory.

Dans la logique business, nous voulons éviter les instructions if/else cascades ou switch en appelant des méthodes génériques sur interfaces, dans lesquelles l'implémenteur sait mieux se comporter.

10
Timothy Truckle

L'approche de code propre habituelle consiste pour le MessageModel à contenir son comportement.

interface Message {
    void manage();
}

abstract class MessageModel implements Message {
}

public class StoringMessage extends MessageModel {
    public void manage() {
        store();
    }
}
public class NotifyingMessage extends MessageModel {
    public void manage() {
        notify();
    }
}

Votre getMessageFromApi renvoie alors le type approprié et votre commutateur est

MessageModel model = getMessageFromApi();
model.manage();

De cette façon, vous avez essentiellement le commutateur dans la méthode getMessageFromApi() car il doit décider quel message générer.

Cependant, cela convient car cela remplit quand même le type de message id; et le code client (où réside actuellement votre commutateur) est résistant aux modifications apportées aux messages; c'est-à-dire que l'ajout d'un autre type de message sera traité correctement.

4
daniu

Le vrai problème que vous avez, c'est que MessageModel n'est pas polymorphe. Vous devez convertir la MessageModels en une classe Message polymorphe, mais vous ne devez définir aucune logique quant à l'utilisation des messages de cette classe. Au lieu de cela, il devrait contenir le contenu réel du message et utiliser le modèle de visiteur, comme indiqué dans Eric's Answer , afin que les autres classes puissent fonctionner sur une Message. Vous n'avez pas besoin d'utiliser une Visitor anonyme; vous pouvez créer des classes d'implémentation telles que MessageActionVisitor.

Pour convertir MessageModels en divers Messages, vous pouvez utiliser une fabrique, comme indiqué dans réponse de l'utilisateur7 . En plus de sélectionner le type de Message à renvoyer, l’usine doit renseigner les champs de chaque type de Message à l’aide de MessageModel.

1
Vaelus

Vous pouvez utiliser le Factory Pattern . J'ajouterais une énumération qui a les valeurs suivantes:

public enum MessageFacotry{
    STORING(StoringMessage.TYPE, StoringMessage.class),
    PUBLIC_MESSAGE(PublicMessage.TYPE, PublicMessage.class),
    PRIVATE_MESSAGE(PrivateMessage.TYPE, PrivateMessage.class);
    Class<? extends Message> clazz;
    int type;
    private MessageFactory(int type, Class<? extends Message> clazz){
        this.clazz = clazz;
        this.type = type;
    }

    public static Message getMessageByType(int type){

         for(MessageFactory mf : values()){
              if(mf.type == type){
                   return mf.clazz.newInstance();
              }
         }
         throw new ..
    }
}

Vous pouvez ensuite appeler la méthode statique de cette énumération et créer une instance du message que vous souhaitez gérer. 

0
Noixes