Come comunicare le dipendenze con l'utente dell'API?

2

Come devo progettare le mie classi e i loro metodi, in modo che sia chiaro, che alcune chiamate al metodo devono essere fatte prima che altre siano utili / valide?

Stage1 firstStage = StageProcessor( userOriginal )

Stage2 secondStage = firstStage.ApplyRequiredTransformationBeforeSecondStage()

UserResult result = StageProcessor( firstStage, secondStage )

Il mio approccio è usare i tipi per rafforzare le dipendenze. Ci sono altri modi?

Rinominare e rifattorizzare tutto in questo esempio per renderlo più leggibile e aumentare l'incapsulamento.

Vorrei sottolineare che il metodo ApplyRequiredTransformationBeforeSecondStage() deve essere chiamato tra Stage1 e Stage2 . Ma ho il permesso di spostarlo in una classe extra.

Inoltre UserResult dipende realmente da Stage1 e Stage2 allo stesso tempo.

    
posta VisorZ 10.12.2018 - 17:01
fonte

6 risposte

4

Puoi fare alcune fantasiose interfacce in stile "Fluent"

public interface IProcess
{
    IStage2 = RunStage1(UserInput input);
}

public interface IStage2 
{
    IStage3 = RunStage3();
}

public interface IStage3 
{
    FinalResult FinalResult();
}

Quindi quando l'utente inserisce il suo codice ottengono ..

var result = Process
        .RunStage1(myinput)
        .RunStage2()
        .RunStage3()
        .FinalResult();
    
risposta data 10.12.2018 - 17:20
fonte
3

Il termine di fantasia per ciò che hai a che fare è accoppiamento temporale .

My approach is to use types to enforce dependencies. Are there other ways?

Ci sono molti modi per farlo.

I metodi che mascherano la loro dipendenza dallo stato dell'oggetto o dalle variabili globali spesso causano questo problema. La soluzione è rendere esplicite tali dipendenze.

Ad esempio potresti avere

ProcessOne();
ProcessTwo();
ProcessThree();

e l'unico suggerimento per cui devono essere chiamati in ordine sono i loro nomi. Questo è meno ideale visto che non è applicato e ci lascia bloccati con nomi che non sono molto descrittivi.

Un modo per rendere esplicite le loro dipendenze è prendere lo stato di cui hanno bisogno come argomenti.

ProcessThree(ProcessTwo(ProcessOne()));

Ciò rende impossibile chiamarli fuori servizio. Funziona all'interno di oggetti e in linguaggi funzionali.

Per un'API orientata agli oggetti a cui si accede dall'esterno, è possibile utilizzare la cosa più propriamente detta Lingua di dominio specifica interna (iDSL). Questo non deve essere confuso con altre interfacce fluenti che consentono di chiamare i metodi in qualsiasi ordine restituendo this . iDSL, come Java 8 stream e JOOQ , restituisce un oggetto intermedio che stabilisce quali metodi sono legali per essere chiamati. Questo è ciò a cui stai suggerendo quando parli di controllare l'ordine delle chiamate con i tipi.

Il ritorno del tipo intermedio e this possono essere mescolati per formare una lingua complessa piena di passaggi obbligatori e facoltativi. Ma se sei in una lingua che offre parametri con parametri predefiniti, questo potrebbe sembrare inutile. Quasi come se ci fosse un sacco di problemi per aggirare un limite di linguaggio.

    
risposta data 10.12.2018 - 19:56
fonte
2

I tipi sono un ottimo modo per applicare staticamente i vincoli, che è quello che stai facendo qui. In quanto tale, il modo in cui hai scritto questo è che il compilatore fa la maggior parte del lavoro sulle gambe per te, che è un buon modo per essere.

Un'alternativa sarebbe chiamare i metodi su un singolo oggetto che, internamente, ha aggiornato una macchina a stati su quell'oggetto. Se una particolare operazione non ha visto lo stato corretto, allora produrrebbe un errore.

