SOLID y Visitor: Un ejemplo explicado (2da Parte)

En un artículo anterior comenzamos una diseñar un componente que permitía recorrer directorios recursivamente y procesar los archivos que fuese encontrando en su recorrido. Todo esto se relacionó con los principios SOLID y el patrón de diseño visitante. En este artículo se terminará de diseñar el componente.

Cuarta Aproximación

Un siguiente paso sería desacoplar (sacar) las reglas que verifican si se puede entrar en un subdirectorio y si se puede procesar un archivo de FileScanner, una mala solución (ya veremos la razón) sería esta:

SOLID_visitor_4_th(click para agrandar)

Como se puede apreciar en el diagrama, se agregaron dos métodos a la interfaz Visitor. Ambos métodos retornan un booleano y responden a las preguntas de si un subdirectorio se debe recorrer (isTraversable) y si un archivo se debe procesar (isProcessable).

Si se analizan las responsabilidades de estas clases tenemos:

Responsabilidades de FileScanner:

  • Recorrer recursivamente los directorios / subdirectorios a partir del directorio raíz pasado en el método scan, invocando al visitante (Visitor) para delegarle la responsabilidad de decidir:
    1. Qué directorios / subdirectorios se deben recorrer.
    2. Qué archivos se deben procesar.
    3. Procesar los archivos que correspondan.

Responsabilidades de Visitor:

  • Servir de interfaz “abstracta/genérica” para que FileScanner pueda:
    1. Saber si un directorio se debe recorrer recursivamente o no.
    2. Saber si un archivo se debe procesar o no.
    3. Invocar al algoritmo de procesamiento de archivos.

Responsabilidades de DoSomethingVisitor:

  • Implementar Visitor y satisfacer sus tres responsabilidades definidas.

Si bien FileScanner tiene una sola responsabilidad, Visitor y DoSomethingVisitor parecieran tener demasiadas responsabilidades y más adelante hablaremos del impacto que esto tiene.

En cuanto a la reusabilidad, FileScanner es completamente reusable en otros contextos en los que se necesite recorrer un directorio y sus subdirectorios para procesar de cierta forma los archivos contenidos en éstos. Las reglas que filtran directorios y archivos y el algoritmo de procesamiento se implementan como realizaciones de la interfaz Visitor, de modo que, si a FileScanner se le brinda una implementación adecuada de Visitor puede reusarse en cualquier contexto.

La reusabilidad de las implementaciones de Visitor es más dudosa, debido principalmente a que mezcla tres responsabilidades que pareciera no tienen mucho que ver entre si, es decir, determinar si un subdirectorio debe recorrerse, si un archivo debe procesarse y realizar el procesamiento del archivo son cosas que no están necesariamente relacionadas entre si, y que sin embargo con esta estructura se están implementando en la misma clase. Cuando se tiene un componente con estas características, es decir, muchas responsabilidades no directamente relacionadas entre sí, se dice que el componente tiene baja cohesión.

Por ejemplo, se pueden tener las siguientes implementaciones de Visitor para distintos contextos:

  • MyVisitorA: (1a) Excluye los directorios ocultos (isTraversable), (1b) excluye cualquier archivo que NO TENGA extensión .java, es decir, procesa los .java (isProcessable) y (1c) elimina el archivo a procesar (visit).
  • MyVisitorB: (2a) Excluye los directorios “CVS” “.svn” y “.hg” (isTraversable), (2b) excluye cualquier archivo que SI TENGA extensión .java, es decir, procesa todo menos los .java (isProcessable) y (2c) saca una copia de respaldo del archivo a procesar (visit).

La funcionalidad de las clases anteriores es totalmente disyunta, es decir, no hay comportamientos comunes entre las dos clases. Ahora bien, que sucedería si se quiere una tercera implementación que haga lo siguiente:

  • MyVisitorC: (*1a*) Excluye los directorios ocultos (isTraversable), (*2b*) excluye cualquier archivo que SI TENGA extensión .java, es decir, procesa todo menos los .java (isProcessable) y (3c) renombra el archivo a procesar (visit).

Dado que esto puede resultar algo confuso, la siguiente figura muestra la relación existente entre las reglas implementadas por MyVisitorA, MyVisitorB y MyVisitorC.

SOLID_visitor_5

MyVisitorC usa la regla de filtrado de directorios (*1a*), es decir la implementada en MyVisitorA, la regla de selección de archivos a procesar (*2b*), es decir la implementada en MyVisitorB, y luego implementa una nueva regla de procesado de archivos (3c).

