cafetera eléctrica oo - dsi.fceia.unr.edu.ar©ctrica_oo.pdf · funciones de interface del hardware...

Post on 27-Jul-2020

5 Views

Category:

Documents

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Cafetera eléctrica OO

Mark IV Special Coffee Maker

• En el libro “Designing object-oriented C++ applications using the Booch method” (1995) de Robert Martin, se plantea diseñar software orientado a objetos (OO) para implementarlo en C++ y,

• ejecutado en un pequeño sistema embebido que controle las distintas funciones de una cafetera eléctrica “especial”

Mark IV Special Coffee Maker

• la idea era demostrar que C++ era viable en este tipo de plataforma y que,

• el DOO (Diseño orientado a objetos) es una metodología apropiada para este tipo de sistemas

Mark IV Special Coffee Maker

• En el 2002, Robert Martin, en el libro “UML for Java programmers” retoma este ejemplo, puesto que, encontró que sus alumnos a menudo repetían algunos errores de diseño y,

• deseaba mostrarles porqué eran errores de diseño y cómo podían corregirlos

Especificación de Mark IV

Especificación de Mark IV

• La cafetera prepara hasta 12 tazas de café por vez.

• El usuario pone un filtro en el portafiltros, llena el filtro con café molido y coloca el portafiltros en su receptáculo.

• El usuario vierte hasta 12 tazas de agua y presiona el botón “Brew”.

• El agua se calienta hasta que hierva. El vapor que se forma hace que el agua se rocíe sobre el café molido y, la infusión de café gotea a través del filtro y cae en la jarra.

Especificación de Mark IV

• La jarra se mantiene caliente por extensos períodos por un plato calefactor, el cual se activa sólo si hay café en la jarra.

• Si la jarra se retira del plato calefactor mientras está goteando el café, se detiene el flujo de agua, de forma tal que el café elaborado no se derrame sobre el plato calefactor.

Especificación • El siguiente hardware necesita ser controlado o

monitoreado:

• El elemento calefactor para el atomizador (boiler), puede activarse o desactivarse (on/off)

• Un sensor para el atomizador que, determina si hay agua o no. Puede adoptar 2 estados: boilerempty/boilernotempty

• El elemento calefactor para el plato, puede activarse o desactivarse (on/off)

• El sensor para el plato calefactor. Puede adoptar 3 estados: warmer empty (la jarra se quitó)/potempty/pot not empty

Especificación

• El botón Brew que se usa para iniciar el ciclo de elaboración del café.

• Tiene un indicador que se enciende cuando el ciclo de elaboración terminó y el café está listo

• Una válvula de alivio de presión que se abre para reducir la presión del atomizador. La caída de presión detiene el flujo de agua al filtro. Puede abrirse o cerrarse

Funciones de interface del hardware

• El hardware se completó.

• Los ingenieros de hardware proveyeron un API de bajo nivel en C, de forma tal de no tener que escribir código para los drivers de I/O a nivel de bits.

• El archivo runtime.h que proveyeron con la interfaz con el hardware:

Funciones de interface del hardware

• El switch en el plato calentador es un transductor de presión, se comporta como sigue

• P<T1 WarmerEmpty

• P>T1 && p<T2 potEmpty

• P>T2 potNotEmpty

Funciones de interface del hardware

Funciones de interface del hardware

Funciones de interface del hardware

Diseño del software de la Mark IV

• En el 1º libro Robert plantea ¿cómo comenzar el proceso de diseño?

• El autor postula como método útil es el intento de describir la secuencia de eventos derivados de un estímulo particular,

• por ejemplo: ¿Qué sucede cuando el usuario presiona el botón Brew?

• Esto es similar a crear un modelo dinámico aunque sin concentrarse en objetos y mensajes

Diseño

• El ciclo de elaboración de la infusión café no puede comenzar hasta que:

• haya agua en el atomizador,

• un recipiente vacío sobre el vaso calentador y,

• se presiona el botón Brew.

• Esto es:

• GetBoilerStatus retorna boilerNotEmpty,

• GetWarmerStatus retorna potEmpty y,

• GetBrewButtonStatus retorna BrewButtonPushed

Diseño

• El ciclo de elaboración comienza

• activando el elementocalefactor del atomizador,

• también habría que asegurarse que la válvula esté cerrada y,

• apagar el indicador lumínico.

• Esto implica invocar:

• SetIndicatorState(indicatorOff);

• SetReliefValveState(valveClosed);

• SetBoilerState(boilerOn);

Diseño

• A medida que el agua rocía el café molido, el recipiente comienza a llenarse y el plato calentador indica potNotEmpty,

• entonces habría que activar el elemento calentador del plato , es decir cuando

• GetWarmerStatus retorne potNotEmpty,

• se debería invocar SetWarmerState(warmerOn);

Diseño

• Cuando el atomizador está casi vacío GetBoilerStatus retornará boilerEmpty, entonces

• se debería desactivar el elemento calefactor del atomizador y,

• encender el indicador lumínico, indicando que el café está listo.

• Esto se logra invocando:

• SetBoilerState(boilerOff);

• SetIndicatorState(indicatorOn);

Diseño • Lejos de estar completo, podemos reconocer

el análisis anterior como un sistema que se mueve de estado a estado basado en un conjunto de eventos externos.

• Esto es claramente una máquina de estados finitos (FSM).

Diseño

• El autor, en su libro de C++, plantea que podría haberse detenido en la 1º FSM y codificarla en C o, en C++ sin objetos.

