Progettazione dell'interfaccia in cui le funzioni devono essere richiamate in una sequenza specifica

24

L'attività consiste nel configurare un componente hardware all'interno del dispositivo, in base ad alcune specifiche di input. Questo dovrebbe essere ottenuto come segue:

1) Raccogli le informazioni di configurazione. Questo può accadere in tempi e luoghi diversi. Ad esempio, il modulo A e il modulo B possono entrambi richiedere (in momenti diversi) alcune risorse dal mio modulo. Quelle "risorse" sono in realtà ciò che è la configurazione.

2) Dopo che è chiaro che non verranno realizzate più richieste, un comando di avvio, che fornisce un riepilogo delle risorse richieste, deve essere inviato all'hardware.

3) Solo dopo, può (e deve) configurare dettagliatamente le risorse suddette.

4) Inoltre, solo dopo 2), è possibile (e necessario) il routing delle risorse selezionate ai chiamanti dichiarati.

Una causa comune di bug, anche per me, che ha scritto la cosa, è di aver sbagliato questo ordine. Quali convenzioni di denominazione, design o meccanismi posso impiegare per rendere l'interfaccia utilizzabile da qualcuno che vede il codice per la prima volta?

    
posta Vorac 22.08.2014 - 12:32
fonte

8 risposte

45

È una riprogettazione, ma puoi evitare l'uso improprio di molte API ma non disporre di alcun metodo che non dovrebbe essere chiamato.

Ad esempio, invece di first you init, then you start, then you stop

Il tuo costruttore init s un oggetto che può essere avviato e start crea una sessione che può essere interrotta.

Ovviamente se si dispone di una restrizione a una sessione alla volta è necessario gestire il caso in cui qualcuno tenta di crearne uno con uno già attivo.

Ora applica questa tecnica al tuo caso.

    
risposta data 22.08.2014 - 12:37
fonte
19

È possibile che il metodo di avvio restituisca un oggetto che è un parametro obbligatorio per la configurazione:

Resource *MyModule::GetResource();
MySession *MyModule::Startup();
void Resource::Configure(MySession *session);

Anche se MySession è solo una struttura vuota, questo imporrà attraverso la sicurezza del tipo che nessun metodo Configure() può essere chiamato prima dell'avvio.

    
risposta data 22.08.2014 - 14:51
fonte
8

Basandosi sulla risposta di Cashcow - perché devi presentare un nuovo oggetto al chiamante, quando puoi semplicemente presentare una nuova interfaccia? Rebrand-Pattern:

class IStartable     { public: virtual IRunnable      start()     = 0; };
class IRunnable      { public: virtual ITerminateable run()       = 0; };
class ITerminateable { public: virtual void           terminate() = 0; };

Puoi anche consentire a ITerminateable di implementare IRunnable, se una sessione può essere eseguita più volte.

Il tuo oggetto:

class Service : IStartable, IRunnable, ITerminateable
{
  public:
    IRunnable      start()     { ...; return this; }
    ITerminateable run()       { ...; return this; }
    void           terminate() { ...; }
}

// And use it like this:
IStartable myService = Service();

// Now you can only call start() via the interface
IRunnable configuredService = myService.start();

// Now you can also call run(), because it is wrapped in the new interface...

In questo modo puoi solo chiamare i metodi giusti, dato che hai solo l'interfaccia IStartable all'inizio e otterrai il metodo run () accessibile solo quando hai chiamato start (); Dall'esterno sembra un pattern con più classi e oggetti, ma la classe sottostante rimane una classe, a cui viene sempre fatto riferimento.

    
risposta data 22.08.2014 - 15:04
fonte
2

