Einzelne Methode mit vielen Parametern im Vergleich zu vielen Methoden, die nacheinander aufgerufen werden müssen

16

Ich habe einige Rohdaten, für die ich viele Dinge tun muss (verschieben, drehen, entlang einer bestimmten Achse skalieren, bis zur Endposition drehen), und ich bin mir nicht sicher, wie ich dies am besten tun kann, um die Lesbarkeit des Codes zu gewährleisten. Einerseits kann ich eine einzelne Methode mit vielen Parametern (10+) erstellen, um das zu tun, was ich brauche, aber dies ist ein Albtraum beim Lesen von Code. Andererseits könnte ich mehrere Methoden mit jeweils 1-3 Parametern erstellen, aber diese Methoden müssten in einer ganz bestimmten Reihenfolge aufgerufen werden, um das richtige Ergebnis zu erzielen. Ich habe gelesen, dass es für Methoden am besten ist, eine Sache zu tun und sie gut zu machen, aber es scheint, dass viele Methoden aufgerufen werden müssen, um Code für schwer zu findende Fehler zu öffnen.

Gibt es ein Programmierparadigma, mit dem ich Fehler minimieren und das Lesen von Code vereinfachen könnte?

Tomsroboter
quelle
3
Das größte Problem ist nicht, sie nicht in der richtigen Reihenfolge aufzurufen, sondern zu wissen, dass Sie (oder genauer gesagt ein zukünftiger Programmierer) sie in der richtigen Reihenfolge aufrufen müssen. Stellen Sie sicher, dass jeder Wartungsprogrammierer die Details kennt (Dies hängt weitgehend davon ab, wie Sie Anforderungen, Design und Spezifikationen dokumentieren). Verwenden Sie Unit-Tests, Kommentare und
Hilfsfunktionen,
Eine fließende Benutzeroberfläche und ein fließendes Befehlsmuster können hilfreich sein. Es liegt jedoch an Ihnen (als Eigentümer) und den Benutzern Ihrer Bibliothek (den Kunden) zu entscheiden, welches Design das beste ist. Wie bereits erwähnt, ist es erforderlich, den Benutzern mitzuteilen, dass die Vorgänge nicht kommutativ sind ( dh dass sie in Bezug auf die Ausführungsreihenfolge kritisch sind), ohne die Ihre Benutzer niemals herausfinden würden, wie sie richtig verwendet werden.
rwong
Beispiele für nicht kommutative Operationen: Bildtransformationen (Drehen, Skalieren und Beschneiden), Matrixmultiplikationen usw.
rwong
Vielleicht können Sie Currying verwenden: Dies würde es unmöglich machen, die Methoden / Funktionen in der falschen Reihenfolge anzuwenden.
Giorgio
An welchen Methoden arbeiten Sie hier? Ich meine, ich denke, der Standard besteht darin, ein Transformationsobjekt (wie Javas Affine Transform for 2D) zu übergeben, das Sie an eine Methode übergeben, die es anwendet. Der Inhalt der Transformation unterscheidet sich je nach der Reihenfolge, in der Sie die anfänglichen Vorgänge für sie aufrufen (also "Sie nennen sie in der Reihenfolge, in der Sie sie benötigen", nicht "in der Reihenfolge, in der ich sie haben möchte").
Clockwork-Muse

Antworten:

24

Vorsicht vor zeitlicher Kopplung . Dies ist jedoch nicht immer ein Problem.

Wenn Sie die Schritte der Reihe nach ausführen müssen, erzeugt Schritt 1 ein für Schritt 2 erforderliches Objekt (z. B. einen Dateistream oder eine andere Datenstruktur). Dies setzt voraus, dass die zweite Funktion nach der ersten aufgerufen werden muss . Es ist nicht einmal möglich, sie versehentlich in der falschen Reihenfolge aufzurufen.

Indem Sie Ihre Funktionalität in mundgerechte Teile aufteilen, ist jedes Teil leichter zu verstehen und auf jeden Fall einfacher für sich zu testen. Wenn Sie eine riesige 100-Zeilen-Funktion und etwas in der Mitte haben, wie sagt Ihnen Ihr fehlgeschlagener Test, was falsch ist? Wenn eine Ihrer fünf Zeilenmethoden unterbrochen wird, werden Sie durch den fehlgeschlagenen Komponententest sofort zu dem Code geleitet, der überprüft werden muss.

So sollte komplexer Code aussehen:

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;
  }
}

Zu jedem Zeitpunkt während der Konvertierung von Rohdaten in ein fertiges Widget gibt jede Funktion etwas zurück, das für den nächsten Schritt im Prozess erforderlich ist. Man kann aus Schlacke keine Legierung bilden, man muss sie zuerst schmelzen (reinigen). Ein Widget darf nicht ohne die entsprechende Erlaubnis (zB Stahl) als Eingabe erstellt werden.