Ahora bien, tal y como está definido Visitor y sus implementaciones, es muy difícil implementar MyVisitorC y reutilizar las implementaciones ya existentes de (*1a*) y (*2b*) en MyVisitorA y MyVisitorB, porque al estar implementadas en las mismas clases que (1b) y (1c) por un lado y que (2a) y (2c) por el otro, no se pueden tomar por separado sin tener que arrastrar las implementaciones de las otras reglas que no se necesitan.

En el ejemplo del virus y el anti-virus, se puede hacer el siguiente análisis: tanto el visitante del virus como el del antivirus procesarán cualquier directorio (isTraversable), procesarán archivos .exe, .com y .dll (isProcessable), pero uno infectará con virus los archivos procesados y el otro removerá los virus (visit). Si imaginamos una empresa que se dedique a implementar anti-virus, pero que maliciosamente también implemente virus, entonces la empresa se encontrará con las siguientes clases:

SOLID_visitor_7

El problema es que la clase VirusVisitor y AntiVirusVisitor comparten la misma implementación de los métodos isTraversable e isProcessable (en azul), pero tienen implementaciones completamente distintas de los métodos visit (en rojo), donde una infecta un archivo y la otra elimina la infección. Con esta estructura no hay una manera sencilla y directa de compartir la implementación común entre isTraversable e isProcessable más que el copy / paste o la delegacion de estos métodos a otras clases y su referenciación (lo que igualmente se considera duplicación de código) desde los métodos de las implementaciones de Visitor.

Más adelante se volverá a este punto y se mostrará una forma de resolverlo.

En cuanto a la facilidad de pruebas, parece que no tenemos mayores problemas. FileScanner depende de interfaces y recibe sus dependencias en el constructor, de modo que podemos inyectarle un Mock Object de Visitor y probarla independientemente de cualquier implementación particular de esta interfaz.

Las implementaciones concretas de Visitor, por ejemplo DoSomethingVisitor, también se pueden probar de forma independiente de FileScanner, y en este caso, pareciera que el exceso de responsabilidades de la clase y la falta de cohesión entre éstas no es un inconveniente a la hora de implementar pruebas.

El problema de reusabilidad es más grave, siendo su causa el exceso de responsabilidades y la falta de cohesión entre éstas en la interfaz Visitor y sus implementaciones. Cuando un grupo de responsabilidades que no tienen relación entre sí se atan a una sola interfaz y/o se implementan en una sola clase, el resultado es que no es posible reutilizar una sola de las responsabilidades en otro contexto, al menos no sin tener que arrastrar las otras responsabilidades que no se desean reutilizar.

En realidad, para “reutilizar” (si, entre comillas) esas responsabilidades, tendríamos que hacer un desvergonzado copy / paste (¿ahora entienden las comillas?) o hacer cualquier otra maroma, como enviar cada responsabilidad a una tercera clase e invocarlas o referenciarlas de las clases donde se van a reutilizar. El resultado sería código duplicado o excesivamente complejo, cosas nada deseables y que son “smells” que indican que algo anda muy mal en el código.

Quinta Aproximación

Una solución elegante es apegarse al ISP (Interface Segregation Principle) y dividir la interfaz Visitor en tres interfaces, una para cada responsabilidad actualmente existente en Visitor. Haciendo esto, el resultado es:

SOLID_visitor_6_th(click para agrandar)

Luego de hacer el último cambio, tenemos a FileScanner que depende de Visitor y de dos interfaces adicionales: Traversable y Processable. Visitor sigue cumpliendo la función que tenía la primera vez que la definimos, mientras que Traversable decide ahora si un subdirectorio debe recorrerse recursivamente o no, y Processable decide si un archivo debe procesarse o no. De esta forma, cada una de las tres responsabilidades asociadas anteriormente a la interfaz Visitor ha terminado en su propia interfaz, y la implementación anterior de DoSomethingVisitor que antes satisfacía estas tres responsabilidades ha quedado dividida en tres clases separadas: DoSomethingVisitor, que cumple la única responsabilidad que quedó asociada a Visitor, MyTraversable, que implementa la lógica particular a un contexto para decidir si se debe o no recorrer un directorio / subdirectorio y MyProcessable, que implementa la lógica para decidir si se debe procesar o no un archivo.

Las responsabilidades quedan de esta forma:

