Ir al contenido principal

Programación asíncrona con async/await en .Net

Introducción A partir de la versión 4.5 de .Net framework se ha simplificado de forma considerable la forma en que podemos trabajar con código asíncrono. Con los anteriores frameworks, si queríamos contar con los beneficios de una programación asíncrona nos veíamos obligados a lidiar con una gran complejidad en nuestro código. Esto nos hacía evitar su uso en lo posible a pesar de perder esta importante característica. Stephen Cleary , un MVP especializado en concurrencia define la programación asíncrona como: Una forma de concurrencia que utiliza futuros o "callbacks" para evitar hilos innecesarios. La programación moderna con async y await nos abstrae de la utilización de "callbacks" y nos permite utilizar futuros (Tasks) que se encargarán de notificar al llamante cuando el método asíncrono se complete. async y await Las palabras async y await son las palabras clave que .Net ha introducido en el lenguaje para que podamos implementar métodos asíncronos co...

Composición o herencia: Ser o tener

Introducción

El diseño de una solución software basado en objetos requiere que los objetos se relacionen entre sí. Cuando una clase necesita utilizar el código de otra clase necesitamos relacionar estas dos clases. Esta relación la podemos establecer de varias formas y dos de las más importantes en POO son:
  • heredando de una clase base: el objeto adquiere todas las propiedades y comportamientos del objeto base;
  • por composición: conteniendo instancias de otros objetos que implementen esa funcionalidad.
Un principio de diseño en POO dice que ante la posibilidad de resolver un problema con cualquiera de estas dos soluciones deberíamos "favorecer" la composición sobre la herencia. Hay que remarcar que la palabra "favorecer" sólo significa eso: "favorecer". No se trata de una imposición. Más bien es una recomendación cuando ambas posibilidades son aplicables. Existen muchos escenarios en los que nos encontraremos en que las dos soluciones son fácilmente reemplazables entre si y deberemos decidir cual de las dos opciones es más adecuada para cada caso.

Este principio también lo podemos encontrar de forma un poco más extendida: favorecer la composición o las interfaces sobre la herencia. Si es necesario el polimorfismo podemos plantearnos el utilizar interfaces antes que la herencia de clases.

Herencia

La herencia permite centralizar ciertas funcionalidades (reutilización de código) y promueve el polimorfismo (interfaces). Ambas cualidades son dos pilares de la POO pero mal aplicadas pueden resultar en un mal diseño.

El punto donde flaquea la herencia es la rigidez que se produce en la relación entre las dos clases. En el momento en que una clase hereda de otra clase se produce un enlace estático entre la clase y la superclase.

Abuso de la herencia

Cuando aplicamos la herencia con el único propósito de obtener funcionalidad de la base puede ser una señal de que estamos abusando de su uso. Obtener solamente una parte de la funcionalidad de la base nos lleva a romper el principio de sustitución de Liskov, el cual dice que cualquier objeto de una clase debe poder ser sustituido por un objeto de su base.

Para aplicar la herencia debemos tener una relación real entre las clases de tipo ES UN. Un ejemplo típico sería la relación entre un Vehículo representando una abstracción y un Coche o una Moto como clases concretas. De esta forma las subclases se pueden convertir en una especialización de la abstracción.

Si implementamos funcionalidades en la abstracción que sólo tienen sentido en algunas de las implementaciones estaríamos "engordando" la interfaz base con un comportamiento extra para algunas de sus extensiones. Siguiendo con el ejemplo anterior, la implementación de un método en Vehículo que fuera BajarVentanilla tendría sentido en un Coche, Camión, Furgoneta, pero no tendría ningún sentido en una Moto o en una Bicicleta.

Mejor con interfaces...

Para estos casos en los que la herencia de clases no nos sirve pero a su vez queremos utilizar nuestras clases de forma polimórfica, deberíamos utilizar interfaces que implementen funcionalidades específicas de subgrupos. De esta manera, siguiendo el ejemplo de nuestro Vehículo, podríamos tener una interfaz IVentanilla que nos permitiese aplicarla sólo a los vehículos con ventanillas, y una interfaz IIntermitentes sólo a los vehículos con intermitentes. Las interfaces nos permiten una herencia múltiple a través de sus contratos sin implementación de código:



