Diseño de interfaz donde las funciones deben llamarse en una secuencia específica

24

La tarea es configurar una pieza de hardware dentro del dispositivo, de acuerdo con algunas especificaciones de entrada. Esto debe lograrse de la siguiente manera:

1) Recopile la información de configuración. Esto puede suceder en diferentes momentos y lugares. Por ejemplo, el módulo A y el módulo B pueden solicitar (en diferentes momentos) algunos recursos de mi módulo. Esos 'recursos' son en realidad la configuración.

2) Una vez que está claro que no se realizarán más solicitudes, debe enviarse al hardware un comando de inicio que proporcione un resumen de los recursos solicitados.

3) Solo después de eso, puede (y debe) realizarse una configuración detallada de dichos recursos.

4) Además, solo después de 2), se puede (y debe) enrutar los recursos seleccionados a las personas que llaman declaradas.


Una causa común de errores, incluso para mí, que escribí la cosa, es confundir esta orden. ¿Qué convenciones de nombres, diseños o mecanismos puedo emplear para que alguien que vea el código por primera vez pueda utilizar la interfaz?

Vorac
fuente
Etapa 1 se llama mejor discoveryo handshake?
rwong
1
El acoplamiento temporal es un antipatrón y debe evitarse.
1
El título de la pregunta me hace pensar que podría estar interesado en el patrón del generador de pasos .
Joshua Taylor el

Respuestas:

45

Es un rediseño, pero puede evitar el uso indebido de muchas API pero no tener disponible ningún método que no deba llamarse.

Por ejemplo, en lugar de first you init, then you start, then you stop

Su constructor inites un objeto que se puede iniciar y startcrea una sesión que se puede detener.

Por supuesto, si tiene una restricción para una sesión a la vez, debe manejar el caso en el que alguien intenta crear una con una ya activa.

Ahora aplique esa técnica a su propio caso.

CashCow
fuente
zliby jpeglibson dos ejemplos que siguen este patrón para la inicialización. Aún así, se necesitan muchas documentaciones para enseñar el concepto a los desarrolladores.
rwong
55
Esta es exactamente la respuesta correcta: si el orden importa, cada función devuelve un resultado que luego se puede invocar para realizar el siguiente paso. El compilador en sí mismo puede imponer las restricciones de diseño.
2
Esto es similar al patrón del generador de pasos ; solo presente la interfaz que tiene sentido en una fase dada.
Joshua Taylor el
@JoshuaTaylor mi respuesta es una implementación de patrón de generador de pasos :)
Silviu Burcea
@SilviuBurcea Su respuesta no es una implementación de creador de pasos, pero lo comentaré más que aquí.
Joshua Taylor el
19

Puede hacer que el método de inicio devuelva un objeto que es un parámetro requerido a la configuración:

Recurso * MyModule :: GetResource ();
MySession * MyModule :: Startup ();
Recurso nulo :: Configurar (sesión de MySession *);

Incluso si su MySessiones solo una estructura vacía, esto impondrá a través de la seguridad de tipo que no Configure()se puede llamar a ningún método antes del inicio.

jpa
fuente
¿Qué impide que alguien haga module->GetResource()->Configure(nullptr)?
svick
@svick: Nada, pero debes hacer esto explícitamente. Este enfoque le dice lo que espera y eludir esa expedición es una decisión consciente. Como con la mayoría de los lenguajes de programación, nadie te impide dispararte en el pie. Pero siempre es bueno que una API indique claramente que lo estás haciendo;)
Michael Klement
+1 se ve genial y simple. Sin embargo, puedo ver un problema. Si tengo objetos a, b, c, d, entonces puedo comenzar a, y usarlo MySessionpara intentar usarlo bcomo un objeto ya iniciado, mientras que en realidad no lo es.
Vorac
8

Basándose en la respuesta de Cashcow: ¿por qué tiene que presentar un nuevo objeto a la persona que llama, cuando puede presentar una nueva interfaz? Rebrand-Pattern:

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

También puede dejar que ITerminateable implemente IRunnable, si una sesión se puede ejecutar varias veces.