• Pero, se plantea que su compañía se propone diseñar otras cafeteras y, entonces piensan que la estrategia de plantear un diseño orientado a objetos es correcta.

• Invertir tiempo extra en el diseño y llegar a crear una librería de componentes reusables para facilitar la creación de productos similares

Hallar los objetos

• Uno de los primeros y más grandes problemas con los cuales se enfrenta el diseñador es descubrir los objetos, las principales abstracciones del problema.

• Hallar estos objetos puede ser una tarea muy complicada

• “The translation of a concept in the application area into a class in a design is not a simple mechanical operation. It often requires significant insights.” Stroustrup (1991)

Enfoque tradicional OO

• Buscar los sustantivos (y sustantivos adjetivados) en el problema y modelarlos con clases

• Buscar verbos en las especificaciones del problema y modelarlos como comportamientos de dichas clases (funciones)

• Describir las relaciones entre clases

Una solución común, aunque terrible

• El autor del libro al plantear este problema de diseño a sus alumnos encontró que la mayoría de los estudiantes ideó una solución tal como la que se verá en el siguiente diagrama de clases UML

Solución de diseño común, pero terrible

• En el diagrama se observa una clase central CoffeeMaker rodeada de “sirvientes” (minions) que controlan distintos dispositivos de hardware

• La clase CoffeeMaker contiene un Boiler, un WarmerPlate, un Button y una Light

• La clase Boiler contiene un BoilerSensor y un BoilerHeater

• La clase WarmerPlate contiene un PlateSensor y un PlateHeater

• Finalmene hay 2 clases bases , Sensor y Heater, que actúan como clases bases de Boiler y WarmerPlate, respectivamente.

Solución de diseño común pero terrible

• Hay algunos errores serios escondidos en este diagrama.

• Muchos de estos errores no se registran hasta que se intenta llevar a código este diseño, momento en el cual se manifiesta que el código es absurdo.

Faltan métodos

• El problema mayor del diagrama de clases anterior es una falta completa de métodos.

• Los programas tratan con comportamiento!

• ¿Dónde está el comportamiento en este diagrama?

• Los diseñadores al crear este diagrama sin métodos pueden estar particionando el software en algo que no sea el comportamiento (se basaron en el hardware)

Faltan métodos

• La partición del software que no se basa en comportamiento, casi siempre conduce a errores significativos.

• El comportamiento del sistema es el primer indicio para particionar el software

Clases vapor • Para poder apreciar qué tan pobremente se particionó este

diseño particular, consideremos los métodos que pondríamos en la clase Light

• Claramente un objeto de esta clase puede encenderse (on) o apagarse (off), por tanto podría implementarse así:

class Light{

public: static void on(){ SetIndicatorState(indicatorOn); } static void off(){ SetIndicatorState(indicatorOff); } };

Clases vapor

• Hay algunas cosas peculiares en la clase Light, en primer lugar no hay variables, esto es anormal puesto que, usualmente un objeto tiene alguna clase de estado que manipula.

• Segundo, los métodos on y off simplemente delegan a la función SetIndicatorState() del API,

• aparentemente la clase Light no es más que un adaptador de llamadas a funciones, realmente no está haciendo nada útil

Clases vapor

• ¿Porqué estas funciones miembro inline se declaran static?

• El sistema en tiempo de ejecución que fue dado por los ingenieros de hardware, no permite más de un atomizador, indicador, botón, etc.

• Por tanto no tiene sentido instanciar ninguna de las clases mencionadas puesto que, no puede haber más que una instancia de cada una de ellas, por lo cual,

• bien podríamos llamar a estas funciones como Light::on().

• Pero, ¿cual es la diferencia entre esto y simplemente llamar a SetIndicatorState(indicatorOn)? No mucha

Clases vapor

• El mismo razonamiento puede aplicarse a las clases Boiler, Button y WarmerPlate.

• Quizás hemos fallado en hallar las verdaderas abstracciones.

• Quizás deberíamos examinar más a fondo el problema y tratar de hallar las abstracciones subyacentes.

• Podríamos quitarlas del diseño sin cambiar para nada la lógica de la clase CoffeeMaker, la cual podría llamar directamente a las funciones del API, sin necesidad de hacerlo a través de adaptadores.

Clases vapor • Considerando todo esto hemos degradado

estas clases de su posición prominente en el diagrama de clases en meras variables sin mucha razón para existir. Por eso el autor las llama clases vapor

Abstracciones imaginarias

• Observemos las clases base Sensor y Heater. Habíamos visto que sus clases derivadas “venden humo” pero,

• ¿qué ocurre con estas clases? A simple vista parecen tener mucho sentido.

• Las abstracciones suelen ser engañosas, los humanos solemos verlas por todos lados pero, muchas de ellas no son apropiadas para ser convertidas en clases.

• Estas en particular, no tienen lugar en el diseño. ¿Quién las usa?

Abstracciones imaginarias • Ninguna clase del sistema realmente usa estas clases base. • Si nadie las usa ¿Cuál es el sentido para que existan? • A veces puede tolerarse una clase base que nadie usa, si

suministran código a sus derivadas pero, • en estas clases base no hay código, sólo métodos abstractos.

Oficialmente inútil

class Heater{ public: virtual void on()=0; virtual void off()=0; };

Abstracciones imaginarias • La clase Sensor es peor! • Al igual que Heater tiene sólo métodos abstractos y

