Encapsulación Usada En Programación Como Patrón De Diseño

encapsulation

Antes de nada vamos hablar sobre la definición de la encapsulación aplicada a la programación, si echamos un vistazo en algún medio conocido por todos como Wikipedia dice lo siguiente:

En programación modular, y más específicamente en programación orientada a objetos, se denomina Encapsulamiento al ocultamiento del estado, es decir, de los datos miembro de un objeto de manera que sólo se pueda cambiar mediante las operaciones definidas para ese objeto.

Cada objeto está aislado del exterior, es un módulo natural, y la aplicación entera se reduce a un agregado o rompecabezas de objetos. El aislamiento protege a los datos asociados de un objeto contra su modificación por quien no tenga derecho a acceder a ellos, eliminando efectos secundarios e interacciones.

De esta forma el usuario de la clase puede obviar la implementación de los métodos y propiedades para concentrarse sólo en cómo usarlos. Por otro lado se evita que el usuario pueda cambiar su estado de maneras imprevistas e incontroladas.

Por tanto, después de esta definición entendemos que deberíamos aplicar la encapsulación para componentes reusables pudiéndolo llamar Framework, Toolkit… generalmente se usa para proyectos cerrados que tienen una serie de objetivos concretos, por ejemplo, todos los Nuget Packages que podemos encontrarnos en el repositorio oficial aplican este principio y también en cualquier desarrollo móvil donde conectamos normalmente con una API que necesitamos sea segura y fácil de trabajar con ella.

Ejemplo de código sin encapsulación

A continuación, vamos a mostrar un claro ejemplo donde no se ha aplicado la encapsulación, con esta base transformaremos un código inicial mal codificado en una clase encapsulada:

encapsulation-1

Al ver este ejemplo claramente vemos que los métodos que se incluyen en una clase no son entendibles por sí solos, no sabemos por qué devuelven datos de un tipo String o por qué se ha creado un método o una propiedad. Por lo tanto, este es el motivo del por qué de la encapsulación, hay que programar para principiantes, es decir, especificar cada elemento para alguien que no tiene ningún tipo de información sobre las especificaciones de tu implementación.

¿Cómo aplicariamos la encapsulación y para qué?

“Long term productivity” es un concepto que popularmente se conoce como el tiempo que gastamos a la hora de programar, y dentro de este intervalo de tiempo se sabe que gastamos hasta 10-20 veces más leyendo código que escribiéndolo. Esto resulta bastante impactante si se lo presentamos así a nuestro jefe de proyecto, pero de esta forma seguro que conseguiremos que recapacite y estime un sobrecoste por cada funcionalidad / requerimiento que nos pida el cliente con mótivo del encapsulamiento en la programación.

En conclusión, volvemos al punto anterior de que el código que implementemos debe ser entendido por cualquier programador, independientemente del nivel que tenga, por tanto podemos concluir con que

El código debe ser entendido por sí solo, sin necesidad de realizar un soporte para documentación especialmente extenso.

Definiciones Aplicadas a la Programación Orientada a Objetos (OO)

Punto de vista clásico

Dentro de esta definición podemos identificar 2 elementos imprescindibles de abordar, por un lado tenemos:

  • Information hiding: Nos referimos con este término a la información o implementación del código que mostramos al cliente con el fin de aclarar dudas sobre su uso parámetros a pasar a esa caja negra llama “Encapsulamiento”. Es decir, qué propiedades u objetos son públicos los cuáles están a la vista de todo el mundo, y sobre todo cuáles de ellos no incumben al cliente tratándolos como privados.
  • Protection of invariants: Consiste básicamente en comprobar las pre-condiciones a la hora de realizar una llamada a la API (podemos tomarlo como la implementación encapsulada) que garantizan un buen resultado cumpliendo por lo tanto las directrices de las post-condiciones. Tanto las pre como las post-condiciones (conocidas como “assertions” según Bertrand Meyer) son reglas que identifican los estados válidos e inválidos del objeto facilitado, cuya finalidad es la de garantizar que los estados inválidos son imposibles de darse a cabo.

Punto de vista alternativo

Aquí nos encontramos con un par de principios en OO más concretos y accionables, que hacen de la encapsulación algo mucho más tangible desde el punto de vista del programador:

Command Query Separation (CQS – Bertrand)

Este principio según Bertrand, dice que las operaciones identificadas en nuestra clase deberían ser comandos (Commands) o consultas (Queries), pero nunca ambas. Para diferenciar ambos conceptos vamos a entrar un poco en detalle mostrando un ejemplo muy claro y conciso:

  • Commands: Para identificar qué métodos son de este tipo debemos fijarnos en un par de cosas, por un lado nunca devuelven ningún tipo de dato (void) y por otro siempre causan efectos colaterales en su ejecución, es decir, implícitamente se realizan otras acciones que afectan al entorno del propio objeto que alberga el método llamado.
  • Queries: En cambio, en este caso sucede exactamente lo contrario, por un lado siempre devolvemos datos ya que estamos pidiendo a través de la consulta ciertos resultados y por otro existe una base que es la siguiente: “Da igual el número de veces que ejecutes una query, siempre deberá devolverte resultados idénticos válidos” lo cual podemos definir con los términos “Idempotence” y “Safe to invoke”.