Estos contratos pueden ser incluso implementados por clases totalmente dispares. Tenemos el ejemplo de las interfaces IComparable, IEnumerable, IDisposable, etc.
Estas interfaces permiten a los objetos que las implementan comportarse como tales objetos: polimorfismo. Así, un objeto en el que implementemos la interfaz IComparer nos compromete a tener un método Comparer el cual podrá ser invocado por cualquiera que requiera una comparación entre dos de nuestros objetos, como es el caso del método Sort de la clase ArrayList, el cual aprovecha nuestra cualidad de comparar para realizar su tarea de ordenación. Independientemente de la jerarquía de herencia a la que pertenezca nuestro objeto una cosa es segura, nuestro objeto es comparable, por lo que va a tener este comportamiento disponible para quien así lo requiera.
Nota: las interfaces deben ser simples (interface-segregation principle: ISP). Las clases que las implementan no deberían depender de métodos que no van a usar. Las interfaces grandes deben dividirse en otras más pequeñas y específicas.

Composición

El propósito de la composición es bastante simple de entender: hacer un "todo" formado por sus componentes. Un objeto Coche puede estar compuesto a su vez por un objeto Ruedas, Chasis, Motor, etc. La composición mantiene una relación de pertenencia de tipo TIENE UN.
El punto fuerte de la composición es la flexibilidad que existe en la relación. Podemos usar una abstracción de una clase como parte de otra clase y proporcionar acceso mediante cualquier método de inyección de dependencias. De esta forma podríamos introducir y/o modificar en tiempo de ejecución un objeto concreto y por lo tanto cambiar su comportamiento al vuelo.

En el ejemplo anterior inyectamos una abstracción en el momento en que instanciamos la clase. De esta manera estamos decidiendo en tiempo de ejecución cual será la implementación que usará nuestra clase.

Esta característica es precisamente una consecuencia de haber utilizado la herencia para conseguir depender de una abstracción. Por lo tanto, se puede decir que en la composición se suelen utilizar objetos que a su vez usan la herencia. De hecho, la inyección de dependencias suele emplearse con abstracciones (interfaces o clases abstractas) en lugar de implementaciones concretas. Esta es una forma habitual de composición en sistemas desaónados.

Como contrapartida en la composición, cuando necesitamos exponer funcionalidad de alguno de nuestros componentes nos vemos forzados a implementar esta funcionalidad en el contenedor aunque sólo sea para delegar de forma directa la responsabilidad hacia estos componentes. Esta característica en la herencia se implementa de forma automática ya que la subclase adquiere la interfaz (API) que tiene la superclase.

Aunque en cierto sentido esta contrapartida se podría interpretar también como una ventaja. Puede servir como un arma de encapsulamiento muy potente donde tenemos la facultad de ocultar y/o modificar el modo de acceso a nuestros componentes. Hay patrones de diseño como el Adapter, Decorator o el Wrapper que hacen un buen uso de esta característica.

En el ejemplo estamos envolviendo un objeto Coche en nuestra clase CocheWrapper. El objeto envuelto es inaccesible desde el exterior. Sólo puede acceder a nuestro método público que amplía la funcionalidad del objeto Coche original. Esta tipo de diseño es muy utilizado para desacoplar el cliente que utiliza el Wrapper del objeto envuelto.

Ley de Demeter

Un error en el que podemos caer al utilizar la composición es en las cadenas de llamadas a métodos. Esto ocurre cuando violamos la Ley de Demeter. Para cumplir esta ley debemos tener en cuenta la encapsulación y la cohesión, es decir, restringir el acceso a los miembros de nuestra clase y evitar por otro lado que una clase dependa de clases que no deberían tener una relación directa con la nuestra.
En este ejemplo podemos comprobar que nuestro ejemplo no sólo depende del objeto A, sino que también depende del B, del C e incluso se ha comprometido a usar el método doSomething del objeto C. Esto nos hace vulnerables a cualquier cambio que se produzca en cualquier punto de la cadena. Existe un acoplamiento excesivo entre nuestras clases. Como solución podemos utilizar la técnica del Wrapper vista en el punto anterior.
Otra ventaja de utilizar la composición o las interfaces es la facilidad que da a la hora de refactorizar el código. En la composición no tenemos esa restricción que nos ata de forma directa a una jerarquía donde cualquier cambio aguas arriba afecta a las subclases aguas abajo. En el caso de usar interfaces  lo único que nos ata es nuestro contrato a cada interfaz implementada. Sólo necesitaríamos realizar cambios cuando ese contrato se rompa. La implementación de la interfaz recae en cada clase concreta que suscriba ese contrato.

La composición y la herencia en los patrones de diseño