Responsabilidades de FileScanner:

  • Recorrer recursivamente los directorios / subdirectorios a partir del directorio raíz pasado en el método scan, invocando a la interfaz Traversable para (a) delegarle la responsabilidad de decidir que directorios / subdirectorios se deben recorrer, a la interfaz Processable para (b) delegarle la responsabilidad de decidir que archivos se deben procesar y a la interfaz Visitor para (c) procesar los archivos que correspondan. También es posible que FileScanner provea un comportamiento por defecto en caso de que Traversable y/o Processable sean nulos, este comportamiento podría ser recorrer y procesar todo.

Es importante mencionar que aunque FileScanner pareciera tener demasiadas responsabilidades, en el fondo todo el trabajo lo hacen Traversable, Processable y Visitor.

Responsabilidades de Traversable:

  • Servir de interfaz “abstracta/genérica” para que FileScanner pueda saber si un directorio se debe recorrer recursivamente o no.

Responsabilidades de Processable:

  • Servir de interfaz “abstracta/genérica” para que FileScanner pueda saber si un archivo se debe procesar o no.

Responsabilidades de Visitor:

  • Servir de interfaz “abstracta/genérica” para que FileScanner pueda invocar al algoritmo de procesamiento de turno.

Responsabilidades de MyTraversable:

  • Implementar Traversable y satisfacer la única responsabilidades definidas para Traversable.

Responsabilidades de MyProcessable:

  • Implementar Processable y satisfacer la única responsabilidades definidas para Processable.

Responsabilidades de DoSomethingVisitor:

  • Implementar Visitor y satisfacer la única responsabilidades definidas para Visitor.

Como se puede ver, cada clase / interfaz tiene asociada una única responsabilidad, lo que implica desde el punto de vista de la reutilización que las distintas implementaciones de las interfaces se podrían utilizar de forma independiente de las demás implementaciónes.

En cuanto a la facilidad de prueba no se han hecho cambios que hagan más difíciles las pruebas. FileScanner sigue dependiendo de interfaces y las dependencias se le inyectan en el constructor y por medio de setters, de modo que es posible crear un FileScanner con Mock Objects y probarlo de forma separada de cualquier implementación de Traversable, Processable y Visitor. Por otro lado, cualquier implementación de estas tres últimas interfaces se puede probar de forma independiente una de la otra, y de forma independiente de FileScanner.

¿Y luego qué?

Para terminar, les dejo un reto. En este punto, nuestra implementación está fuertemente acoplada a la clase File de Java. Esto no es en todos los casos malo, suponiendo que nuestro FileScanner debe recorrer únicamente directorios y archivos, pero sería bueno analizar el impacto en cuanto a reusabilidad y facilidad de pruebas. ¿Cómo se puede desacoplar File la clase FileScanner y de todas las demás clases e interfaces asociadas? ¿Cómo se rompe la dependencia entre FileScanner y File? ¿Qué pasaría si se quiere utilizar FileScanner para recorrer cualquier estructura arbórea? Evidentemente ya dejaría de ser un “FileScanner” y habría que evaluar si realmente es práctico llegar a esos niveles de generalización, pero en cualquier caso es un buen ejercicio mental. ¿Ideas? ¡Espero que si!

SOLID y Visitor: Un ejemplo explicado (1era Parte)

No es fácil encontrar ejemplos de los principios SOLID que sean lo suficientemente “realistas” y “didácticos” al mismo tiempo.

El siguiente ejemplo surge de un fragmento de código de un trabajo de tesis que actualmente estoy dirigiendo, y sirve para hablar de SRP (Single Responsibility Principle), OCP (Open-Closed Principle), ISP (Interface Segregation Principle) y DIP (Dependency Inversion Principle). El ejemplo resulta interesante porque abarca casi todos los principios SOLID y el único que queda por fuera es el LSP (Liskov Substitution Principle). Adicionalmente se muestra el uso del patrón Visitor, que es un patrón de diseño útil en algunos casos.

Problema

El problema a resolver es el siguiente: Dado un directorio (un File en Java), se debe recorrer el directorio y todos sus subdirectorios, y para cada archivo encontrado, es necesario realizar cierto proceso. En algunos casos, dependiendo de ciertas reglas, algunos subdirectorios se deben ignorar, por lo que no es necesario recorrerlos recursivamente. También existen casos en los que según ciertas reglas algunos archivos se deben ignorar y no deben ser procesados.