Como podemos ver a continuación, hemos refactorizado parte del código mostrado anteriormente, convirtiendo en Commands y Queries cada método, lo cuál hace que sea más fácil de entender para un programador las pre-condiciones y los resultados esperados en cada caso:

encapsulation-2

 

Por supuesto, algo muy importante que tratar es la comunicación con el equipo de desarrollo, siempre debemos llegar a un acuerdo sobre (en el caso de refactorizaciones) qué módulos / funcionalidades queremos migrar a CQS, y en caso afirmativo necesitamos acordar de qué forma exactamente vamos a trabajar conjuntamente.

Postel’s Law (Principio de robustez)

Con la siguiente frase podemos resumir en consiste este principio:

“Sé muy conservador en lo que tú mandas, pero deberías ser muy flexible en lo que aceptas o consideras cómo válido”

Es decir, los inputs que aceptas deben estar muy restringidos pero los datos que recibes deben ser lo más abiertos posible. Si lo vemos en términos de desarrollo, nosotros cuando acordamos algo con el cliente debemos ceñirnos a esto (WCF contract / firma del método) para obtener los datos que esperamos.

Input (pre-condiciones)

Aquí hablamos de la posibilidad de hacer fallar al sistema si los inputs tienen un tipo / contenido erróneo que permite la compilación del mismo pero un error inevitable en la ejecución del sistema. Con lo cuál se habla de pre-condiciones para que no se genere un estado inválido de la instancia, algunos ejemplos pueden ser:

  1. Evitar un incorrecto estado de inicialización de clases
  2. Obligar a rellenar esos datos requeridos en el constructor con parámetros válidos
  3. Colocar el SET de las propiedades como “private” para evitar rellenarlas directamente, y así no poder evitar la instancia de la propia clase (constructor)
  4. Creación de trazas de error (throw) para detectar excepciones y evitar procesar datos que generen estados inválidos.

Todavía tenemos un problema a este nivel, son los tipos de dato Nullables lo cuál provoca excepciones del tipo ArgumentNullException.

Para ello existe el principio de Fail Fast,  que es la forma de validar pre-condiciones para que el parámetro se considere válido, es decir,  cuando tenemos que verificar otras posibles excepciones como que el dato que me pasas es correcto en cuanto a tipología pero su contenido es incorrecto, lo cuál genera excepción y por ello debemos controlarla con ArgumentException pasando un mensaje lo suficientemente explicativo para otros desarrolladores explicando lo que sucede, así se podrá corregir ese error. Tal y como vemos a continuación con la propiedad SourcePath:

encapsulation-3

Output (post-condiciones)

El diseño de nuestras queries… porque CUANDO HAY OUTPUT es query… debe ser siempre 100% fiable sobre el tipo que devuelven si siempre será ese, es decir, un caso práctico es que nunca sería viable utilizar NULL como dato devuelto válido ya que no es el tipo que dijimos y podemos hacer que no seamos fiables para el cliente que consume nuestra librería. Por lo tanto, existen 3 técnicas eficientes para resolver este problema:

  • Tester/Doer: Consiste en crear un método query que devuelva un valor booleano y así asegurarnos de que los valores serán consistentes y nunca tendremos NULL como resultado, el problema que tenemos aquí es que cuando están actuando diferentes threads a la vez puede que ese resultado ya quede obsoleto y no sea válido.
encapsulation-4
Creamos un método que verifique si nuestro input es válido
encapsulation-5
Llamamos a este método antes de realizar la operación de lectura
  •  TryRead: Aquí hacemos algo parecido, donde en lugar de devolver el tipo tal cual de la query, lo convertimos en una query que devuelva booleano y así hacer la función atómica para evitar el problema explicado anteriormente, devolviendo el resultado de la query real como un parámetro OUT. Aquí el problema que tenemos es que nos salimos de la convención a la hora de implementar queries, devolver resultados a través de parámetros de entrada hace que sigamos el “interface fluent”… lo cual no es recomendable para un diseño OO de una API.

 

encapsulation-6
Creamos un método con parámetro de entrada/salida y devolvemos booleano

 

encapsulation-7
Aquí devolvemos siempre un valor válido en message cuando exists es True
  • Maybe (la mejor estrategia para saber si el output es legal o no – EVITAR NULL como return value): Es la creación de una clase llamada Maybe donde hereda de IEnumerable<string> para asegurar que nuestro resultado tendrá 0 o 1 elementos, en función de sí contiene o no un resultado válido. Esta función es muy de F# programación funcional.
Creamos la clase Maybe que hereda de IEnumerable
Creamos la clase Maybe que hereda de IEnumerable
Utilizamos dicha clase para utilizarlo como dato de salida (0-1 elementos)
Utilizamos dicha clase para utilizarlo como dato de salida (0-1 elementos)

CONCLUSIÓN

La encapsulación es una forma de programar que hace la vida de los desarrolladores más fácil, no solo la de los demás sino la tuya propia, ya que en un futuro cuando tengas que volver a esa funcionalidad encapsulada podrás ver exactamente que es lo que implementa. Mantenibilidad y Productividad son conceptos que deben tenerse en cuenta en cualquier desarrollo de SW (Fácil compresión a la hora de leer si escribes para desarrolladores principiantes).

Como apunte, decir que es muy importante no llevar la encapsulación hasta su punto más extremo, basicamente porque los tiempos de desarrollo se irían totalmente en la planificación del proyecto.

 

Comments

comments

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *