Design dell'interfaccia in cui le funzioni devono essere chiamate in una sequenza specifica


24

Il compito è configurare un componente hardware all'interno del dispositivo, secondo alcune specifiche di input. Ciò dovrebbe essere ottenuto come segue:

1) Raccogliere 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à la configurazione.

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

3) Solo successivamente è possibile (e deve) eseguire una configurazione dettagliata di tali risorse.

4) Inoltre, solo dopo 2), è possibile (e necessario) instradare le risorse selezionate ai chiamanti dichiarati.


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


La fase 1 è meglio chiamata discoveryo handshake?
rwong,

1
L' accoppiamento temporale è un anti-schema e dovrebbe essere evitato.

1
Il titolo della domanda mi fa pensare che potresti essere interessato al modello del generatore di passaggi .
Joshua Taylor,

Risposte:


45

È una riprogettazione ma è possibile impedire l'uso improprio di molte API ma non avere a disposizione alcun metodo che non dovrebbe essere chiamato.

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

Il costruttore initè un oggetto che può essere avviato e startcrea una sessione che può essere arrestata.

Naturalmente se hai una limitazione per una sessione alla volta devi gestire il caso in cui qualcuno tenta di crearne una con una già attiva.

Ora applica quella tecnica al tuo caso.


zlibe jpeglibsono due esempi che seguono questo schema per l'inizializzazione. Tuttavia, sono necessarie molte documentazioni per insegnare il concetto agli sviluppatori.
rwong,

5
Questa è esattamente la risposta giusta: se l'ordine conta, ogni funzione restituisce un risultato che può essere chiamato per eseguire il passaggio successivo. Il compilatore stesso è in grado di applicare i vincoli di progettazione.

2
Questo è simile al modello del generatore di passi ; presenta solo l'interfaccia che ha senso in una determinata fase.
Joshua Taylor,

@JoshuaTaylor la mia risposta è un'implementazione del modello di step builder :)
Silviu Burcea,

@SilviuBurcea La tua risposta non è un'implementazione di step builder, ma ti commenterò piuttosto che qui.
Joshua Taylor,

19

È possibile fare in modo che il metodo di avvio restituisca alla configurazione un oggetto che è un parametro richiesto:

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

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


Cosa impedisce a qualcuno di fare module->GetResource()->Configure(nullptr)?
svick,

@svick: niente, ma devi farlo esplicitamente. Questo approccio ti dice cosa si aspetta e aggirando la previsione è una decisione consapevole. Come con la maggior parte dei linguaggi di programmazione, nessuno ti impedisce di spararti ai piedi. Ma è sempre bene che un'API indichi chiaramente che lo stai facendo;)
Michael Klement,

+1 sembra fantastico e semplice. Tuttavia, posso vedere un problema. Se ho oggetti a, b, c, d, allora posso iniziare ae usarlo MySessionper tentare di usarlo bcome oggetto già avviato, mentre in realtà non lo è.
Vorac,

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, poiché all'inizio hai solo l'interfaccia IStartable e otterrai il metodo run () accessibile solo quando hai chiamato start (); Dall'esterno sembra un modello con più classi e oggetti, ma la classe sottostante rimane una classe, a cui fa sempre riferimento.


1
Qual è il vantaggio di avere solo una classe sottostante anziché diverse? Poiché questa è l'unica differenza con la soluzione che ho proposto, sarei interessato a questo punto particolare.
Michael Le Barbier Grünewald,

1
@ MichaelGrünewald Non è necessario implementare tutte le interfacce con una classe, ma per un oggetto di tipo configurazione, potrebbe essere la tecnica di implementazione più semplice per condividere i dati tra istanze delle interfacce (cioè perché è condivisa in virtù della stessa oggetto).
Joshua Taylor,

1
Questo è essenzialmente il modello del generatore di passi .
Joshua Taylor,

@JoshuaTaylor La condivisione dei dati tra le istanze dell'interfaccia è duplice: mentre potrebbe essere più semplice da implementare, dobbiamo stare attenti a non accedere allo "stato indefinito" (come l'accesso all'indirizzo client di un server non connesso). Poiché l'OP pone l'accento sull'usabilità dell'interfaccia, possiamo giudicare uguali i due approcci. Grazie per aver citato il "modello di generatore di passi" BTW.
Michael Le Barbier Grünewald,

1
@ MichaelGrünewald Se interagisci con l'oggetto solo attraverso la particolare interfaccia specificata in un determinato punto, non dovrebbe esserci alcun modo (senza casting, ecc.) Per accedere a quello stato.
Joshua Taylor,

2