En el libro Design Patterns [GoF] sus autores hac5n una clasificación de patrones de diseño atendiendo al criterio "alcance" (scope) precisamente haciendo esta distinción:
  • Patrones de clases: usan relaciones entre clases y sus subclases. Se establecen a través de la herencia, así que son fijadas de forma estática en el momento de la compilación;
  • Patrones de objetos: usan relaciones entre objetos mediante la composición. De esta forma se pueden cambiar los objetos de forma dinámica en ejecución.
Hay que resaltar que incluso los patrones de objetos se aprovechan del polimorfismo en sus composiciones. Y sin herencia no hay polimorfismo. Así que se podría decir que los patrones de clases son los que únicamente utilizan las relaciones entre clases (herencia). La mayor parte de ellos son patrones de objetos.

Patrón Estrategia

Un ejemplo es el patrón Estrategia utilizado para cambiar el comportamiento de forma dinámica. Al utilizar la composición para mantener un objeto que encapsula el comportamiento nos facilita la posibilidad de "inyectar" comportamientos distintos en tiempo de ejecución. Si utilizáramos la herencia, este comportamiento sería heredado de la base. Si quisiéramos modificarlo tendríamos que sobrescribir este comportamiento quedando de nuevo estático.

Patrón Decorador

Otro ejemplo es el patrón Decorador en el que envolvemos el objeto a decorar mediante la composición. Antes de delegar la responsabilidad al objeto original podemos "decorar" el comportamiento realizando un preprocesado o postprocesado o ambos. De esta forma estamos añadiendo funcionalidad al objeto decorado sin necesidad de extenderlo.

Comentarios

Publicar un comentario

Entradas populares de este blog

La importancia del encapsulamiento. Parte 1. Introducción.

Introducción En programación orientada a objetos el término encapsulamiento es utilizado indistintamente para describir dos conceptos diferentes pero a la vez relacionados entre sí: Como mecanismo de restricción del acceso a componentes de un objeto; Como construcción del lenguaje para facilitar el “empaquetado” del estado y el comportamiento. En este post el significado al que se hace referencia es al primero de ellos. La visibilidad o accesibilidad es el primer paradigma que nos encontramos al empezar a escribir nuestro código. Cuando comenzamos a escribir una clase, interfaz, enumerado o estructura, lo primero que hacemos es establecer su modificador de acceso, y si no lo hacemos, se establecerá el modificador por defecto. Lo mismo ocurre cuando empezamos a escribir cada uno de sus miembros: variables de estado (campos) y métodos: lo primero de todo es escribir su modificador de acceso. A menudo me encuentro con código sin tener en cuenta este aspecto: código demasiado “ab...

La importancia del encapsulamiento. Parte 2. Un ejemplo de encapsulamiento en la vida real.

Un ejemplo de encapsulamiento en la vida real Supongamos un taller de carpintería que fabrica muebles a medida. Por una parte tendríamos el propio taller, que sería la clase a encapsular. Por otra los clientes, que serían nuestros usuarios y representarían las clases que USAN la clase taller. Y por otra parte podríamos pensar en talleres filiales que representarían una especialización del taller matriz, es decir, clases que extienden al taller de carpintería y por lo tanto SON talleres. El taller cuenta con funcionalidades puestas al servicio de los clientes como la elaboración de presupuestos, consulta de catálogos, selección de tipo de madera, selección de color, etc. También cuenta con funcionalidades propias del taller como cortar, lijar, pintar, barnizar, ensamblar mueble, etc. Para la elaboración de los presupuestos el taller matriz cuenta con una tarifa de precios que sólo los talleres pueden acceder para poder elaborar los presupuestos. Esta tarifa la fija el taller mat...

Programación asíncrona con async/await en .Net

Introducción A partir de la versión 4.5 de .Net framework se ha simplificado de forma considerable la forma en que podemos trabajar con código asíncrono. Con los anteriores frameworks, si queríamos contar con los beneficios de una programación asíncrona nos veíamos obligados a lidiar con una gran complejidad en nuestro código. Esto nos hacía evitar su uso en lo posible a pesar de perder esta importante característica. Stephen Cleary , un MVP especializado en concurrencia define la programación asíncrona como: Una forma de concurrencia que utiliza futuros o "callbacks" para evitar hilos innecesarios. La programación moderna con async y await nos abstrae de la utilización de "callbacks" y nos permite utilizar futuros (Tasks) que se encargarán de notificar al llamante cuando el método asíncrono se complete. async y await Las palabras async y await son las palabras clave que .Net ha introducido en el lenguaje para que podamos implementar métodos asíncronos co...