Este artículo para novatos cubre otra ronda de olores y refactorizaciones con las que debe familiarizarse al principio de su carrera. Cubrimos declaraciones de casos, polimorfismo, objetos nulos y clases de datos.
Este también podría llamarse "olor a lista de verificación" o algo así. Las declaraciones de casos son un olor porque causan duplicación, a menudo también son poco elegantes. También pueden conducir a clases innecesariamente grandes porque todos estos métodos que responden a los diversos escenarios de casos (y potencialmente en aumento) a menudo terminan en la misma clase, que luego tiene todo tipo de responsabilidades mixtas. No es raro que tengas muchos métodos privados que estarían mejor en clases propias..
Un gran problema con las declaraciones de casos ocurre si desea expandirlas. Entonces tienes que cambiar ese método particular, posiblemente una y otra vez. Y no solo allí, porque a menudo tienen gemelos repetidos en todo el lugar que ahora también necesitan una actualización. Una gran manera de criar errores de seguro. Como recordarán, queremos estar abiertos para la extensión, pero cerrados para la modificación. Aquí, la modificación es inevitable y solo es cuestión de tiempo..
Usted hace que sea más difícil para usted extraer y reutilizar el código, además de que es una bomba de desorden. A menudo, el código de vista depende de tales declaraciones de casos, que luego duplican el olor y abren la puerta para una ronda de cirugía de escopeta en el futuro. ¡Ay! Además, interrogar a un objeto antes de encontrar el método correcto para ejecutar es una violación desagradable del principio de "decir y no preguntar".
Existe una buena técnica para manejar la necesidad de declaraciones de casos. Palabra elegante entrante! Polimorfismo. Esto le permite crear la misma interfaz para diferentes objetos y usar cualquier objeto que sea necesario para diferentes escenarios. Simplemente puede intercambiar el objeto apropiado y se adapta a sus necesidades porque tiene los mismos métodos. Su comportamiento debajo de estos métodos es diferente, pero mientras los objetos respondan a la misma interfaz, a Ruby no le importa. Por ejemplo, VegetarianDish.new.order
y VeganDish.new.order
comportarse de manera diferente, pero ambos responden a #orden
de la misma manera Solo desea ordenar y no contestar toneladas de preguntas como si come huevos o no.
El polimorfismo se implementa extrayendo una clase para la rama de la declaración de caso y moviendo esa lógica a un nuevo método en esa clase. Continúa haciendo eso para cada tramo en el árbol condicional y dales a todos el mismo nombre de método. De esa manera, encapsula ese comportamiento en un objeto que es el más adecuado para tomar ese tipo de decisión y no tiene ninguna razón para cambiar más. Mira, de esa manera puedes evitar todas estas preguntas molestas sobre un objeto; simplemente dile qué se supone que debe hacer. Cuando surja la necesidad de más casos condicionales, simplemente creará otra clase que se encargue de esa responsabilidad única bajo el mismo nombre de método.
Lógica de declaración de caso
clase Operación def precio caso @mission_tpe when: counter_intelligence @standard_fee + @counter_intelligence_fee when: terrorism @standard_fee + @terrorism_fee when: revenge @standard_fee + @revenge_fee when: extortion @standard_fee + @extortion_fee end end end end counter_intel_op = orden de pago : counter_intelligence) counter_intel_op.price terror_op = Operation.new (mission_type:: terrorismo) terror_op.price revenge_op = Operation.new (mission_type:: revenge) revenge_op.price extortion_op = Operation.new (mission_type:: revenge) extortion_op.price
En nuestro ejemplo, tenemos una Operación
clase que necesita preguntar acerca de su tipo_mision
Antes de que pueda decirle su precio. Es fácil ver que esto precio
El método solo espera cambiar cuando se agrega un nuevo tipo de operación. Cuando también desee mostrar eso en su vista, también deberá aplicar ese cambio allí. (Para su información, para las vistas, puede usar parciales polimórficos en Rails para evitar que estas declaraciones de casos sean criticadas en toda su vista).
Clases polimorfas
class CounterIntelligenceOperation def price @standard_fee + @counter_intelligence_fee end class finperationpetration en la tabla de finalización de la competencia. .price terror_op = CounterIntelligenceOperation.new terror_op.price revenge_op = CounterIntelligenceOperation.new revenge_op.price extortion_op = CounterIntelligenceOperation.new extortion_op.price
Entonces, en lugar de ir por ese agujero de conejos, creamos un montón de clases de operaciones que tienen su propio conocimiento de cuánto suman sus tarifas al precio final. Solo podemos decirles que nos den su precio. No siempre tienes que deshacerte del original (Operación
) solo en clase cuando descubres que has extraído todo el conocimiento que tenía.
Creo que la lógica detrás de las declaraciones de casos es inevitable. Los escenarios en los que tiene que pasar por algún tipo de lista de verificación antes de encontrar el objeto o el comportamiento que le permite realizar el trabajo son demasiado comunes. La pregunta es simplemente cómo se manejan mejor. Estoy a favor de no repetirlos y usar las herramientas de programación orientada a objetos que me ofrece para diseñar clases discretas que se pueden intercambiar fácilmente a través de su interfaz..
La comprobación de nulo en todo el lugar es un tipo especial de olor a declaración de caso. Preguntar un objeto sobre nil es a menudo un tipo de declaración de caso oculto. Manejar condicionalmente puede tomar la forma de object.nil?
, objeto.presente?
, object.try
, y luego algún tipo de acción en el caso nulo
aparece en tu fiesta.
Otra pregunta más astuta es preguntarle a un objeto sobre su veracidad, si existe o si es nulo, y luego actuar. Parece inofensivo, pero es solo un disfraz. No te dejes engañar: operadores ternarios o ||
Los operadores también entran en esta categoría, por supuesto. Dicho de otra manera, los condicionales no solo se muestran claramente identificables como a no ser que
, si-si no
o caso
declaraciones Tienen formas más sutiles de romper tu fiesta. No pregunte a los objetos por su nulidad, pero dígales si el objeto de su ruta feliz está ausente, que un objeto nulo ahora está a cargo de responder a sus mensajes..
Los objetos nulos son clases ordinarias. No hay nada especial en ellos, solo un "nombre elegante". Extraes alguna lógica condicional relacionada con nulo
Y luego lo tratas de manera polimórfica. Contiene ese comportamiento, controla el flujo de su aplicación a través de estas clases y también tiene objetos que están abiertos para otras extensiones que se adapten a ellos. Piensa en cómo un Juicio
(Suscripción nula
) La clase podría crecer con el tiempo. No solo es más SECO y yadda-yadda-yadda, sino que también es mucho más descriptivo y estable..
Un gran beneficio de usar objetos nulos es que las cosas no pueden explotar tan fácilmente. Estos objetos responden a los mismos mensajes que los objetos emulados (no siempre es necesario copiar toda la API a los objetos nulos, por supuesto), lo que da a su aplicación pocas razones para volverse loco. Sin embargo, tenga en cuenta que los objetos nulos encapsulan la lógica condicional sin eliminarla por completo. Usted acaba de encontrar un mejor hogar para ello.
Dado que tener una gran cantidad de acciones relacionadas con nada en su aplicación es bastante contagioso y poco saludable para su aplicación, me gusta pensar en los objetos nulos como el "Patrón de Contención Nula" (¡Por favor, no me demanden!). ¿Por qué contagioso? Porque si pasas nulo
Alrededor, en algún otro lugar en su jerarquía, otro método es tarde o temprano también obligado a preguntar si nulo
está en la ciudad, lo que luego lleva a otra ronda de tomar contramedidas para tratar un caso así.
Dicho de otra manera, nil no es bueno para pasar el rato porque pedir su presencia se vuelve contagioso. Pidiendo objetos para nulo
es muy probable que siempre sea un síntoma de mal diseño, ¡no te ofendas y no te sientas mal! Todos hemos estado allí. No quiero hablar "voluntariamente" de lo hostil que puede ser, pero hay que mencionar algunas cosas:
En general, el escenario de que falta un objeto aparece con mucha frecuencia. Un ejemplo a menudo citado es una aplicación que tiene un registro Usuario
y un NilUser
. Pero dado que los usuarios que no existen es un concepto tonto si esa persona está explorando claramente su aplicación, probablemente sea mejor tener un Huésped
que aún no se ha registrado. Una suscripción perdida podría ser una Juicio
, una carga cero podría ser una Freebie
, y así.
Nombrar los objetos nulos es a veces obvio y fácil, a veces muy difícil. Pero trate de evitar nombrar todos los objetos nulos con un "Nulo" o "No". ¡Puedes hacerlo mejor! Creo que proporcionar un poco de contexto hace mucho. Elija un nombre que sea más específico y significativo, algo que refleje el caso de uso real. De esa manera, se comunicará más claramente con otros miembros del equipo y con su futuro yo, por supuesto..
Hay un par de maneras de implementar esta técnica. Te mostraré uno que creo que es sencillo y para principiantes. Espero que sea una buena base para entender enfoques más involucrados. Cuando te encuentras repetidamente con algún tipo de condicional que trata con nulo
, sabes que es hora de simplemente crear una nueva clase y cambiar ese comportamiento. Después de eso, le dejas saber a la clase original que este nuevo jugador trata ahora con la nada..
En el siguiente ejemplo, puedes ver que el Espectro
clase pide un poco demasiado sobre nulo
y desordena el código innecesariamente. Quiere asegurarse de que tenemos un operación malvada
Antes de que decida cobrar. ¿Puedes ver la violación de "Decir-No-preguntar"?
Otra parte problemática es la razón por la cual Specter debería preocuparse por la implementación de un precio cero. los tratar
método también pregunta a escondidas si el operación malvada
tiene un precio
para manejar la nada a través de la o
(||
) declaración. mal_operacion.presente?
hace el mismo error Podemos simplificar esto:
la clase Specter incluye ActiveModel :: Model attr_accessor: credit_card,: evil_operation def cargo a menos que evil_operation.nil? evil_operation.charge (credit_card) end end def has_discount? mal_operacion.presente? && evil_operation.has_discount? final def precio evil_operation.try (: precio) || 0 clase final EvilOperation incluye ActiveModel :: Modelo attr_accessor: discount,: price def has_discount? cargo final de descuento cargo (credit_card) credit_card.charge (precio) end end
clase NoOperation def charge (tarjeta de crédito) "No se registró ninguna operación perversa" end def has_discount? falso final def precio 0 final fin clase Specter incluye ActiveModel :: Modelo attr_accessor: credit_card,: evil_operation def charge evil_operation.charge (credit_card) end def has_discount? evil_operation.has_discount? end def price evil_operation.price end private def evil_operation @evil_operation || NoOperation.new end end class EvilOperation incluye ActiveModel :: Model attr_accessor: discount,: price def has_discount? cargo final de descuento cargo (credit_card) credit_card.charge (precio) end end
Supongo que este ejemplo es lo suficientemente simple como para ver de inmediato lo elegantes que pueden ser los objetos nulos. En nuestro caso, tenemos un No operacion
clase nula que sabe cómo lidiar con una operación malvada no existente a través de:
0
cantidades.Creamos esta nueva clase que trata con la nada, un objeto que maneja la ausencia de cosas, y la cambiamos a la clase antigua si nulo
aparece. La API es la clave aquí porque si coincide con la de la clase original, puede intercambiar perfectamente el objeto nulo con los valores de retorno que necesitamos. Eso es exactamente lo que hicimos en el método privado. Specter # evil_operation
. Si tenemos el operación malvada
objeto, necesitamos usar eso; Si no, usamos nuestro nulo
camaleón que sabe cómo lidiar con estos mensajes, por lo que operación malvada
Nunca más volverá a cero. Pato escribiendo en su mejor momento.
Nuestra lógica condicional problemática ahora está envuelta en un lugar, y nuestro objeto nulo está a cargo del comportamiento. Espectro
está buscando. ¡SECO! Nosotros reconstruimos un poco la misma interfaz del objeto original que nil no pudo manejar. Recuerde, nil hace un mal trabajo al recibir mensajes, ¡nadie está en casa, siempre! De ahora en adelante, solo le está diciendo a los objetos qué hacer sin pedirles primero su "permiso". Lo que también es bueno es que no había necesidad de tocar el EvilOperation
clase en absoluto.
Por último, pero no por ello menos importante, me deshice de la verificación si una operación malvada está presente en Espectro # has_discount?
. No es necesario asegurarse de que existe una operación para obtener el descuento. Como resultado del objeto nulo, el Espectro
La clase es mucho más delgada y no comparte las responsabilidades de otras clases tanto..
Una buena guía para eliminar esto es no verificar los objetos si están dispuestos a hacer algo. Mándalos como un sargento de instrucción. Como suele ser el caso, probablemente no sea bueno en la vida real, pero es un buen consejo para la programación orientada a objetos. Ahora dame 20!
En general, todos los beneficios de usar Polymorphism en lugar de las declaraciones de casos también se aplican a los objetos nulos. Después de todo. Es solo un caso especial de declaraciones de casos. Lo mismo ocurre con los inconvenientes:
Vamos a cerrar este artículo con algo ligero. Esto es un olor porque es una clase que no tiene ningún comportamiento, excepto obtener y configurar sus datos; básicamente, no es más que un contenedor de datos sin métodos adicionales que hacen algo con esos datos. Eso los hace de naturaleza pasiva porque estos datos se guardan allí para ser utilizados por otras clases. Y ya sabemos que este no es un escenario ideal porque grita característica de la envidia. En general, queremos tener clases que tengan datos de significado estatal que cuiden, así como comportamiento a través de métodos que puedan actuar sobre esos datos sin mucho obstáculo o espionaje en otras clases.
Puede iniciar clases de refactorización como éstas posiblemente extrayendo el comportamiento de otras clases que actúan sobre los datos en su clase de datos. Al hacer eso, puede atraer lentamente un comportamiento útil para esos datos y darle un hogar adecuado. Está totalmente bien si estas clases crecen el comportamiento con el tiempo. A veces puede mover todo un método fácilmente, y otras veces necesita extraer partes de un método más grande primero y luego extraerlo en la clase de datos. Cuando tenga dudas, si puede reducir el acceso que tienen otras clases en su clase de datos, definitivamente debería hacerlo y cambiar ese comportamiento. Tu objetivo debe ser retirar en algún momento a los captadores y definidores que dieron acceso a otros datos a esos objetos. Como resultado, tendrá un menor acoplamiento entre clases, lo que siempre es una victoria!
Mira, no era tan complicado! Hay una gran cantidad de jerga sofisticada y técnicas de sonido complicadas en torno a los olores de código, pero con suerte te darás cuenta de que los olores y sus refactorizaciones también comparten un par de rasgos que son limitados en número. Los olores del código nunca desaparecerán o se volverán irrelevantes, al menos no hasta que nuestros AI sean realzados por AIs que nos permiten escribir el tipo de código de calidad del que estamos a años luz de distancia en este momento.
A lo largo de los últimos tres artículos, le presenté una buena parte de los escenarios de olfato de código más importantes y comunes que encontrará durante su carrera de programación orientada a objetos. Creo que esta fue una buena introducción para los novatos en este tema, y espero que los ejemplos de código sean lo suficientemente detallados como para seguir fácilmente el hilo..
Una vez que haya descubierto estos principios para el diseño de códigos de calidad, obtendrá nuevos olores y sus refactorizaciones en un instante. Si lo has logrado hasta ahora y sientes que no has perdido el panorama general, creo que estás listo para acercarte al estado de POO a nivel de jefe.