Su objeto:

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...

De esta forma, solo puede llamar a los métodos correctos, ya que solo tiene la interfaz IStartable al principio y obtendrá el método run () solo accesible cuando haya llamado a start (); Desde el exterior, parece un patrón con múltiples clases y objetos, pero la clase subyacente sigue siendo una clase, a la que siempre se hace referencia.

Falco
fuente
1
¿Cuál es la ventaja de tener solo una clase subyacente en lugar de varias? Como esta es la única diferencia con la solución que propuse, me interesaría este punto en particular.
Michael Le Barbier Grünewald
1
@ MichaelGrünewald No es necesario implementar todas las interfaces con una clase, pero para un objeto de tipo de configuración, puede ser la técnica de implementación más simple para compartir los datos entre instancias de las interfaces (es decir, porque se comparte en virtud de ser el mismo objeto).
Joshua Taylor el
1
Este es esencialmente el patrón del generador de pasos .
Joshua Taylor el
@JoshuaTaylor Compartir datos entre instancias de la interfaz es doble: si bien puede ser más fácil de implementar, debemos tener cuidado de no acceder al "estado indefinido" (como acceder a la dirección del cliente de un servidor no conectado). A medida que el OP pone énfasis en la usabilidad de la interfaz, podemos juzgar los dos enfoques por igual. Gracias por citar el "patrón de creación de pasos" BTW.
Michael Le Barbier Grünewald
1
@ MichaelGrünewald Si solo interactúa con el objeto a través de la interfaz particular que se especifica en un punto dado, no debería haber ninguna forma (sin conversión, etc.) para acceder a ese estado.
Joshua Taylor el
2

Hay muchos enfoques válidos para resolver su problema. Basile Starynkevitch propuso un enfoque de "burocracia cero" que lo deja con una interfaz simple y confía en que el programador use adecuadamente la interfaz. Si bien me gusta este enfoque, presentaré otro que tiene más eingineering pero permite que el compilador detecte algunos errores.

  1. Identificar los diferentes estados de su dispositivo puede estar en, como Uninitialised, Started, Configuredy así sucesivamente. La lista tiene que ser finita.

  2. Para cada estado, defina structla información adicional necesaria relevante para ese estado, por ejemplo DeviceUninitialised, DeviceStartedetc.

  3. Empaque todos los tratamientos en un objeto DeviceStrategydonde los métodos usan estructuras definidas en 2. como entradas y salidas. Por lo tanto, puede tener un DeviceStarted DeviceStrategy::start (DeviceUninitalised dev)método (o cualquiera que sea el equivalente según las convenciones de su proyecto).

Con este enfoque, un programa válido debe llamar a algunos métodos en la secuencia aplicada por los prototipos de métodos.

Los diversos estados son objetos no relacionados, esto se debe al principio de sustitución. Si le resulta útil que estas estructuras compartan un ancestro común, recuerde que el patrón de visitante se puede utilizar para recuperar el tipo concreto de la instancia de una clase abstracta.

Mientras describí en 3. una DeviceStrategyclase única , hay situaciones en las que es posible que desee dividir la funcionalidad que proporciona en varias clases.

Para resumirlos, los puntos clave del diseño que describí son:

  1. Debido al principio de sustitución, los objetos que representan estados de dispositivo deben ser distintos y no tener relaciones de herencia especiales.

  2. Empaque los tratamientos del dispositivo en objetos de estrategia en lugar de en los objetos que representan los dispositivos mismos, de modo que cada dispositivo o estado del dispositivo se vea solo a sí mismo, y la estrategia los vea a todos y exprese posibles transiciones entre ellos.

Juraría que vi una vez una descripción de la implementación de un cliente telnet siguiendo estas líneas, pero no pude encontrarla nuevamente. ¡Habría sido una referencia muy útil!

¹: Para esto, siga su intuición o encuentre las clases de equivalencia de métodos en su implementación real para la relación “método₁ ~ método₂ iff. es válido usarlos en el mismo objeto ", suponiendo que tenga un gran objeto que encapsule todos los tratamientos en su dispositivo. Ambos métodos de enumerar estados dan resultados fantásticos.

