Ventajas del patrón de estrategia

15

¿Por qué es beneficioso usar el patrón de estrategia si solo puede escribir su código en casos if / then?

Por ejemplo: tengo una clase TaxPayer, y uno de sus métodos calcula los impuestos usando diferentes algoritmos. Entonces, ¿por qué no puede tener casos if / then y descubrir qué algoritmo usar en ese método, en lugar de usar el patrón de estrategia? Además, ¿por qué no puede simplemente implementar un método separado para cada algoritmo en la clase TaxPayer?

Además, ¿qué significa que el algoritmo cambie en tiempo de ejecución?

Armon Safai
fuente
2
¿Es esta tarea? Es mejor decir eso por adelantado si es así.
Fuhrmanator
2
@Fuhrmanator no es
Armon Safai

Respuestas:

20

Por un lado, grandes grupos de if/elsebloques no son fácilmente comprobables . Cada nueva "rama" agrega otra ruta de ejecución y, por lo tanto, aumenta la complejidad ciclomática . Si desea probar su código a fondo, tendría que cubrir todas las rutas de ejecución, y cada condición requeriría que escriba al menos una prueba más (suponiendo que escriba pruebas pequeñas y enfocadas). Por otro lado, las clases que implementan estrategias suelen exponer solo 1 método público, que es fácil de probar.

Entonces, con anidado if/else, terminará con muchas pruebas para una sola parte de su código, mientras que con Estrategia tendrá pocas pruebas para cada una de las estrategias más simples. Con este último, es fácil tener una mejor cobertura, porque es más difícil pasar por alto las rutas de ejecución.

En cuanto a la extensibilidad , imagine que está escribiendo un marco, donde se supone que los usuarios pueden inyectar su propio comportamiento. Por ejemplo, desea crear algún tipo de marco de cálculo de impuestos y desea apoyar los sistemas fiscales de diferentes países. En lugar de implementarlos todos, solo desea dar a los usuarios del marco la oportunidad de proporcionar una implementación de cómo calcular algunos impuestos particulares.

Aquí está el patrón de estrategia:

  • Usted define una interfaz, por ejemplo TaxCalculation, y su marco acepta instancias de este tipo para calcular impuestos
  • Un usuario del marco crea una clase que implementa esta interfaz y la pasa a su marco, proporcionando así una forma de realizar parte de los cálculos.

No puede hacer lo mismo if/else, porque eso requeriría cambiar el código del marco, en cuyo caso ya no sería un marco. Dado que los marcos a menudo se distribuyen en forma compilada, esta puede ser la única opción.

Aún así, incluso si solo escribe un código regular, la Estrategia es beneficiosa porque aclara sus intenciones. Dice "esta lógica es conectable y condicional", es decir, puede haber múltiples implementaciones que pueden variar según las acciones del usuario, la configuración o incluso la plataforma.

El uso del patrón de estrategia puede mejorar la legibilidad porque, si bien una clase que implementa una estrategia particular generalmente debe tener un nombre descriptivo, por ejemplo USAIncomeTaxCalculator, los if/elsebloques no tienen nombre, en el mejor de los casos solo se comentan, y los comentarios pueden mentir. Además, para mi gusto personal, solo tener más de 3 if/elsebloques seguidos no es legible, y se vuelve bastante malo con los bloques anidados.

El principio Abierto / Cerrado también es muy relevante porque, como describí en el ejemplo anterior, la Estrategia le permite extender una lógica en algunas partes de su código ("abierto para extensión") sin reescribir esas partes ("cerrado para modificación" )

scriptin
fuente
1
if/elseLos bloques también reducen la legibilidad del código. En cuanto al patrón de estrategia, vale la pena mencionar el principio Abierto / Cerrado de la OMI.
Maciej Chałapuk
1
La capacidad de prueba es la gran razón. (La mayoría) Cada rama en su código debe ser probada. Cuantos más ifs tenga, más rutas posibles existen a través de su código, más pruebas tiene que escribir y más formas de que ese método falle. Si puedo citar al difunto Yogi Berra: "Si llegas a una bifurcación en el camino, tómalo". Esto se aplica brillantemente a las pruebas unitarias. Además, muchas ifdeclaraciones significan que es probable que repita la lógica para esas condiciones, lo que aumenta aún más su carga de prueba y aumenta el riesgo de que surjan errores.
Greg Burghardt
gracias por la respuesta. Entonces, ¿por qué no puedo usar métodos separados para diferentes algoritmos en la misma clase?
Armon Safai
Puede, pero aún necesitaría un montón de if/elsebloques para llamarlos (o dentro de ellos, para determinar si debería hacer algo o no), por lo que no es de mucha ayuda, excepto quizás un código más legible. Y tampoco hay extensibilidad para los usuarios de su marco hipotético.
scriptin
1
¿Puedes ser más claro acerca de por qué es más fácil de probar? El ejemplo para refactorizar una declaración de caso (o si / luego) a un método polimórfico (base para la estrategia) es bastante fácil de probar. refactoring.com/catalog/replaceConditionalWithPolymorphism.html Si conozco todas las condiciones para evaluar, escribo una prueba para cada una. Si tengo estrategias, tengo que crear instancias y ejecutar una para cada una. ¿Cómo es más fácil probar el enfoque estratégico? No estamos hablando de complejos if anidados cuando refactoriza la estrategia.
Fuhrmanator
5