ningún usuario. • Es peor porque, su único método tiene valores de

retorno ambigüos. En BoilerSensor retorna 2 valores posibles (boilerOn/boilerOff) pero, en WarmerPlateSensor retorna 3 valores posibles (warmerEmpty, potEmpty, potNotEmpty).

• No podemos especificar el contrato de Sensor, lo mejor que se puede hacer es retorna un entero, esto es bastante débil

class Sensor{ public: virtual int sense()=0; }

Abstracciones

• Lo que ocurrió es que, leído las especificaciones,

• hallado un conjunto de sustantivos probables,

• hecho inferencias acerca de sus relaciones y creado un diagrama de clases UML basado en este razonamiento,

• se las implementó y terminó quedando una clase todopoderosa CoffeeMaker rodeada de una serie de sirvientes jactanciosos.

• Bien podríamos programar esto en C!

Clases dioses

Clases dioses

• Son una mala idea.

• No es buena idea concentrar toda la inteligencia de un sistema en un solo objeto o en una sola función.

• Uno de los objetivos del diseño OO es la partición y distribución del comportamiento en muchas clases y muchas funciones.

• Resulta, sin embargo que, muchos modelos de objetos que parecen ser distribuidos son en realidad, el disfraz de la morada de los dioses

Clases dioses • El diagrama de clases visto es el perfecto

ejemplo. A primera vista parece que hubiese muchas clases con comportamiento interesante pero, profundizando en el código que implementarían dichas clases se halla que, sólo una tiene un comportamiento interesante: CoffeeMaker y el resto son clases vapor o abstracciones imaginarias

Análisis OO: Buscar las abstracciones subyacentes

• Resolver este problema de la cafetera es un ejercicio interesante en abstracción

• El truco es retroceder y separar los detalles de la naturaleza esencial.

• Olvidémonos de atomizadores, válvulas, calentadores, sensores y todos los pequeños detalles y concentrémonos en el problema subyacente.

• ¿Cuál es el problema?: ¿Cómo preparar café?

Análisis OO: Buscar las abstracciones subyacentes

• Pero ¿qué se necesita para preparar café? • Los ingenieros de hardware proveyeron un buen sistema en tiempo

de ejecución que nos permite llevar a cabo esta tarea, sin embargo, el mismo está completamente ligado a la solución del problema y no a la tarea real de preparar café.

• Por ejemplo, el propósito de la válvula es aliviar la presión en el atomizador, de forma tal que pare de rociar agua sobre el café molido pero, la abstracción subyacente no tiene nada que ver con válvulas, atomizadores o presión!

• La abstracción subyacente es simplemente controlar el rociado de agua sobre el café molido y, esta abstracción permanece válida independientemente de la solución de hardware.

• Los ingenieros de hardware podrían reemplazar la válvula y añadir una bomba de agua caliente y, esto no cambiaría la abstracción

Análisis OO: Buscar las abstracciones subyacentes

• La implementación de esta abstracción tiene algunas complejidades.

• Para rociar agua caliente, el elemento calefactor del atomizador tiene que activarse y, la válvula tiene que cerrarse.

• Para detener el rociado la válvula debe abrirse y, probablemente, se desee desactivar el elemento calefactor del atomizador puesto que, probablemente no se desee que la cafetera exude una gran columna de vapor (salvo que se quisiera usar como humidificador)

Análisis OO: Buscar las abstracciones subyacentes

• Cuando complejidades como estas pueden ocultarse por una simple abstracción, se gana terreno sobre la complejidad del problema, no se elimina la complejidad sino que se la maneja!,

• queda aislada dentro de la abstracción, de la cual no puede escapar para contaminar el resto de la aplicación

• ¿Qué acerca del atomizador? ¿Representa una abstracción separada de esta?

• Es irrelevante que la cafetera lo tenga mientras que, nuestra abstracción entregue agua caliente. Sin embargo, el atomizador puede bien aparecer nuevamente, como una abstracción de segundo nivel dentro de esta que el autor llama HotWaterSource

Análisis OO: Búscar las abstracciones subyacentes

• La más simple y más común solución a este problema es: – Rociar agua caliente sobre café molido y,

– Obtener la infusión resultante en alguna clase de recipiente

• ¿De dónde sacamos el agua caliente? Llamémosle HotWaterSource

• ¿Dónde recogemos la infusión? Llamémosle ContainmentVessel

• Estas 2 abstracciones ¿son realmente clases?

Asignar responsabilidades a las abstracciones

• Asignar responsabilidades a las abstracciones escogidas, a veces, es tan difícil como hallar los objetos

• O, quizás es más difícil, como señala Booch (1991), “This step is much harder than the first and takes much longer. This is the phase in which there may be fierce debates, wailing and gnashing of teeth, and general name-calling during design reviews. Finding classes and objects is the easy part; deciding upon the protocol of each object is hard.”

Asignar responsabilidades a las abstracciones

• Sería bueno desacoplar las abstracciones halladas cada una de la otra,

• dividiendo las responsabilidades y manteniéndolas separadas.

• Sin embargo, esto puede ser todo un desafío

Una posible solución

• ¿Qué comportamiento de HotWaterSource puede capturarse en software?

– Calentar el agua, rociarla sobre el café molido para que gotee sobre el recipiente

– Podemos imaginar al atomizador, válvula y sensor del atomizador jugando el rol de HotWaterSource

Una posible solución

• ¿ContainmentVessel hace algo que el software podría controlar?