Die spezifischen Details jedes Schritts sind in einzelnen Funktionen enthalten, die getestet werden können: Anstatt den gesamten Prozess des Gesteinsabbaus und der Erstellung von Widgets als Einheit zu testen, testen Sie jeden einzelnen Schritt. Jetzt können Sie auf einfache Weise sicherstellen, dass Sie den genauen Grund eingrenzen können, wenn der Prozess zum Erstellen eines Widgets fehlschlägt.

Abgesehen von den Vorteilen des Testens und des Beweises der Korrektheit ist das Schreiben von Code auf diese Weise viel einfacher zu lesen. Niemand kann eine riesige Parameterliste verstehen . Zerlegen Sie es in kleine Stücke und zeigen Sie, was jedes kleine Stück bedeutet: das ist grokkable .

Gemeinschaft
quelle
2
Vielen Dank, ich denke, das ist ein guter Weg, um das Problem anzugehen. Auch wenn die Anzahl der Objekte erhöht wird (und dies als unnötig empfunden wird), wird die Reihenfolge erzwungen, während die Lesbarkeit erhalten bleibt.
Tomsrobots
10

Das Argument "muss in der richtigen Reihenfolge ausgeführt werden" ist umstritten, da so gut wie der gesamte Code in der richtigen Reihenfolge ausgeführt werden muss. Schließlich können Sie nicht in eine Datei schreiben, sondern sie öffnen und schließen, oder?

Sie sollten sich darauf konzentrieren, was Ihren Code am wartungsfreundlichsten macht. Dies bedeutet normalerweise Schreibfunktionen, die klein und leicht verständlich sind. Jede Funktion sollte einen einzigen Zweck haben und keine unerwarteten Nebenwirkungen haben.

Dave Nay
quelle
5

Ich würde einen » ImageProcessor erstellen « (oder einen beliebigen Namen, der zu Ihrem Projekt passt) und ein Konfigurationsobjekt ProcessConfiguration erstellen , das alle notwendigen Parameter enthält.

 ImageProcessor p = new ImageProcessor();

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

Innerhalb des Bildprozessors kapseln Sie den gesamten Prozess hinter einer einzigen Methode 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
    }
}

Diese Methode ruft die transformierende Methoden in der richtigen Reihenfolge shift(), rotate(). Jede Methode erhält entsprechende Parameter aus den übergebenen ProcessConfiguration .

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;

}

ich benutzte flüssige Schnittstellen verwendet

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

Dies ermöglicht eine rasche Initialisierung (wie oben gezeigt).

Der offensichtliche Vorteil, notwendige Parameter in einem Objekt zu kapseln. Ihre Methodensignaturen werden lesbar:

private void shift(Image i, ProcessConfiguration c)

Es geht darum , ein Bild zu verschieben und detaillierte Parameter sind irgendwie konfiguriert .

Alternativ können Sie eine ProcessingPipeline erstellen :

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
    }

}

Ein Methodenaufruf einer Methode processImagewürde eine solche Pipeline instanziieren und transparent machen, was und in welcher Reihenfolge getan wird: Verschieben , Drehen

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
quelle
3

Haben Sie darüber nachgedacht, Currys zu verwenden ? Stellen Sie sich vor, Sie haben eine Klasse Processeeund eine Klasse 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
    }
}

Jetzt können Sie die Klasse Processordurch zwei Klassen ersetzen Processor1und 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
    }
}

Anschließend können Sie die Vorgänge in der richtigen Reihenfolge aufrufen:

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

Sie können dieses Muster mehrmals anwenden, wenn Sie mehr als zwei Parameter haben. Sie können die Argumente auch nach Belieben gruppieren, dh Sie müssen nicht für jede processMethode genau ein Argument verwenden.

Giorgio
quelle
Wir hatten fast die gleiche Idee;) Der einzige Unterschied ist, dass Ihre Pipeline eine strenge Verarbeitungsreihenfolge erzwingt.
Thomas Junk
@ThomasJunk: Soweit ich verstanden habe, ist dies eine Anforderung: "Diese Methoden müssten in einer ganz bestimmten Reihenfolge aufgerufen werden, um das richtige Ergebnis zu erhalten." Eine strikte Ausführungsreihenfolge zu haben, klingt sehr nach Funktionskomposition.
Giorgio
Und ich auch. Aber wenn sich die Auftragsabwicklung ändert, müssen Sie viel überarbeiten;)
Thomas Junk
@ThomasJunk: Stimmt. Es kommt wirklich auf die Anwendung an. Wenn die Verarbeitungsschritte sehr oft vertauscht werden können, ist Ihre Vorgehensweise wahrscheinlich besser.
Giorgio