¿Por qué es beneficioso usar el patrón de estrategia si solo puede escribir su código en casos if / then?

A veces deberías usar if / then. Es un código simple que es fácil de leer.

Los dos problemas clave con el simple código if / then es que puede violar el principio abierto cerrado . Si alguna vez tiene que entrar y agregar o cambiar una condición, está modificando este código. Si espera tener más condiciones, solo agregar una nueva estrategia es más simple / más limpio / menos probable que se rompa.

El otro problema es el acoplamiento. Al usar if / then, todas las implementaciones están vinculadas a esa implementación, lo que hace que sea más difícil cambiarlas en el futuro. Al usar la estrategia, el único acoplamiento es a la interfaz de la estrategia.

Telastyn
fuente
¿Qué hay de malo en modificar el código en el código if / then? ¿no tendría que modificar también el código en el patrón de estrategia si decide cambiar cómo funciona uno de los algoritmos?
Armon Safai
@armonsafai: si modifica la estrategia, solo necesita probar la estrategia. Si modifica todos los algoritmos, debe probar todos los algoritmos. Peor aún, si agrega una nueva estrategia, solo necesita probar la estrategia. Si agrega un nuevo condicional, debe probar todos los condicionales.
Telastyn
4

La estrategia es útil cuando las if/thencondiciones se basan en tipos , como se explica en http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html

Los condicionales de verificación de tipo generalmente no tienen una alta complejidad ciclomática, por lo que no diría que la Estrategia mejora las cosas necesariamente allí.

La razón principal de la Estrategia se explica en el libro GoF p.316 que introdujo el patrón:

Use el patrón de estrategia cuando

...

  • una clase define muchos comportamientos, y estos aparecen como múltiples declaraciones condicionales en sus operaciones. En lugar de muchos condicionales, mueva ramas condicionales relacionadas a su propia clase de estrategia.

Como se menciona en otras respuestas, si se aplica adecuadamente, el patrón de Estrategia permite agregar nuevas extensiones (estrategias concretas) sin necesariamente requerir modificaciones del resto del código. Este es el llamado principio abierto-cerrado o principio de variaciones protegidas . Por supuesto, aún debe codificar la nueva estrategia concreta, y el código del cliente debe reconocer las estrategias como complementos (esto no es trivial).

Con if/thencondicionales, es necesario cambiar el código de la clase que contiene la lógica condicional. Como se menciona en otras respuestas, a veces esto está bien cuando no desea agregar la complejidad para admitir agregar nuevas funcionalidades (complementos) sin volver a compilar.

Fuhrmanator
fuente
3

[...] si solo puede escribir su código en casos if / then?

Ese es exactamente el mayor beneficio del patrón estratégico. No tener condiciones.

Desea que sus clases / métodos / funciones sean lo más simples y breves posible. El código corto es muy fácil de probar y muy fácil de leer.

Las condiciones ( if/ elseif/ else) hacen que sus clases / métodos / funciones sean largos, porque generalmente el código donde se evalúa una decisión truees diferente de la parte donde se evalúa la decisión false.


Otro gran beneficio del patrón de estrategia es que es reutilizable en todo su proyecto.

Al usar el patrón de diseño de la estrategia, es muy probable que tenga algún tipo de contenedor de IoC, del cual obtiene la implementación deseada de una interfaz, tal vez por un getById(int id)método, donde idpodría ser un miembro enumerador.

Esto significa que la creación de la implementación es solo en un lugar de su código.

Si desea agregar más implementaciones, agregue la nueva implementación al getByIdmétodo y este cambio se refleja en todas partes en el código donde lo llama.

Con if/ elseif/ elseesto es imposible de hacer. Al agregar una nueva implementación, debe agregar un nuevo elseifbloque y hacerlo en todas partes donde se usaron las implementaciones, o podría terminar con un código que no es válido, porque olvidó agregar la implementación a su estructura.


Además, ¿qué significa que el algoritmo cambie en tiempo de ejecución?

En mi ejemplo, idpodría ser una variable que se rellena en función de una entrada del usuario. Si el usuario hace clic en un botón A, entonces id = 2, si hace clic en un botón B, entonces id = 8.