– Sería responsable de mantener caliente la infusión de café contenida en el recipiente; también permitiría conocer si queda café o no en el recipiente y, si está o no el contenedor sobre el plato calentador

– Podemos imaginar al plato calentador y a su sensor jugando el rol de ContainmentVessel

Cables cruzados

Cables cruzados

• ¿Cómo se podría capturar la anterior discusión en un diagrama de clases UML?

• Un posible esquema donde HotWaterSource y ContainmentVessel son representados como clases y asociadas por el flujo del café (infusión)

Cables cruzados

• La asociación muestra un error que los iniciados en OO suelen cometer.

• La asociación se realizó en base a algo físico acerca del problema en vez del comportamiento del software de control.

• El hecho que el café fluye del HotWaterSource a ContainmentVessel es totalmente irrelevante para la asociación entre estas dos clases

Cables cruzados

• Por ejemplo, ¿qué si ContainmentVessel le indica a HotWaterSource cuando comenzar y cuándo detener el flujo del agua caliente hacia el recipiente?

Cables cruzados • ContainmentVessel envía el mensaje start a

HotWaterSource.

• Esto indica que la asociación es al revés. HotWaterSource no depende para nada de ContainmentVessel.

• Mas bien, ContainmentVessel depende de HotWaterSource

Cables cruzados

• La lección aquí es: las asociaciones son los caminos a través de los cuales se envían mensajes a los objetos, no tienen nada que ver con el flujo de objetos físicos.

• El hecho que el agua caliente fluya del atomizador al recipiente no indica que debería haber una asociación de HotWaterSource hacia ContainmentVessel

• El autor llama cables cruzados a este error particular debido a que la conexión entre las clases se cruzaron entre los dominios lógicos y los dominios físicos

La interfaz de usuario de la cafetera • Está claro que algo está faltando en nuestro modelo de

la cafetera. • No hay ninguna forma para que el humano interactúe

con el sistema. En algún lado nuestro sistema tiene que atender los comandos del humano.

• Igualmente el sistema debe ser capaz de reportar su estado a los propietarios humanos.

• Ciertamente, esta cafetera tiene hardware dedicado a este propósito. El botón y la luz sirven como interfaz de usuario (UI)

• Por tanto, se añadirá la clase UserInterface a nuestro modelo de cafetera.

• Esto proporciona una triada de clases interactuando para preparar café bajo la dirección de un usuario

Casos de uso

Casos de uso

• Dadas estas 3 clases, ¿Cómo se comunican sus instancias?

• Veamos los siguientes casos de uso para ver si descubrimos cuál es el comportamiento de estas clases

Caso de uso 1: El usuario pulsa el botón Brew

• ¿Qué objeto (de qué clase) detecta el hecho que el usuario presiona el botón Brew?

• Claramente debe ser el objeto UserInterface

• ¿Qué hace este objeto cuando se pulsa este botón?

Caso de uso 1: El usuario pulsa el botón Brew

• Nuestro objetivo es iniciar el flujo de agua caliente.

• Sin embargo, antes de hacer eso, hay que asegurarnos que ContainmentVessel está listo para aceptar café.

• También sería mejor asegurarse que HotWaterSource esté lista.

• Para esta cafetera esto significa asegurar que el atomizador está lleno y que, el recipiente está vacío y situado sobre el plato calentador.

Caso de uso 1: El usuario pulsa el botón Brew

• De forma tal que, la primera acción que nuestro objeto UserInterface realizar es enviar un mensaje a HotWaterSource y ContainmentVessel encuestándolos para ver si están listos.

Caso de uso 1: El usuario pulsa el botón Brew

• Si cualquiera de estas peticiones retorna false entonces no debería comenzar a prepararse el café.

• El objeto UserInterface puede hacerse cargo de informarle al usuario de esta situación (requerimiento denegado). En el caso de esta cafetera podria hacer titilar la luz varias veces

• Si ambas peticiones retornan true debe comenzar el flujo de agua caliente.

Caso de uso 1: El usuario pulsa el botón Brew

• Probablemente el objeto UserInterface enviaría un mensaje Start al objeto HotWaterSource, éste hará lo necesario para proveer agua caliente que se rociará sobre el café.

• En el caso de esta cafetera se cerrará la válvula y activará (on) el elemento calefactor del atomizador.

• El escenario completo de este caso de uso representado en un diagrama de colaboración UML:

Caso de uso 1: El usuario pulsa el botón Brew

Caso de uso 2: el recipiente no está listo

• En las especificaciones de la cafetera sabemos que el usuario puede retirar el recipiente del plato calentador durante la elaboración del café.

• ¿Qué objeto detecta esta situación?

• Sin duda será el ContainmentVessel.

• Según los requerimientos, cuando esto sucede, hay que detener el flujo de café.

Caso de uso 2: el recipiente no está listo

• Por tanto, ContainmentVessel debe ser capaz de comunicarle a HotWaterSource que pare de enviarle agua caliente.

• Igualmente debe ser capaz de informarle que comience nuevamente a producir café cuando el recipiente se repone sobre el plato calentador.

• En la figura se añaden nuevos métodos:

Caso de uso 2: el recipiente no está listo

Caso de uso 3: Preparación del café completa

• En algún momento se habrá terminado la elaboración de la infusión café y se deberá cortar el flujo de agua caliente.

• ¿Cuál de los objetos sabe cuando la preparación se completó?

• En el caso de esta cafetera el sensor en el atomizador nos indica que el atomizador está vacío, o sea, es detectado por el objeto HotWaterSource