Este artículo tiene su origen en el código de una herramienta de internacionalización (I18N) para Java, de modo que lo que se recorre es un árbol de directorios que contiene código fuente. Entre los directorios que se ignoran están los “CVS”, “.svn” y “.hg”, entre otros, y los archivos que se deben procesar son todos aquellos con extensión “.properties” que comiencen con cualquiera de los prefijos “I18N”, “Numb” y “Date”.

Para no entrar en los detalles técnicos de la herramienta de internacionalización, en algunos casos a lo largo de este artículo se usarán como ejemplo dos posibles aplicaciones de esta idea: La primera es un anti-virus y su misión es recorrer un conjunto de directorios y procesar los archivos “.com”, “.exe” y “.dll”, buscando posibles virus y eliminándolos si los encuentra. La segunda es todo lo contrario, es decir es un virus, su misión es recorrer un conjunto de directorios, también buscando archivos “.com”, “.exe” y “.dll” para infectarlos.

En algunos casos se hablará de la funcionalidad general, es decir, de un componente que debe recorrer un conjunto de directorios filtrando según ciertas reglas cuáles recorre recursivamente y cuales ignora, y procesar o no también según ciertas reglas algunos archivos. En otros casos nos referiremos al ejemplo del antivirus – virus para poder resaltar algunos aspectos asociados a la reusabilidad.

Primera Aproximación

SOLID_visitor_1

La primera aproximación es un enfoque monolítico donde la única clase (FileScanner) tiene un único punto de entrada (scan) y hace absolutamente todo el trabajo.

Veamos que sucede si analizamos esta clase desde tres puntos de vista: Responsabilidades, Reusabilidad y Facilidad de Prueba.

Las responsabilidades de la clase son:

  • Recorrer recursivamente los directorios/subdirectorios a partir del directorio raíz pasado en el método scan.
  • Filtrar (decidir) cuales subdirectorios deben recorrerse recursivamente y cuales deben ignorarse.
  • Filtrar (decidir) cuales archivos deben procesarse y cuales deben ignorarse.
  • Procesar cada uno de los archivos encontrados.

En total podemos enumerar al menos cuatro responsabilidades diferentes en una sola clase (Baja Cohesión).

Sobre la reusabilidad del componente: Es reutilizable sólo en situaciones en las que las reglas de filtrado para directorios y archivos y el algoritmo de procesamiento de los archivos sean exactamente las mismos que en la implementación original, es decir, no es muy reutilizable (de hecho podríamos decir que no es nada reutilizable).

Sobre la facilidad de pruebas: Sólo podemos hacer pruebas de integración/sistema, es decir, pruebas fin a fin. No es posible hacer pruebas para los filtros individuales (de archivos o subdirectorios) o para el algoritmo de procesamiento de los archivos, porque todas las responsabilidades están encapsuladas en una misma clase y tenemos sólo un punto de entrada. Evidentemente esto es malo y hace que escribir pruebas para esta clase sea difícil.

En general aquí tenemos varios “smells” (olores de código/arquitectura, síntomas que nos dicen que hay algún problema de diseño). (1) La clase tiene demasiadas responsabilidades y hace muchas cosas, (2) la clase es larga (dado el punto anterior seguro que será larga), (3) la clase no es muy reutilizable en contextos distintos (no es flexible), y finalmente, (4) la clase, o más bien sus responsabilidades, son difíciles (o imposibles) de probar.

Para resolver el problema deberíamos primero revisar el SRP (Single Responsibility Principle / Principio de Responsabilidad Única) y tratar de romper la clase en varias partes de manera que podamos distribuir las responsabilidades.

En este caso particular, se puede utilizar el patrón visitante, o al menos una versión inicial del patrón visitante, porque en teoría no se estaremos usándolo sino hasta el siguiente refactor en el que incluyamos la interfaz Visitor. La idea general del patrón visitante es que dada una estructura de datos o un conjunto de objetos, se escribe un algoritmo que recorre dicha estructura y que para cada elemento en ella invoca a un método particular en una interfaz dada (el visitante), permitiendo procesar o hacer algo con el elemento actual. Lo interesante de este patrón, es que de hecho separa el recorrido de una estructura de datos del procesamiento de cada uno de los datos de la estructura de datos.

Segunda Aproximación

SOLID_visitor_2

En este escenario, FileScanner hace el recorrido de los directorios y el filtrado de los subdirectorios/archivos y cuando consigue un archivo para procesar invoca a visit en DoSomethingVisitor.

Hagamos el mismo análisis basado en responsabilidades, reusabilidad y facilidad de pruebas:

Responsabilidades de FileScanner:

  • Recorrer recursivamente los directorios/subdirectorios a partir del directorio raíz pasado en el método scan.
  • Filtrar (decidir) cuales subdirectorios deben recorrerse recursivamente y cuales deben ignorarse.
  • Filtrar (decidir) cuales archivos deben procesarse y cuales deben ignorarse.

Responsabilidades de DoSomethingVisitor:

  • Procesar cada uno de los archivos encontrados.

Es claro que este aspecto ha mejorado porque ahora las responsabilidades se han distribuido entre dos clases.

Sobre la reusabilidad: Sigue siendo poco reutilizable, porque sucede lo mismo que el escenario anterior. En general, la lógica del filtrado sigue codificada directamente en FileScanner y si bien la lógica del procesamiento está ahora codificada en DoSomethingVisitor, existe una dependencia directa entre las dos clases y aparentemente FileScanner crea su dependencia, es decir, crea una instancia de DoSomethingVisitor, lo que acopla fuertemente ambas clases entre si (o al menos acopla FileScanner a DoSomethingVisitor). Por esta razón no es posible cambiar la lógica de procesamiento según sea necesario en otro contexto, es decir, no es posible usar la implementación de FileScanner con otra lógica de procesamiento, al menos no sin cambiar el código de FileScanner.

Pensando en el ejemplo del antivirus – virus, no es posible compartir la misma implementación de FileScanner entre ambas aplicaciones porque simplemente esta clase está atada (acoplada) a DoSomethingVisitor, y para cambiar esto no queda más remedio que modificar el código de FileScanner. Vale decir en este punto que cortar y pegar no es una buena definición de reutlización, y que estar haciendo mucho copy/paste puede llegar a considerarse un “smell” de código también.

Sobre la facilidad de pruebas: Debido a que está implementado en una clase aparte, el algoritmo de procesamiento se puede probar de forma independiente de FileScanner, lo que en principio mejora un poco la situación con respecto al escenario anterior, pero FileScanner no se puede probar de forma independiente del algoritmo de procesamiento. Esto se debe a que existe una dependencia de una clase concreta y a que (probablemente) FileScanner crea esta dependencia, es decir, hace el new DoSomethingVisitor() en algún lugar de su código.

El siguiente paso podría consistir en tratar de mejorar la facilidad de prueba de FileScanner y en romper el acoplamiento/dependencia de FileScanner a un algoritmo de procesamiento concreto.

En relación con los principios SOLID, en este caso si se analiza FileScanner se puede ver que para modificar su comportamiento (lo que se hace con un archivo visitado) es necesario cambiar la implementación de DoSomethingVisitor, o peor aún, cambiar FileScanner para que apunte a una clase visitante que haga algo distinto. Este hecho, el tener que cambiar la clase (tocar el código) para cambiar su comportamiento, es una violación al OCP (Open-Close Principle / Principio Abierto-Cerrado). El OCP dice que las clases deberían estar abiertas para ser extendidas (no en el sentido de la herencia) pero cerradas para su modificación. Es decir, debería ser posible cambiar el comportamiento de un componente o clase sin que sea necesario cambiar el código de la clase. Esto puede sonar un poco confuso, pero en la próxima aproximación se verá un poco más claro.

Tercera Aproximación

SOLID_visitor_3

En este caso hemos introducido la interfaz Visitor y hemos hecho que FileScanner dependa de la interfaz en lugar de depender de un algoritmo o clase concreta. Además, y esto es extremadamente importante, estamos pasando la instancia usada de la interfaz en el constructor de FileScanner, de manera que ésta última clase ya no construye su dependencia concreta sino que se le “inyecta” en el constructor. Desde el punto de vista de FileScanner podría estar utilizando cualquier algoritmo de procesamiento, es decir, a FileScanner no le importa que algoritmo le pasen en el constructor, siempre y cuando se apegue al contrato definido por la interfaz Visitor.

Sobe SOLID, en este punto vale la pena mencionar el DIP o Dependency Inversion Principle / Principio de Inversión de Dependencias. Como hemos comentado, al FileScanner se le inyecta una instancia de Visitor en el constructor, pero ¿qué pasaría sin no le pasaramos la instancia en el constructor, sino que hicieramos algo como esto:

private Visitor visitor;