Questo è peggio nel senso che i vincoli vengono controllati in fase di esecuzione anziché in fase di compilazione. Se si dispone di un insieme di vincoli particolarmente complesso e di un sistema di tipi che non è in grado di descrivere adeguatamente tali vincoli, potrebbe essere un'alternativa utile. Tuttavia, personalmente, preferisco l'approccio basato sul tipo, ove possibile.

    
risposta data 10.12.2018 - 17:33
fonte
1

Mi piace la risposta di Ewan perché è una soluzione elegante al problema che porti al tavolo, ma sospetto che la domanda stessa segnali un problema con il tuo progetto. Il modo in cui lo presenti, sembra piuttosto inutile lasciare che il cliente chiami i colpi per eseguire passi che possono essere eseguiti in sequenza in una volta dall'oggetto di servizio stesso. Quindi non ci sarebbero problemi in primo luogo.

Naturalmente avresti comunque pensato e non ha funzionato per te. Quindi immagino che ci debba essere qualcosa che non ci stai dicendo. Il cliente ha bisogno di pasticciare con risultati intermedi o qualcosa del genere. Se così fosse, potresti avere tre diverse responsabilità sulla tua mano che non dovrebbero essere gestite dalla stessa classe.

Che cosa ti fa desiderare un processore a tre stadi?

    
risposta data 10.12.2018 - 20:27
fonte
0

Penso che sia un buon modo per i casi in cui potrebbero esserci percorsi di ramificazione (es: Stage1- > [Stage2A o Stage2B] - > Risultato finale) e per i casi in cui potresti voler memoizzare e riutilizzare uno di quelle fasi per valutare un risultato diverso. Hai il compilatore e il sistema di tipi per prevenire ogni possibile uso improprio lì ed eliminato l'accoppiamento temporale in quel modo e non posso dirlo con certezza, ma sembra che potrebbe aver eliminato anche la necessità di mutare lo stato a favore del ritorno di nuovi stati effetti collaterali assenti.

Un altro modo nei casi in cui non è necessario quel tipo di flessibilità e c'è una dipendenza di ordine sottostante nelle interfacce è solo avere una funzione che accetta un oggetto / delegato / puntatore funzione come parametro. Esempio:

do_something(SomeFunction func, ...)
{
     do_first_required_thing(...);
     do_second_required_thing(...);

     func(...);

     do_final_cleanup(...);
}

Questo tipo di approccio può essere utile a volte quando hai bisogno di alcune cose da fare prima di eseguire il codice dell'utente e possibilmente alcune cose dopo (tendo ad usarlo nei casi in cui è richiesto un qualche tipo di blocco prima di richiamare la funzione specificata oppure c'è un processo di avvio / arresto prima e dopo ciò che l'utente vuole fare.

    
risposta data 10.12.2018 - 18:18
fonte
0

Se desideri applicare l'ordine nel codice, l'esempio fluente di Ewan è un ottimo modo per farlo.

In alternativa, è possibile utilizzare un builder per garantire che le cose accadano nell'ordine corretto, senza doversi preoccupare dell'ordine specificato nel codice client:

var processor = new StageProcessor(userOriginal);
processor.AddToSecondStage(someOtherData);
processor.AddToPreStageTwoTransformations(someTransformation);
processor.AddToFirstStage(someData);

var result = processor.Resolve();

In quanto sopra, ho deliberatamente specificato le azioni in ordine inverso per evidenziare la disconnessione tra la ricodifica delle azioni e la loro risoluzione. Poiché ogni chiamata memorizza semplicemente l'azione richiesta fino a quando viene chiamato Resolve , l'ordine non ha importanza.

Entrambi gli approcci hanno il loro posto. Quindi, se utilizzare le interfacce contestuali o il builder è un approccio migliore, dipende molto dalla tua specifica applicazione.

    
risposta data 11.12.2018 - 09:36
fonte

Leggi altre domande sui tag