Pensando en reusar las abstracciones

Pensando en reusar las abstracciones

Pensando en reusar las abstracciones

• https://sprudge.com/5-ridiculous-coffee-combination-brewers-just-gotta-see-43583.html

Pensando en reusar las abstracciones

Caso de uso 3: Preparación del café completa

• Sin embargo, no es difícil imaginar una cafetera en la cual ContainmentVessel pueda detectar que la preparación del café está lista,

• ¿Qué si la cafetera estaría conectada a la red de agua y por tanto tendría un suministro infinito de agua?

• ¿Qué si el agua fuese calentada por un generador de microondas intenso como si fluyese a través de los tubos de un recipiente térmicamente aislado?

• ¿Qué si el recipiente tendría una espita desde donde el usuario obtendría su café?

• En este caso el recipiente tendría un sensor que sabría si éste está lleno y que, por tanto debería cortarse el flujo de agua caliente

Caso de uso 3: Preparación del café completa

• El punto está en el dominio abstracto de HotWaterSource y ContainmentVessel, ninguno de los 2 es un candidato convincente para detectar que se terminó la elaboración del café.

• La solución del autor es ignorar este tema, asumiendo que cualquiera de los objetos puede informarle a los otros que la elaboración está lista.

Caso de uso 3: Preparación del café completa

• En nuestro modelo ¿Cuáles objetos necesitan conocer que la preparación está lista?

• Ciertamente UserInterface necesita saberlo desde que esta cafetera, cuando esto ocurre, debe encender una luz.

• Está claro que también HotWaterSource necesita saber que la preparación se terminó para detener el flujo de agua caliente.

• ¿ContainmentVessel necesita saberlo? ¿Hay algo especial que necesita hacer o controlar una vez que la preparación se terminó?.

Caso de uso 3: Preparación del café completa

• En esta cafetera, hay que detectar que el recipiente vacío se coloca nuevamente en el plato calentador, indicando que el usuario se ha servido lo último que quedaba del café, con lo cual debería apagarse la luz.

• Por tanto ContainmentVessel necesita saber que la preparación está lista.

• De hecho, el mismo argumento puede usarse para decir que UserInterface enviaría el mensaje Start al ContainmentVessel cuando comienza la preparación

Caso de uso 3: Preparación del café completa

• La figura muestra los nuevos mensajes. Puede notarse que HotWaterSource o ContainmentVessel, pueden enviar el mensaje Done

Caso de uso 4: el café se terminó

• Esta cafetera apaga la luz cuando la preparación está lista y se coloca un recipiente vacío sobre el plato calentador.

• Claramente en nuestro modelo ContainmentVessel detectaría esta situación y tendría que enviar un mensaje a UserInterface

• La figura muestra el diagrama completo de colaboración y, a partir de este diagrama, se puede dibujar un diagrama de clases con todas las asociaciones. Este diagrama no depara sorpresas

Implementando el modelo abstracto

• Nuestro modelo de objetos está razonablemente bien particionado.

• Tenemos 3 áreas distintas de responsabilidad, cada una de ella parecen estar enviando y recibiendo mensajes en forma balanceada.

• No parecen ser ni clases dioses ni clases vapor.

• Hasta aquí todo bien pero,

• ¿Cómo implementamos la cafetera en esta estructura?

• ¿Implementamos los métodos de estas 3 clases para invocar el API? Esto sería una verdadera lástima!

Implementando el modelo abstracto

• Hemos capturado la esencia de lo que implica elaborar café.

• Sería un lamentablemente pobre diseño si, vinculáramos esa esencia a nuestra cafetera MarkIV.

• El autor propone una nueva regla: ninguna de estas 3 clases debe saber nada acerca de la cafetera Mark IV.

• Este es el Principio de Inversión de la Dependencia (DIP). No vamos a permitir que la política de alto nivel de hacer café de este sistema dependa de detalles de la implementación de bajo nivel

Principio de inversión de dependencia (DIP)

Un ejemplo simple

Un ejemplo simple

Ejemplo completo de Robert Martin

• DIP puede aplicarse siempre que una clase envía un mensaje a otra.

• Por ejemplo, considere el caso de un objeto Button y uno Lamp

• El objeto Button sensa el entorno externo, puede determinar si un usuario lo ha presionado o no,

• sin importar el mecanismo de sensado. • Podría ser el ícono de un botón en una GUI, un botón

físico pulsado por un humano o aún un detector de movimiento en un sistema de seguridad hogareño

• El botón detecta que el usuario lo ha activado o desactivado

Ejemplo completo de Robert Martin

• El objeto Lamp afecta el entorno externo. Cuando recibe un mensaje TurnOn enciende una luz de algún tipo, cuando recibe el mensaje TurnOff se apaga dicha luz,

• el mecanismo físico no es importante.

• Podría ser un indicador LED en un aparato, una luz de vapor de mercurio en un estacionamiento o aún el laser de una impresora laser.

Ejemplo completo de Robert Martin

• ¿Cómo podemos diseñar un sistema donde el objeto Button controle al objeto Lamp?

• En la figura siguiente se observa un diseño naif.

• El objeto Button simplemente envía los mensajes TurnOn/TurnOff al objeto Lamp.

• Para facilitar esto la clase Button usa una relación “contiene” para mantener una instancia de la clase Lamp.

Ejemplo completo de Robert Martin

Ejemplo completo de Robert Martin