public FileScanner() {
//...
visitor = new DoSomethingVisitor();
//...

En efecto, estaríamos atando la funcionalidad del visitante a una interfaz (lo que es correcto) pero también estaríamos creando nuestra propia instancia (dependencia) concreta asociada a esa interfaz en el constructor. El punto es que de querer cambiar el comportamiento de FileScanner tendríamos que alterar el constructor, en especial cambiar el new para poder crear otra instancia concreta. El constructor anterior es una violación del principio DIP, y para resolver el problema se debería escribir de esta forma:

private Visitor visitor;

public FileScanner(Visitor visitor) {
//...
this.visitor = visitor;
//...

Es decir, la clase FileScanner no debería construir sus propias dependencias sino que estas deberían ser inyectadas en el constructor (o por setters) de forma que sea posible pasar diferentes implementaciones y generar comportamientos distintos (lo que es coherente con el OCP).

Hagamos el mismo análisis basado en responsabilidades, reusabilidad y facilidad de pruebas y veamos cómo ha valido la pena hacer este cambio:

Responsabilidades de FileScanner:

  • Recorrer recursivamente los directorios/subdirectorios a partir del directorio raíz pasado en el método scan, invocando al visitante pasado (Visitor) por cada archivo a procesar.
  • Filtrar (decidir) cuales subdirectorios deben recorrerse recursivamente y cuales deben ignorarse.
  • Filtrar (decidir) cuales archivos deben procesarse y cuales deben ignorarse.

Responsabilidades de Visitor:

  • Servir de interfaz “abstracta/genérica” para que FileScanner pueda invocar al algoritmo de procesamiento de turno.

Responsabilidades de DoSomethingVisitor:

  • Implementar Visitor y procesar cada uno de los archivos encontrados con un algoritmo particular.

En este aspecto no se ven muchos cambios, es decir, todas las clases tienen más o menos la misma cantidad de responsabilidades que tenían en el escenario anterior.

Desde el punto de vista de la reusabilidad: Hemos ganado mucho, porque ahora podemos usar FileScanner con cualquier algoritmo de procesamiento que sea necesario. Por ejemplo, podríamos tener una implementación de Visitor que saca una copia de los archivos procesados, otra que realiza cambios particulares y otra que borra los archivos, y todas las podríamos utilizar con el mismo FileScanner en diferentes contextos según sea necesario.

Pensando en el ejemplo del antivirus – virus, podríamos compartir/reusar la clase FileScanner (bueno, como veremos más adelante, no todavía, pero casi) y la interfaz Visitor tanto en el antivirus como en el virus, y lo único que cambiaría sería la implementación de la interfaz Visitor que le pasaríamos al FileScanner, es decir, en el caso del antivirus la implementación de Visitor removería los virus, en el caso del virus, la implementación infectaría los archivos procesados.

Lo importante, para cambiar el comportamiento es implementar un Visitor que haga lo que deseamos y parametrizar/configurar FileScanner con el Visitor correcto por medio del constructor antes de invocar el método scan.

Por otro lado, aún estamos atados al filtro de directorios/subdirectorios y archivos que está implementado en FileScanner. Este último punto es crítico, porque es lo único que aún no nos permite compartir el mismo FileScanner entre el antivirus y el virus, porque hay que recordar que el antivirus procesa los “.exe”, “.com” y “.dll”, mientras que el virus procesa sólo los “.exe” y los “.com”, es decir, los filtros entra ambas aplicaciones son distintos, pero como los filtros aún están codificados directamente en FileScanner, entonces eso genera un problema. Más adelante, al igual que como hicimos con el algoritmo de procesamiento, veremos que este no es un problema difícil de resolver.

Desde el punto de vista de la facilidad de pruebas también hemos ganado algo. El (los) algoritmos de procesamiento siguen siendo fáciles de probar de forma individual sin depender de FileScanner

Sin embargo ahora FileScanner es más fácil de probar, porque ahora no existe una dependencia directa entre esta clase y un algoritmo de procesamiento en particular, sino que ahora depende de una interfaz (Visitor) de la cual recibe una instancia en el constructor. De esta forma podemos probar FileScanner de manera independiente de cualquier algoritmo de procesamiento, esencialmente pasando en el constructor un Mock Object de Visitor, es decir, una implementación “falsa” de Visitor especialmente hecha para pruebas, que nos permite validar que sea invocada correctamente por FileScanner.

Más Adelante…

En una segunda parte de este artículo, seguiremos mejorando y generalizando la estructura de clases diseñada y continuaremos discutiendo los principios SOLID que aún faltan por mencionar.