Esistono molti approcci validi per risolvere il tuo problema. Basile Starynkevitch ha proposto un approccio "zero-burocrazia" che ti lascia con un'interfaccia semplice e fa affidamento sul programmatore che usa appropriatamente l'interfaccia. Mentre mi piace questo approccio, presenterò un altro che ha più eingineering ma consente al compilatore di rilevare alcuni errori.

  1. Identifica i vari stati in cui può essere inserito il tuo dispositivo, come Uninitialised , Started , Configured e così via. L'elenco deve essere finito.¹

  2. Per ogni stato, definisci struct contenente l'ulteriore necessario informazioni pertinenti a tale stato, ad es. %codice%, DeviceUninitialised e così via.

  3. Comprimi tutti i trattamenti in un oggetto DeviceStarted dove i metodi usano strutture definite in 2. come input e output. Quindi, potresti avere un metodo DeviceStrategy (o qualunque cosa l'equivalente potrebbe essere secondo le convenzioni del tuo progetto).

Con questo approccio, un programma valido deve chiamare alcuni metodi nella sequenza imposta dai prototipi del metodo.

I vari stati sono oggetti non correlati, questo a causa del principio di sostituzione. Se è utile che queste strutture condividano un antenato comune, ricorda che il pattern visitor può essere utilizzato per recuperare il tipo concreto dell'istanza di una classe astratta.

Mentre ho descritto in 3. una classe DeviceStarted DeviceStrategy::start (DeviceUninitalised dev) unica, ci sono situazioni in cui potresti voler dividere la funzionalità che fornisce su diverse classi.

Per riassumerli, i punti chiave del disegno che ho descritto sono:

  1. A causa del principio di sostituzione, oggetti che rappresentano stati del dispositivo dovrebbe essere distinto e non avere speciali relazioni di ereditarietà.

  2. Comprime i trattamenti del dispositivo in oggetti di tipo stella anziché negli oggetti rappresentando i dispositivi stessi, in modo che ogni stato del dispositivo o dispositivo vede solo se stesso, e la strategia vede tutti loro ed esprimere la possibilità transizioni tra di loro.

Giuro di aver visto una volta una descrizione di un'implementazione di un client telnet seguendo queste linee, ma non ero in grado di trovarlo di nuovo. Avrebbe stato un riferimento molto utile!

¹: Per questo, segui la tua intuizione o trova le classi di equivalenza dei metodi nella tua implementazione effettiva per la relazione "metodo₁ ~ metodo₂ iff. è valido utilizzarli sullo stesso oggetto "- assumendo che tu abbia un grosso oggetto che incapsula tutti i trattamenti sul tuo dispositivo. Entrambi i metodi degli stati di assegnazione danno risultati fantastici.

    
risposta data 22.08.2014 - 13:50
fonte
2

Utilizza uno schema costruttore.

Avere un oggetto che ha metodi per tutte le operazioni che hai menzionato sopra. Tuttavia, non esegue queste operazioni subito. Ricorda solo ogni operazione per dopo. Poiché le operazioni non vengono eseguite immediatamente, l'ordine in cui le passate al builder non ha importanza.

Dopo aver definito tutte le operazioni sul builder, si chiama execute -method. Quando viene chiamato questo metodo, esegue tutti i passaggi elencati sopra nell'ordine corretto con le operazioni memorizzate in precedenza. Questo metodo è anche un buon posto per eseguire alcuni controlli di integrità di operazioni (come provare a configurare una risorsa che non è stata ancora impostata) prima di scriverli sull'hardware. Ciò potrebbe evitare di danneggiare l'hardware con una configurazione non sensata (nel caso in cui l'hardware sia suscettibile a questo).

    
risposta data 22.08.2014 - 14:08
fonte
1

Hai solo bisogno di documentare correttamente come viene utilizzata l'interfaccia e di dare un esempio di tutorial.

Potresti anche avere una variante di libreria di debug che esegue alcuni controlli di runtime.

Forse definire e documentare correttamente alcune convenzioni di denominazione (ad esempio preconfigure* , startup* , postconfigure* , run* ....)

BTW, molte interfacce esistenti seguono uno schema simile (ad esempio X11 toolkit).

    
risposta data 22.08.2014 - 12:36
fonte
1

Questo è in effetti un errore comune e insidioso, perché i compilatori possono solo applicare le condizioni di sintassi, mentre è necessario che i programmi client siano "grammaticalmente" corretti.

Sfortunatamente, le convenzioni di denominazione sono quasi del tutto inefficaci contro questo tipo di errore. Se vuoi veramente incoraggiare le persone a non fare cose sgrammaticate, dovresti distribuire un oggetto comando di qualche tipo che deve essere inizializzato con i valori per le precondizioni, in modo che non possano eseguire i passi fuori di ordine.

    
risposta data 22.08.2014 - 12:38
fonte
1
public class Executor {

private Executor() {} // helper class

  public void execute(MyStepsRunnable r) {
    r.step1();
    r.step2();
    r.step3();
  }
}

interface MyStepsRunnable {

  void step1();
  void step2();
  void step3();
}

Usando questo modello sei sicuro che qualsiasi implementatore verrà eseguito in questo preciso ordine. Puoi fare un ulteriore passo avanti e creare un ExecutorFactory che costruirà Executor con percorsi di esecuzione personalizzati.

    
risposta data 22.08.2014 - 14:13
fonte

Leggi altre domande sui tag