• Veamos el código en C++ que resulta de este modelo.

• La clase Button depende directamente de la clase Lamp, el módulo button.cc incluye el módulo lamp.h.

• Esta dependencia indica que la clase Button debe cambiar o, por lo menos recompilarse, siempre que cambie la clase Lamp.

• Asimismo, no será posible reusar la clase Button para control un objeto Motor.

Ejemplo completo de Robert Martin

Ejemplo completo de Robert Martin

• Se viola el DIP, la política de alto nivel de la aplicación no se separó de los módulos de bajo nivel;

• la abstracción no se separó de los detalles.

• Sin tal separación las políticas de alto nivel automáticamente dependen de los módulos de bajo nivel y,

• las abstracciones automáticamente dependen de los detalles

Ejemplo completo de Robert Martin

• ¿Qué son las políticas de alto nivel? Las abstracciones que forman la base de la aplicación, las verdades que no varían cuando se cambian los detalles.

• En este ejemplo, las abstracción de base es detectar una acción on/off de un usuario y transmitir esa acción al objeto “target”.

• El mecanismo usado para detectar esa acción es irrelevante!

• Cuál es el objeto target también es irrelevante! • Estos son detalles que no impactan la abstracción

Ejemplo completo de Robert Martin

• Para conformar el principio de DIP debemos aislar esta abstracción de los detalles del problema.

• Debemos dirigir las dependencias del diseño de forma tal que los detalles dependan de abstracciones.

• La siguiente figura muestra tal diseño, donde se aísla la abstracción de la clase Button

Ejemplo completo de Robert Martin

Ejemplo completo de Robert Martin

• En el código puede observarse que las políticas de alto nivel quedan enteramente capturada dentro de la clase abstracta Button.

• La clase Button no conoce nada sobre el mecanismo de detección de las acciones del usuario (el método Button::Detect() es un “template” que hace uso de la función virtual pura Button::GetState()),

• ni tampoco sabe nada acerca de la lámpara. • Estos detalles están aislados dentro de las clases

derivadas concretas: ButtonImplementation y Lamp.

Ejemplo completo de Robert Martin

Ejemplo completo de Robert Martin

• La política de alto nivel de este código es reusable con cualquier clase de botón y, con cualquier clase de dispositivo que necesite controlarse,

• además de no ser afectada por los cambios de los mecanismos de bajo nivel.

• Por tanto es robusto frente a los cambios y reusable

Ejemplo completo de Robert Martin • Se podría presentar una queja legítima acerca de

este diseño/código. El dispositivo controlado por el botón debe derivarse de la clase ButtonClient. ¿Qué ocurre si la clase Lamp pertenece a una librería de terceros y no puede modificarse su código fuente.

• En la siguiente figura se usa una clase “adaptadora” para conectar un objeto Lamp de terceros al modelo.

• La clase LampAdapter simplemente traslada los mensajes TurnOn/TurnOff (virtuales puros) heredados de ButtonClient en cualquier mensaje que la clase Lamp necesite ver

Ejemplo completo de Robert Martin

Implementando el modelo abstracto

• Entonces, ¿Cómo creamos la implementación de la cafetera Mark IV?

• Miremos todos los casos de uso nuevamente ahora desde el punto de vista de la Mark IV

• Veremos cómo aparecen todas estas ideas de diseño a la luz de la realidad inyectada por el código y, tal vez haga que el diseño se modifique

Caso de uso 1: El usuario pulsa el botón Brew

• Mirando nuestro modelo, ¿Cómo sabe UserInterface que se pulsó el botón Brew?

• Claramente, debe llamar a la función del API getBrewButtonStatus().

• ¿Dónde se invocaría esta función? Nosotros decretamos que UserInterface no puede saber nada del API.

• ¿Entonces dónde iría esta invocación? • Aplicaremos el DIP y pondremos esta invocación

dentro de una función miembro de una clase derivada de UserInterface

Caso de uso 1: El usuario pulsa el botón Brew

Caso de uso 1: El usuario pulsa el botón Brew

• Se deriva M4UserInterface de UserInterface y, en M4UserInterface se pone el método checkButton().

• Cuando se invoca a esta función, ella llamará a la función del API GetBrewButtonState().

• Si el botón Brew se presionó, se invoca a la función protected startBrewing(), miembro de UserInterface

Caso de uso 1: El usuario pulsa el botón Brew

class M4UserInterface: public UserInterface{

private:

void checkButton(){

int buttonStatus=GetBrewButtonStatus();

if(buttonStatus==brewButtonPushed)

startBrewing(); //protected

}

};

Caso de uso 1: El usuario pulsa el botón Brew

class UserInterace{ private: // son clases abstractas no puedo instanciar objetos, uso punteros o & HotWaterSource* hws; ContainmentVessel* cv; public: //implementación incompleta void done(); void complete(); protected: void startBrewing(){

if(hws->isReady() && cv->isReady()){ hws->start(); cv->start(); } } };

Caso de uso 1: El usuario pulsa el botón Brew

• Se pueden estar preguntando, ¿porqué se crea método startBrewing() ?

• ¿Porqué no se llama directamente a las funciones start() directamente desde M4UserInterface?

• La razón es simple pero significativa. Las verificaciones isReady() y las consiguientes llamadas a start() de HotWaterSource y ContainmentVessel son políticas de alto nivel que la clase UserInterface posee.

Caso de uso 1: El usuario pulsa el botón Brew

