Metodo singolo con molti parametri vs molti metodi che devono essere chiamati in ordine


16

Ho alcuni dati grezzi per cui devo fare molte cose (spostarli, ruotarli, ridimensionarli lungo un determinato asse, ruotarli in una posizione finale) e non sono sicuro di quale sia il modo migliore per farlo per mantenere la leggibilità del codice. Da un lato, posso creare un singolo metodo con molti parametri (10+) per fare ciò di cui ho bisogno, ma questo è un incubo di lettura del codice. D'altra parte, potrei creare più metodi con 1-3 parametri ciascuno, ma questi metodi dovrebbero essere chiamati in un ordine molto specifico per ottenere il risultato corretto. Ho letto che è meglio che i metodi facciano una cosa e la facciano bene, ma sembra che avere molti metodi che devono essere chiamati per aprire il codice per i bug difficili da trovare.

Esiste un paradigma di programmazione che potrei usare per minimizzare i bug e rendere più facile la lettura del codice?


3
Il problema più grande non è "non chiamarli in ordine", è "non sapere" che tu (o più precisamente, un futuro programmatore) devi chiamarli in ordine. Assicurarsi che qualsiasi programmatore di manutenzione sia a conoscenza dei dettagli (ciò dipenderà in gran parte da come documentare requisiti, design e specifiche). Usa i test unitari, i commenti e fornisci le funzioni di supporto che accettano tutti i parametri e chiamano gli altri
mattnz

Proprio come un'osservazione off-hand, un'interfaccia fluida e un modello di comando potrebbero essere utili. Tuttavia, spetta a te (in qualità di proprietario) e agli utenti della tua biblioteca (i clienti) decidere quale sia il design migliore. Come altri sottolineano, è necessario comunicare agli utenti che le operazioni sono non commutative (che sono sensibili all'ordine di esecuzione), senza le quali gli utenti non scopriranno mai come usarle correttamente.
rwong

Esempi di operazioni non commutative: trasformazioni di immagine (rotazione, ridimensionamento e ritaglio), moltiplicazioni di matrice, ecc.
rwong

Forse puoi usare il curry: questo renderebbe impossibile applicare i metodi / le funzioni nell'ordine sbagliato.
Giorgio

Su quale set di metodi stai lavorando qui? Voglio dire, penso che lo standard sia passare un oggetto di trasformazione (come l' affine Transform di Java per roba 2D) che passi ad un metodo che lo applica. Il contenuto della trasformazione è diverso a seconda dell'ordine in cui chiamate le operazioni iniziali, in base alla progettazione (quindi è "lo chiamate nell'ordine in cui ne avete bisogno", non "nell'ordine in cui lo voglio").
Clockwork-Muse

Risposte:


24

Attenzione all'accoppiamento temporale . Tuttavia, questo non è sempre un problema.

Se è necessario eseguire i passaggi nell'ordine, ne consegue che il passaggio 1 produce alcuni oggetti richiesti per il passaggio 2 (ad esempio un flusso di file o altra struttura di dati). Questo da solo richiede che la seconda funzione debba essere chiamata dopo la prima, non è nemmeno possibile chiamarli nell'ordine sbagliato per errore.

Dividendo la tua funzionalità in pezzi di dimensioni ridotte, ogni parte è più facile da capire e sicuramente più facile da testare in isolamento. Se hai un'enorme funzione da 100 linee e qualcosa nel mezzo si interrompe, in che modo il tuo test fallito ti dice cosa è sbagliato? Se uno dei tuoi metodi a cinque righe si interrompe, il test unitario fallito ti indirizza immediatamente verso l'unico pezzo di codice che richiede attenzione.

Ecco come dovrebbe apparire un codice complesso :

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

In qualsiasi momento durante il processo di conversione dei dati grezzi in un widget finito, ogni funzione restituisce qualcosa richiesto dal passaggio successivo del processo. Non si può formare una lega dalle scorie, bisogna prima fonderla (purificarla). Non è possibile creare un widget senza il permesso adeguato (ad es. Acciaio) come input.

I dettagli specifici di ogni passaggio sono contenuti in singole funzioni che possono essere testate: piuttosto che test unitari l'intero processo di estrazione di rocce e creazione di widget, testare ogni passaggio specifico. Ora hai un modo semplice per assicurarti che se il tuo processo "crea widget" fallisce, puoi restringere il motivo specifico.

A parte i vantaggi del test e della dimostrazione della correttezza, scrivere codice in questo modo è molto più facile da leggere. Nessuno può capire un enorme elenco di parametri . Suddividilo in piccoli pezzi e mostra cosa significa ogni piccolo pezzo: è grokkable .


2
Grazie, penso che sia un buon modo per affrontare il problema. Anche se aumenta il numero di oggetti (e ciò potrebbe sembrare superfluo), forza l'ordine mantenendo la leggibilità.
Tomsrobot

10

L'argomento "deve essere eseguito in ordine" è controverso poiché praticamente tutto il codice deve essere eseguito nell'ordine corretto. Dopotutto, non puoi scrivere su un file, quindi aprirlo e chiuderlo, vero?

Dovresti concentrarti su ciò che rende il tuo codice più gestibile. Questo di solito significa che le funzioni di scrittura sono piccole e facilmente comprensibili. Ogni funzione dovrebbe avere un unico scopo e non avere effetti collaterali imprevisti.


5

Vorrei creare un " ImageProcesssor " (o qualunque nome si adatti al tuo progetto) e un oggetto di configurazione ProcessConfiguration , che contiene tutti i parametri necessari.

 ImageProcessor p = new ImageProcessor();

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

All'interno del processore d'immagine incapsuli l'intero processo dietro 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
    }
}

Questo metodo chiama i metodi di trasformazione nell'ordine corretto shift(), rotate(). Ogni metodo ottiene i parametri appropriati dalla ProcessConfiguration passata .

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;

}

Ho usato interfacce fluide

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

che consente un'iniziale inizializzazione (come visto sopra).

L'ovvio vantaggio, incapsulando i parametri necessari in un oggetto. Le firme del metodo diventano leggibili:

private void shift(Image i, ProcessConfiguration c)

Si tratta di spostare un'immagine e parametri descritti sono in qualche modo configurato .

In alternativa, è possibile creare 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
    }

}

Una chiamata di metodo a un metodo processImagecreerebbe un'istanza di tale pipeline e renderebbe trasparente cosa e in quale ordine: spostare , ruotare

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

3

Hai mai pensato di utilizzare un tipo di curry ? Immagina di avere una classe Processeee una 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
    }
}

Ora puoi sostituire la classe Processorcon due classi Processor1e 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
    }
}

È quindi possibile chiamare le operazioni nell'ordine giusto utilizzando:

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

È possibile applicare questo modello più volte se si hanno più di due parametri. È inoltre possibile raggruppare gli argomenti come desiderato, ovvero non è necessario che ogni processmetodo prenda esattamente un argomento.


Avevamo quasi la stessa idea;) L'unica differenza è che la tua pipeline applica un rigoroso ordine di elaborazione.
Thomas Junk

@ThomasJunk: Per quanto ho capito, questo è un requisito: "questi metodi dovrebbero essere chiamati in un ordine molto specifico per ottenere il risultato corretto". Avere un ordine di esecuzione rigoroso sembra molto simile alla composizione delle funzioni.
Giorgio

E così ho fatto io. Ma, se si modifica l'ordine, è necessario eseguire molti refactoring;)
Thomas Junk

@ThomasJunk: True. Dipende davvero dall'applicazione. Se le fasi di elaborazione possono essere scambiate molto spesso, probabilmente il tuo approccio è migliore.
Giorgio
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.