Esistono molti approcci validi per risolvere il tuo problema. Basile Starynkevitch ha proposto un approccio di "burocrazia zero" che ti lascia con una semplice interfaccia e si affida al programmatore usando l'interfaccia in modo appropriato. Mentre mi piace questo approccio, ne presenterò un altro che ha più eingineering ma consente al compilatore di rilevare alcuni errori.

  1. Identificare i vari stati il dispositivo può essere in, come Uninitialised, Started, Configurede così via. L'elenco deve essere finito .¹

  2. Per ogni stato, definire in structpossesso delle informazioni aggiuntive necessarie relative a quello stato, ad esempio DeviceUninitialised, DeviceStartede così via.

  3. Comprimere tutti i trattamenti in un oggetto in DeviceStrategycui i metodi utilizzano strutture definite in 2. come input e output. Pertanto, potresti avere un DeviceStarted DeviceStrategy::start (DeviceUninitalised dev)metodo (o qualunque sia l'equivalente secondo le convenzioni del tuo progetto).

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

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

Mentre ho descritto in 3. una DeviceStrategyclasse unica , ci sono situazioni in cui potresti voler dividere la funzionalità che fornisce tra diverse classi.

Per riassumere, i punti chiave del design che ho descritto sono:

  1. A causa del principio di sostituzione, gli oggetti che rappresentano gli stati dei dispositivi dovrebbero essere distinti e non avere relazioni di ereditarietà speciali.

  2. Comprimere i trattamenti dei dispositivi in ​​oggetti stellari piuttosto che negli oggetti che rappresentano i dispositivi stessi, in modo che ogni stato del dispositivo o dispositivo veda solo se stesso e la strategia li veda tutti ed esprima le possibili transizioni tra di essi.

Vorrei giurare di aver visto una volta una descrizione dell'implementazione di un client telnet seguendo queste righe, ma non sono riuscito a trovarla di nuovo. Sarebbe stato un riferimento molto utile!

¹: Per questo, segui la tua intuizione o trova le classi di equivalenza dei metodi nella tua effettiva implementazione per la relazione "method₁ ~ method₂ iff. è valido usarli sullo stesso oggetto ”- supponendo che tu abbia un grande oggetto che incapsula tutti i trattamenti sul tuo dispositivo. Entrambi i metodi per elencare gli stati danno risultati fantastici.


1
Invece di definire strutture separate, può essere sufficiente definire le interfacce necessarie che un oggetto in ciascuna fase dovrebbe presentare. Quindi è il modello del generatore di passaggi .
Joshua Taylor,

2

Usa uno schema di costruzione.

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

Dopo aver definito tutte le operazioni sul builder, si chiama un executemetodo. Quando viene chiamato questo metodo, esegue tutti i passaggi sopra elencati nell'ordine corretto con le operazioni memorizzate sopra. Questo metodo è anche un buon posto per eseguire alcuni controlli di integrità che coprono le operazioni (come tentare di configurare una risorsa che non era ancora stata impostata) prima di scriverli sull'hardware. Questo potrebbe salvarti dal danneggiare l'hardware con una configurazione senza senso (nel caso in cui l'hardware sia sensibile a questo).


1

Devi solo documentare correttamente come viene utilizzata l'interfaccia e fare un esempio tutorial.

È inoltre possibile disporre di una variante della libreria di debug che esegue alcuni controlli di runtime.

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

A proposito, molte interfacce esistenti seguono un modello simile (ad es. Kit di strumenti X11).


Un diagramma di transizione di stato, simile al ciclo di vita delle attività delle applicazioni Android , può essere necessario per trasmettere le informazioni.
rwong,

1

Questo è davvero un tipo comune e insidioso di errore, perché i compilatori possono solo applicare 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 davvero incoraggiare le persone a non fare cose non schematiche, dovresti distribuire un oggetto comando di qualche tipo che deve essere inizializzato con valori per le condizioni preliminari, in modo che non possano eseguire i passaggi fuori servizio.


Vuoi dire qualcosa di simile a questo ?
Vorac,

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 una ExecutorFactory che costruirà Executor con percorsi di esecuzione personalizzati.


In un altro commento hai chiamato questa un'implementazione di step builder, ma non lo è. Se si dispone di un'istanza di MyStepsRunnable, è possibile chiamare il passaggio 3 prima del passaggio 1. Un'implementazione di step builder sarebbe più simile a ideone.com/UDECgY . L'idea è quella di ottenere quel qualcosa con un passaggio 2 eseguendo il passaggio 1. Quindi sei costretto a chiamare i metodi nell'ordine giusto. Ad esempio, consultare stackoverflow.com/q/17256627/1281433 .
Joshua Taylor,

Puoi convertirlo in una classe astratta con metodi protetti (o anche di default) per limitare il modo in cui può essere utilizzato. Sarai costretto a usare l'esecutore, ma ho che potrebbero esserci un difetto o due con l'implementazione corrente.
Silviu Burcea,

Ciò non lo rende ancora un costruttore di passi. Nel tuo codice, non c'è nulla che un utente possa fare per eseguire il codice tra i diversi passaggi. L'idea non è solo quella di sequenziare il codice (indipendentemente dal fatto che sia pubblico o privato o altrimenti incapsulato). Come mostra il tuo codice, è abbastanza facile da fare semplicemente step1(); step2(); step3();. Il builder point of step è fornire un'API che espone alcuni passaggi e applicare la sequenza in cui vengono chiamati. Non dovrebbe impedire a un programmatore di fare altre cose tra i passaggi.
Joshua Taylor,
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.