• Este código es válido independientemente de si estamos implementando la MarkIV, por lo tanto, no debería acoplarse a la derivada MarkIVUserInterface.

• El autor aclara que hará esta misma distinción una y otra vez en este ejemplo.

• Él mantiene tanto código como se puede en las clases de alto nivel;

• el único código que pondrá en las clases derivadas es aquél que es directamente e inexorablemente asociado a la MarkIV

Implementando las funciones isReady

• ¿Cómo se implementan los métodos isReady() de HotWaterSource y ContainmentVessel?

• Debería quedar claro que son realmente métodos abstractos.

• Las correspondientes clases derivadas M4HotWaterSource y M4ContainmentVessel los implementarán llamando a las funciones del API apropiadas

Implementando las funciones isReady

Implementando las funciones isReady

class M4HotWaterSource: public HotWaterSource{

public:

bool isReady(){

int boilerStatus=GetBoilerStatus();

return boilerStatus==boilerNotEmpty;

}

};

Implementando las funciones isReady

class M4ContainmentVessel:public ContainmentVessel{

public:

bool isReady(){

int plateStatus=GetWarmerPlateStatus();

return plateStatus==potEmpty;

}

};

Implementando las funciones start

• El método start() de HotWaterSource es un método abstracto que,

• es implementado dentro por su clase derivada M4HotWaterSource para invocar a las funciones del API que cierra la válvula y activa el elemento calefactor del atomizador.

Implementando las funciones start

//public void M4HotWaterSource::start(){ setReliefValveState(valveClosed); setBoilerState(BoilerOn); } • El método start() de ContainmentVessel es mucho

más interesante. • La única acción que M4ContainmentVessel necesita

realizar es, recordar el estado de elaboración (brewing) del sistema (mediante variables de estado).

• Como veremos posteriormente, esto permitirá responder correctamente cuando el recipiente se coloca o quita del plato calentador

Implementando las funciones start class M4ContainmentVessel:public ContainmentVessel{ private: bool isBrewing; public: bool isReady(){ int plateStatus=GetWarmerPlateStatus(); return plateStatus==potEmpty;} void start(){ isBrewing=true; } };

¿Cómo se invocará checkButton?

• ¿Cómo llega el flujo de control a un lugar donde la función del API GetBrewButtonStatus() pueda invocarse?,

• En efecto, ¿Cómo llega el flujo de control donde se puedan detectar los sensores?

• Se adopta el enfoque de usar polling.

• Aquí modificaremos un poco la solución planteada en Java (usan interfaces) y para no usar herencia múltiple en C++

¿Cómo se invocará checkButton?

• Las 3 clases derivadas tienen sensores, por tanto,

• en cada una implementaremos una función poll() que represente la noción de que esta clase de alguna forma debe ganar el control del procesador para detectar algún evento relacionado con estos sensores,

• las cuales serán llamadas repetidamente, de forma tal que, estas clases puedan chequear si tal evento ocurrió

¿Cómo se invocará checkButton?

• Una función main() tomó el lugar de la clase CoffeeMaker (dios)

• La función main() estará en un archivo CoffeeMaker.cpp y simplemente instanciará los 3 componentes de las clases derivadas M4,

• luego invocará funciones init() sobre esos 3 objetos creados, para comunicar cada componente con el otro y,

• finalmente esperará en un lazo infinito invocando al método poll() de cada componente, sucesivamente

¿Cómo se invocará checkButton?

• Luego de lo visto, tenemos claro que checkButton() será sustituida por un método private:

void M4UserInterface::poll(){

int buttonStatus=GetBrewButtonStatus();

if(buttonStatus==brewButtonPushed)

startBrewing();

}

Completando el código

• El mismo razonamiento empleado en las secciones anteriores, puede repetirse para cada uno de los componentes de la cafetera.

UserInterface class UserInterface{ private: //clases abstractas, no puedo instanciar objetos, puedo usar * o & HotWaterSource* hws; ContainmentVessel* cv; protected: bool isComplete; void startBrewing(){ if(hws->isReady() && cv->isReady()){ isComplete=false;// cambia de true a false hws->start(); cv->start(); } } //sigue

UserInterface

public:

UserInterface():hws(0),cv(0){

isComplete=true;

}

void init(HotWaterSource* hws, ContainmentVessel* cv){

this->hws=hws;

this->cv=cv;

}//sigue

UserInterface

void complete(){

isComplete=true;

completeCycle();

}

/*estímulos externos*/

virtual void done()=0;

virtual void completeCycle()=0;

};//fin UserInterface

M4UserInterface /*implementa el modelo de control de la UI de la cafetera MarkIV*/

class M4UserInterface: public UserInterface{

public:

void poll(){

int buttonStatus=GetBrewButtonStatus();

if(buttonStatus==brewButtonPushed)

startBrewing();

}

//sigue

M4UserInterface /*estímulos externos*/

void done(){

SetIndicatorState(indicatorOn);

}

void completeCycle(){

SetIndicatorState(indicatorOff);

}

};//fin clase M4UserInterface

HotWaterSource

class HotWaterSource{ private: //clases abstractas, no puedo instanciar objetos, puedo usar * o & UserInterface* ui; ContainmentVessel* cv; protected: bool isBrewing; //método protected void declareDone(){ ui->done(); cv->done(); isBrewing=false; } //sigue

HotWaterSource public:

HotWaterSource():ui(0), cv(0){

isBrewing=false;

}

void init(UserInterface* ui, ContainmentVessel *cv){

this->ui=ui;

this->cv=cv;

}

void start(){

isBrewing=true;//cambia de false a true

startBrewing();

}//sigue

HotWaterSource

void done(){

isBrewing=false;

}

virtual boolean isReady()=0;

virtual void startBrewing()=0;

virtual void pause()=0;

virtual void resume()=0;

};//fin clase HotWaterSource