Michael Le Barbier Grünewald
fuente
1
En lugar de definir estructuras separadas, puede ser suficiente definir las interfaces necesarias que debe presentar un objeto en cada fase. Entonces es el patrón del generador de pasos .
Joshua Taylor el
2

Usa un patrón de construcción.

Tenga un objeto que tenga métodos para todas las operaciones que mencionó anteriormente. Sin embargo, no realiza estas operaciones de inmediato. Solo recuerda cada operación para más tarde. Debido a que las operaciones no se ejecutan de inmediato, el orden en que las pasa al constructor no importa.

Después de definir todas las operaciones en el constructor, llama a un executemétodo. Cuando se llama a este método, realiza todos los pasos que enumeró anteriormente en el orden correcto con las operaciones que almacenó anteriormente. Este método también es un buen lugar para realizar algunas comprobaciones de cordura que abarcan toda la operación (como intentar configurar un recurso que aún no estaba configurado) antes de escribirlas en el hardware. Esto podría salvarlo de dañar el hardware con una configuración sin sentido (en caso de que su hardware sea susceptible a esto).

Philipp
fuente
1

Solo necesita documentar correctamente cómo se usa la interfaz y dar un ejemplo tutorial.

También puede tener una variante de biblioteca de depuración que realiza algunas comprobaciones de tiempo de ejecución.

Tal vez la definición y documentación correctamente algunas convenciones de nombres (por ejemplo preconfigure*, startup*, postconfigure*, run*....)

Por cierto, muchas interfaces existentes siguen un patrón similar (por ejemplo, kits de herramientas X11).

Basile Starynkevitch
fuente
Puede ser necesario un diagrama de transición de estado, similar al ciclo de vida de la actividad de la aplicación de Android , para transmitir la información.
rwong
1

Este es de hecho un tipo de error común e insidioso, porque los compiladores solo pueden imponer condiciones de sintaxis, mientras que usted necesita que sus programas cliente sean "gramaticalmente" correctos.

Desafortunadamente, las convenciones de nomenclatura son casi completamente ineficaces contra este tipo de error. Si realmente desea alentar a las personas a no hacer cosas no gramaticales, debe pasar un objeto de comando de algún tipo que debe inicializarse con valores para las condiciones previas, de modo que no puedan realizar los pasos fuera de orden.

Kilian Foth
fuente
¿Te refieres a algo como esto ?
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();
}

Al usar este patrón, está seguro de que cualquier implementador se ejecutará en este orden exacto. Puede ir un paso más allá y crear una ExecutorFactory que construirá Ejecutores con rutas de ejecución personalizadas.

Silviu Burcea
fuente
En otro comentario , llamó a esto una implementación de generador de pasos, pero no lo es. Si tiene una instancia de MyStepsRunnable, puede llamar al paso 3 antes del paso 1. Una implementación de creador de pasos sería más similar a ideone.com/UDECgY . La idea es solo obtener ese algo con un paso 2 ejecutando el paso 1. Por lo tanto, se ve obligado a llamar a los métodos en el orden correcto. Por ejemplo, consulte stackoverflow.com/q/17256627/1281433 .
Joshua Taylor
Puede convertirlo en una clase abstracta con métodos protegidos (o incluso predeterminados) para restringir la forma en que se puede usar. Te verás obligado a usar el ejecutor, pero tengo la impresión de que puede haber una falla o dos con la implementación actual.
Silviu Burcea
Eso todavía no lo convierte en un constructor de pasos. En su código, no hay nada que un usuario pueda hacer para ejecutar el código entre los diferentes pasos. La idea no es solo secuenciar el código (independientemente de si es público o privado, o de otro modo encapsulado). Como muestra su código, es bastante fácil hacerlo simplemente step1(); step2(); step3();. El objetivo del generador de pasos es proporcionar una API que exponga algunos pasos y aplicar la secuencia en la que se los llama. No debería evitar que un programador haga otras cosas entre pasos.
Joshua Taylor el