Méthode unique avec plusieurs paramètres vs plusieurs méthodes qui doivent être appelées dans l'ordre

16

J'ai des données brutes sur lesquelles je dois faire beaucoup de choses (le déplacer, le faire pivoter, le mettre à l'échelle le long d'un certain axe, le faire pivoter vers une position finale) et je ne sais pas quelle est la meilleure façon de le faire pour maintenir la lisibilité du code. D'une part, je peux faire une seule méthode avec de nombreux paramètres (10+) pour faire ce dont j'ai besoin, mais c'est un cauchemar de lecture de code. D'un autre côté, je pourrais créer plusieurs méthodes avec 1 à 3 paramètres chacune, mais ces méthodes devraient être appelées dans un ordre très spécifique pour obtenir le résultat correct. J'ai lu qu'il est préférable que les méthodes fassent une chose et le fassent bien, mais il semble que de nombreuses méthodes doivent être appelées pour ouvrir le code des bogues difficiles à trouver.

Existe-t-il un paradigme de programmation que je pourrais utiliser pour minimiser les bogues et faciliter la lecture du code?

tomsrobots
la source
3
Le plus gros problème n'est pas de «ne pas les appeler dans l'ordre», c'est de «ne pas savoir» que vous (ou plus précisément, un futur programmeur) devez les appeler dans l'ordre. Assurez-vous que tout programmeur de maintenance connaît les détails (cela dépendra largement de la façon dont vous documenterez les exigences, la conception et les spécifications). Utilisez les tests unitaires, les commentaires et fournissez des fonctions d'assistance qui prennent tous les paramètres et appellent les autres
mattnz
Tout comme une remarque désinvolte, une interface fluide et un modèle de commande peuvent être utiles. Cependant, c'est à vous (en tant que propriétaire) et aux utilisateurs de votre bibliothèque (les clients) de décider quelle conception est la meilleure. Comme d'autres le soulignent, il est nécessaire de communiquer aux utilisateurs que les opérations sont non commutatives (qu'elles sont sensibles à l'ordre d'exécution), sans lesquelles vos utilisateurs ne sauraient jamais comment les utiliser correctement.
rwong
Exemples d'opérations non commutatives: transformations d'images (rotation, mise à l'échelle et recadrage), multiplications matricielles, etc.
rwong
Vous pouvez peut-être utiliser le curry: cela rendrait impossible l'application des méthodes / fonctions dans le mauvais ordre.
Giorgio
Sur quel ensemble de méthodes travaillez-vous ici? Je veux dire, je pense que la norme est de passer un objet de transformation (comme Affine Transform de Java pour des trucs 2D) que vous passez à une méthode qui l'applique. Le contenu de la transformation est différent selon l'ordre dans lequel vous appelez les opérations initiales, par conception (c'est donc "vous l'appelez dans l'ordre dont vous en avez besoin", et non "dans l'ordre que je veux").
Clockwork-Muse

Réponses:

24

Attention au couplage temporel . Cependant, ce n'est pas toujours un problème.

Si vous devez exécuter les étapes dans l'ordre, il s'ensuit que l'étape 1 produit un objet requis pour l'étape 2 (par exemple, un flux de fichiers ou une autre structure de données). Cela seul exige que la seconde fonction doit être appelée après la première, il est même possible de les appeler dans le mauvais ordre accidentellement.

En divisant votre fonctionnalité en morceaux de la taille d'une bouchée, chaque partie est plus facile à comprendre et certainement plus facile à tester de manière isolée. Si vous avez une énorme fonction de 100 lignes et quelque chose au milieu, comment votre échec de test vous indique-t-il ce qui ne va pas? Si l'une de vos cinq méthodes de ligne se casse, votre test unitaire échoué vous dirige immédiatement vers le morceau de code qui nécessite votre attention.

Voici à quoi devrait ressembler un code complexe :

public List<Widget> process(File file) throws IOException {
  try (BufferedReader in = new BufferedReader(new FileReader(file))) {
    List<Widget> widgets = new LinkedList<>();
    String line;
    while ((line = in.readLine()) != null) {
      if (isApplicable(line)) { // Filter blank lines, comments, etc.
        Ore o = preprocess(line);
        Ingot i = smelt(o);
        Alloy a = combine(i, new Nonmetal('C'));
        Widget w = smith(a);
        widgets.add(w);
      }
    }
    return widgets;
  }
}

À tout moment pendant le processus de conversion des données brutes en un widget fini, chaque fonction renvoie quelque chose requis par l'étape suivante du processus. On ne peut pas former un alliage à partir de scories, il faut d'abord le fondre (purifier). On ne peut pas créer un widget sans l'autorisation appropriée (par exemple l'acier) en entrée.

Les détails spécifiques de chaque étape sont contenus dans des fonctions individuelles qui peuvent être testées: plutôt que de tester en bloc l'ensemble du processus d'extraction de roches et de création de widgets, testez chaque étape spécifique. Vous avez maintenant un moyen facile de vous assurer que si votre processus de "création de widget" échoue, vous pouvez affiner la raison spécifique.

Mis à part les avantages de tester et de prouver l'exactitude, l'écriture de code de cette façon est beaucoup plus facile à lire. Personne ne peut comprendre une énorme liste de paramètres . Décomposez-le en petits morceaux et montrez ce que signifie chaque petit morceau: c'est grokkable .

Communauté
la source
2
Merci, je pense que c'est une bonne façon de résoudre le problème. Même s'il augmente le nombre d'objets (et cela peut sembler inutile), il force l'ordre tout en maintenant la lisibilité.
tomsrobots
10

L'argument "doit être exécuté dans l'ordre" est théorique car presque tout votre code doit être exécuté dans le bon ordre. Après tout, vous ne pouvez pas écrire dans un fichier, puis l'ouvrir puis le fermer, n'est-ce pas?

Vous devez vous concentrer sur ce qui rend votre code le plus facile à maintenir. Cela signifie généralement des fonctions d'écriture petites et faciles à comprendre. Chaque fonction doit avoir un objectif unique et ne pas avoir d'effets secondaires imprévus.

Dave Nay
la source
5

Je créerais un » ImageProcesssor « (ou n'importe quel nom convenant à votre projet) et un objet de configuration ProcessConfiguration , qui contient tous les paramètres nécessaires.

 ImageProcessor p = new ImageProcessor();

 ProcessConfiguration config = new processConfiguration().setTranslateX(100)
                                                         .setTranslateY(100)
                                                         .setRotationAngle(45);
 p.process(image, config);

À l'intérieur du processeur d'image, vous encapsulez l'ensemble du processus derrière un mehtod process()

public class ImageProcessor {

    public Image process(Image i, ProcessConfiguration c){
        Image processedImage=i.getCopy();
        shift(processedImage, c);
        rotate(processedImage, c);
        return processedImage;
    }

    private void rotate(Image i, ProcessConfiguration c) {
        //rotate
    }

    private void shift(Image i, ProcessConfiguration c) {
        //shift
    }
}

Cette méthode appelle les méthodes de transformation dans l'ordre correct shift(), rotate(). Chaque méthode obtient les paramètres appropriés de la ProcessConfiguration passée .

public class ProcessConfiguration {

    private int translateX;

    private int rotationAngle;

    public int getRotationAngle() {
        return rotationAngle;
    }

    public ProcessConfiguration setRotationAngle(int rotationAngle){
        this.rotationAngle=rotationAngle;
        return this;
    }

    public int getTranslateY() {
        return translateY;
    }

    public ProcessConfiguration setTranslateY(int translateY) {
        this.translateY = translateY;
        return this;
    }

    public int getTranslateX() {
        return translateX;
    }

    public ProcessConfiguration setTranslateX(int translateX) {
        this.translateX = translateX;
        return this;
    }

    private int translateY;

}

J'ai utilisé des interfaces fluides

public ProcessConfiguration setRotationAngle(int rotationAngle){
    this.rotationAngle=rotationAngle;
    return this;
}

qui permet une initialisation astucieuse (comme vu ci-dessus).

L'avantage évident, encapsulant les paramètres nécessaires dans un seul objet. Vos signatures de méthode deviennent lisibles:

private void shift(Image i, ProcessConfiguration c)

Il s'agit de déplacer une image et les paramètres détaillés sont en quelque sorte configurés .

Alternativement, vous pouvez créer un ProcessingPipeline :

public class ProcessingPipeLine {

    Image i;

    public ProcessingPipeLine(Image i){
        this.i=i;
    };

    public ProcessingPipeLine shift(Coordinates c){
        shiftImage(c);
        return this;
    }

    public ProcessingPipeLine rotate(int a){
        rotateImage(a);
        return this;
    }

    public Image getResultingImage(){
        return i;
    }

    private void rotateImage(int angle) {
        //shift
    }

    private void shiftImage(Coordinates c) {
        //shift
    }

}

Un appel de méthode à une méthode processImageinstancierait un tel pipeline et rendrait transparent quoi et dans quel ordre est fait: décalage , rotation

public Image processImage(Image i, ProcessConfiguration c){
    Image processedImage=i.getCopy();
    processedImage=new ProcessingPipeLine(processedImage)
            .shift(c.getCoordinates())
            .rotate(c.getRotationAngle())
            .getResultingImage();
    return processedImage;
}
Thomas Junk
la source
3

Avez-vous envisagé d'utiliser une sorte de curry ? Imaginez que vous ayez une classe Processeeet une classe Processor:

class Processor
{
    private final Processee _processee;

    public Processor(Processee p)
    {
        _processee = p;
    }

    public void process(T1 a1, T2 a2)
    {
        // Process using a1
        // then process using a2
    }
}

Vous pouvez maintenant remplacer la classe Processorpar deux classes Processor1et Processor2:

class Processor1
{
    private final Processee _processee;

    public Processor1(Processee p)
    {
        _processee = p;
    }

    public Processor2 process(T1 a1)
    {
        // Process using argument a1

        return new Processor2(_processee);
    }
}

class Processor2
{
    private final Processee _processee;

    public Processor(Processee p)
    {
        _processee = p;
    }

    public void process(T2 a2)
    {
        // Process using argument a2
    }
}

Vous pouvez ensuite appeler les opérations dans le bon ordre en utilisant:

new Processor1(processee).process(a1).process(a2);

Vous pouvez appliquer ce modèle plusieurs fois si vous avez plus de deux paramètres. Vous pouvez également grouper les arguments comme vous le souhaitez, c'est-à-dire que vous n'avez pas besoin que chaque processméthode prenne exactement un argument.

Giorgio
la source
Nous avions à peu près la même idée;) La seule différence est que votre Pipeline applique un ordre de traitement strict.
Thomas Junk
@ThomasJunk: D'après ce que j'ai compris, c'est une exigence: "ces méthodes devraient être appelées dans un ordre très spécifique pour obtenir le résultat correct". Avoir un ordre d'exécution strict ressemble beaucoup à la composition d'une fonction.
Giorgio
Et moi aussi. Mais, si l'ordre de traitement change, vous devez faire beaucoup de refactoring;)
Thomas Junk
@ThomasJunk: Vrai. Cela dépend vraiment de l'application. Si les étapes de traitement peuvent être échangées très souvent, votre approche est probablement meilleure.
Giorgio