M4HotWaterSource

class M4HotWaterSource:public HotWaterSource{

public:

void isReady(){//hay agua en el atomizador

int boilerStatus=GetBoilerStatus();

return boilerStatus==boilerNotEmpty;

}

void startBrewing(){//spray

SetReliefValveState(valveClosed);

SetBoilerState(boilerOn);

}

M4HotWaterSource

void poll(){ int boilerStatus=GetBoilerStatus(); if(isBrewing){ if(boilerStatus==boilerEmpty){//stop spray SetBoilerState(boilerOff); SetReliefValveState(valveClosed); declareDone(); } } }

M4HotWaterSource

void pause(){

SetBoilerState(boilerOff);

SetReliefValveState(valveOpen);

}

void resume(){

SetBoilerState(boilerOn);

SetReliefValveState(valveClosed);

}

};//fin clase M4HotWaterSource

ContainmentVessel class ContainmentVessel{

private:

//clases abstractas, no puedo instanciar objetos, puedo usar * o &

UserInterface* ui;

HotWaterSource* ws;

ContainmentVessel

protected:

bool isBrewing; bool isComplete; //funciones miembro protected void declareComplete(){ isComplete=true; ui->complete(); } void containerAvailable(){ hws->resume(); } void containerUnavailable(){ hws->pause(); }

ContainmentVessel public:

ContainmentVessel():ui(0),hws(0){

isBrewing=false;

isComplete=true;

}

void init(UserInterface* ui, HotWaterSource* hws){

this->ui=ui;

this->hws=hws;

}

void start(){

isBrewing=true;

isComplete=false;

}

void done(){

isBrewing=false;

}

virtual bool isReady()=0;

};//fin clase ContainmentVessel

M4ContainmentVessel class M4ContainmentVessel:public ContainmentVessel{ private: int lastPotStatus; //métodos privados void handleBrewingEvent(int potStatus){ if(potStatus==potNotEmpty) containerAvailable(); SetWarmerState(warmerOn);} else if(potStatus==warmerEmpty){//retiraron el recipiente containerUnavailable(); SetWarmerState(warmerOff);} else{//potStatus==potEmpty, recipiente vacío, sin café containerAvailable(); SetWarmerState(warmerOff);} } } //sigue private

M4ContainmentVessel

//sigue private: void handleIncompleteEvent(int potStatus){ if(potStatus==potNotEmpty){ SetWarmerState(warmerOn);} else if(potStatus==warmerEmpty){ SetWarmerState(warmerOff);} else{//potStatus==potEmpty, brewingComplete setWarmerState(warmerOff); declareComplete(); } }

M4ContainmentVessel

public: M4ContainmentVessel(){ lastPotStatus=potEmpty;} bool isReady(){ int plateStatus=GetWarmerPlateStatus(); return plateStatus==potEmpty; }

M4ContainmentVessel

void poll(){ int potStatus=GetWarmerPlateStatus(); if(potStatus!=lastPotStatus){ if(isBrewing){ handleBrewingEvent(potStatus);} else if(isComplete==false){ handleIncompleteEvent(potStatus);} lastPotStatus=potStatus; } } }//fin clase M4ContainmentVessel

CoffeMaker.cpp int main(){ /*punteros a las clases base para que funcione el polimorfismo, con las funciones virtuales*/ UserInterface* uiA=new M4UserInterface(); HotWaterSource* hwsA=new M4HotWaterSource(); ContainmentVessel* cvA=new M4ContainmentVessel(); /*para poder invocar a poll(), no es miembro de las clases base*/ M4UserInterface* ui=dynamic_cast<M4UserInterface*> uiA; M4HotWaterSource* hws=dynamic_cast<M4HotWaterSource*>hwsA; M4ContainmentVessel* cv=dynamic_cast<M4ContainmentVessel*>cvA; ui->init(hws,cv); hws->init(ui,cv); cv->init(ui, hws); while(true){ ui->poll(); hws->poll(); cv->poll(); } }//fin main

Beneficios de este diseño

• A pesar la naturaleza trivial de este problema, este diseño muestra características muy buenas.

• Hay 3 clases que mantienen las políticas de alto nivel de la cafetera, estas abstracciones están completamente aisladas de los detalles.

• Son clases abstractas que no saben nada de botones, luces, válvulas, sensores ni ningún otro elemento detallado de la cafetera.

• En la misma línea, las clases derivadas son dominadas por estos detalles

Beneficios de este diseño

Conclusiones

• Los buenos diseños OO comprenden buenos modelos dinámicos y buenos modelos estáticos

• Los buenos modelos dinámicos describen los comportamientos demandados por los requerimientos de las especificaciones

• Los buenos modelos estáticos apoyan esas dinámicas mientras que permiten separar el diseño en componentes reusables

Beneficios de este diseño

• Estas 3 clases abstractas podrían reutilizarse para muchas clases de máquinas de café.

• Fácilmente podríamos usarla en una cafetera conectada a la red de agua y que, use un tanque y una espita.

• Parece probable que, también podamos usarlo para una máquina expendedora de café, en una elaborada de té automática o aún en una máquina de sopas

top related