Debido al idvalor diferente , se obtiene una implementación diferente de una interfaz del contenedor IoC y el código realiza diferentes operaciones.

Andy
fuente
gracias por la respuesta. Entonces, ¿por qué no puedo usar métodos separados para diferentes algoritmos en la misma clase?
Armon Safai
@ArmonSafai ¿Los métodos separados realmente resolverían algo? No lo creo. Está trasladando el problema de un lugar a otro, y la decisión sobre qué método llamar se tomará en función del resultado de una condición. De nuevo, un if/ elseif/ elseestado. Igual que antes, solo en un lugar diferente.
Andy
Entonces, ¿los casos if / then estarían en la derecha principal? ¿No tendría que usar también los casos si / luego en main para el patrón de estrategia?
Armon Safai
1
@ArmonSafai No, no lo harías. Tendría un interruptor en la idvariable en el getByIdmétodo, que devolvería la implementación específica. Cada vez que necesite una implementación de la interfaz, le pedirá al contenedor de IoC que se la entregue.
Andy
1
@ArmonSafai También podría tener un método que getSortByEnumType(SortEnum type)devuelva una implementación de una Sortinterfaz, un método que getSortTypedevuelva una SortEnumvariable y tome una colección como parámetro, y el getSortByEnumTypemétodo nuevamente contendrá un interruptor en el typeparámetro que le devolverá el algoritmo de clasificación correcto. Si necesita agregar un nuevo algoritmo de clasificación, solo necesita editar la enumeración y un método. Y estás listo.
Andy
2

¿Por qué es beneficioso usar el patrón de estrategia si solo puede escribir su código en casos if / then?

El Patrón de estrategia le permite separar sus algoritmos (los detalles) de su lógica empresarial (política de alto nivel). Estas dos cosas no solo son confusas de leer cuando se mezclan, sino que también tienen razones muy diferentes para cambiar.

También hay un factor importante de escalabilidad del trabajo en equipo aquí. Imagine un gran equipo de programación donde muchas personas están trabajando en este paquete de contabilidad. Si todos los algoritmos de impuestos están en la clase o módulo TaxPayer , entonces los conflictos de fusión se vuelven probables. Los conflictos de fusión requieren mucho tiempo y son propensos a errores. Esta vez, la productividad del equipo se debilita y los errores introducidos por las malas fusiones dañan la credibilidad con los clientes.

Además, ¿qué significa que el algoritmo cambie en tiempo de ejecución?

Un algoritmo que cambia en tiempo de ejecución es aquel cuyo comportamiento está determinado por la configuración o el contexto. Un enfoque if / then en el lugar no permite esto de manera efectiva, ya que implica volver a cargar las clases existentes utilizadas activamente. Con el Patrón de estrategia, los objetos de estrategia que implementan cada algoritmo podrían construirse en uso. Como resultado, los cambios en estos algoritmos (correcciones de errores o mejoras) podrían realizarse y recargarse en tiempo de ejecución. Este enfoque podría usarse para permitir la disponibilidad continua y las versiones de tiempo de inactividad cero.

Alain O'Dea
fuente
1

No hay nada malo en if/elsesí mismo. En muchos casos if/elsees la forma más simple y legible de expresar la lógica. Por lo tanto, el enfoque que describe es perfectamente válido en muchos casos. (También es perfectamente comprobable, por lo que no es un problema).

Pero hay algunos casos particulares en los que un patrón de estrategia puede mejorar la capacidad de mantenimiento del código general. Por ejemplo:

  • Si los algoritmos de cálculo de impuestos particulares pueden cambiar independientemente uno del otro y de la lógica central. En este caso, sería bueno separarlos en clases distintas, ya que los cambios se localizarán.
  • Si se pueden agregar nuevos algoritmos en el futuro, sin que cambie la lógica central.
  • Si la causa de la diferencia entre los dos algoritmos también afecta a otras partes del código. Supongamos que selecciona entre los dos algoritmos en función del nivel de ingresos del contribuyente. Si este grupo de ingresos también hace que seleccione diferentes sucursales en otros lugares del código, entonces es más claro crear una instancia de una estrategia correspondiente al grupo de ingresos una vez, y llamar cuando sea necesario, en lugar de tener varias sucursales if / else dispersas en el código.

Para que el patrón de estrategia tenga sentido, la interfaz entre la lógica central y los algoritmos de cálculo de impuestos debe ser más estable que los componentes individuales. Si es igual de probable que un cambio de requisitos haga que cambie la interfaz, entonces el patrón de estrategia podría ser realmente una responsabilidad.

Todo se reduce a si los "algoritmos de cálculo de impuestos" se pueden separar limpiamente de la lógica central que lo invoca. Un patrón de estrategia tiene algunos gastos generales en comparación con un if/else, por lo que tendrá que decidir caso por caso si la inversión vale la pena.

JacquesB
fuente