Conceptos básicos sobre el olor de Ruby / Rails 01

Los temas

  • Aviso
  • Resistencia
  • Clase grande / Clase de Dios
  • Clase de extracto
  • Método largo
  • Lista larga de parámetros

Aviso

La siguiente breve serie de artículos está dirigida a desarrolladores y principiantes de Ruby con poca experiencia por igual. Tenía la impresión de que los olores de código y sus refactorizaciones pueden ser muy intimidantes e intimidantes para los novatos, especialmente si no están en la posición afortunada de tener mentores que puedan convertir los conceptos de programación mística en bombillas brillantes..

Habiendo entrado obviamente en estos zapatos, recordé que era innecesariamente confuso meterme en los olores de código y refactorizaciones.

Por un lado, los autores esperan un cierto nivel de competencia y, por lo tanto, pueden no sentirse súper obligados a proporcionarle al lector la misma cantidad de contexto que un novato podría necesitar para sumergirse cómodamente en este mundo antes..

Como consecuencia, tal vez, los novatos por otro lado tienen la impresión de que deben esperar un poco más hasta que estén más avanzados para aprender sobre los olores y refactorizaciones. No estoy de acuerdo con ese enfoque y creo que hacer este tema más accesible los ayudará a diseñar un mejor software al principio de su carrera. Al menos, espero que ayude a los peeps junior con una ventaja sólida..

Entonces, ¿de qué estamos hablando exactamente cuando las personas mencionan los olores de código? ¿Es siempre un problema en su código? ¡No necesariamente! ¿Se pueden evitar por completo? ¡No lo creo! ¿Quieres decir que los olores del código llevan a un código roto? Bueno, a veces y otras veces no. ¿Debería ser mi prioridad arreglarlos de inmediato? La misma respuesta, me temo: a veces sí y otras veces debes freír peces grandes primero. ¿Estas loco? Buena pregunta en este punto!

Antes de continuar sumergiéndose en todo este asunto apestoso, recuerde quitar una cosa de todo esto: no intente arreglar todos los olores que encuentre, esto es sin duda una pérdida de tiempo.!

Me parece que los olores de código son un poco difíciles de envolver en una caja bien etiquetada. Hay todo tipo de olores con diferentes opciones para abordarlos. Además, diferentes lenguajes de programación y marcos son propensos a diferentes tipos de olores, pero definitivamente hay muchas cepas "genéticas" comunes entre ellos. Mi intento de describir los olores del código es compararlos con los síntomas médicos que le indican que podría tener un problema. Pueden señalar todo tipo de problemas latentes y tener una amplia variedad de soluciones si se diagnostican..

Afortunadamente, en general no son tan complicados como tratar con el cuerpo humano y la psique, por supuesto. Sin embargo, es una comparación justa, ya que algunos de estos síntomas deben tratarse de inmediato y otros le brindan el tiempo suficiente para encontrar una solución que sea mejor para el bienestar general del "paciente". Si tiene un código de trabajo y se encuentra con algo maloliente, tendrá que tomar la decisión difícil si vale la pena buscar una solución y si esa refactorización mejora la estabilidad de su aplicación..

Dicho esto, si te topas con un código que puedes mejorar de inmediato, es un buen consejo dejar el código un poco mejor que antes, incluso un poquito mejor aumenta sustancialmente con el tiempo.

Resistencia

La calidad de su código se vuelve cuestionable si la inclusión de un nuevo código se vuelve más difícil, por ejemplo, decidir dónde colocar un nuevo código es una molestia o viene con una gran cantidad de efectos en todo el código, por ejemplo. Esto se llama resistencia..

Como una guía para la calidad del código, puede medirlo siempre por lo fácil que es introducir cambios. Si eso es cada vez más difícil, definitivamente es hora de refactorizar y tomar la última parte de REFACTOR ROJO-VERDE mas seriamente en el futuro.

Clase grande / Clase de Dios

Comencemos con algo que suene sofisticado: "Clases de Dios", porque creo que son particularmente fáciles de entender para los principiantes. Las clases de Dios son un caso especial de un código llamado olor. Clase grande. En esta sección me ocuparé de ambos. Si has pasado un poco de tiempo en Rails land, es probable que los hayas visto tan a menudo que te parecen normales..

¿Seguramente recuerdas el mantra de "modelos gordos, controlador flaco"? Bueno, en realidad, flaco es bueno para todas estas clases, pero como guía es un buen consejo para los novatos, supongo.

Las clases de Dios son objetos que atraen todo tipo de conocimiento y comportamiento como un agujero negro. Sus sospechosos habituales incluyen con mayor frecuencia el modelo de usuario y cualquier problema (¡con suerte!) Que su aplicación está tratando de resolver, al menos en primer lugar. Una aplicación de todo podría aumentar en el Todos modelo, una aplicación de compras en Productos, una aplicación de fotos en Las fotos-obtienes la deriva.

La gente los llama clases de dios porque saben demasiado. Tienen demasiadas conexiones con otras clases, principalmente porque alguien las estaba modelando perezosamente. Sin embargo, es un trabajo duro mantener a raya las clases de dioses. Hacen que sea realmente fácil volcarles más responsabilidades, y como muchos héroes griegos atestiguan, se necesita un poco de habilidad para dividir y conquistar a los "dioses"..

El problema con ellos es que se vuelven cada vez más difíciles de entender, especialmente para los nuevos miembros del equipo, más difíciles de cambiar, y volver a usarlos se convierte en una opción cada vez menor a medida que aumenta la gravedad. Oh, sí, tienes razón, tus pruebas son innecesariamente más difíciles de escribir también. En resumen, no hay realmente una ventaja al tener clases grandes, y las clases de dioses en particular.

Hay un par de síntomas / signos comunes de que su clase necesita algo de heroísmo / cirugía:

  • Necesitas desplazarte!
  • Toneladas de métodos privados.?
  • ¿Tu clase tiene siete o más métodos??
  • Es difícil decir lo que realmente hace tu clase, concisamente!
  • ¿Tiene tu clase muchas razones para cambiar cuando tu código evoluciona??

Además, si entrecierras los ojos en tu clase y piensas "¿Eh? ¡Ew! ”Usted podría estar en algo también. Si todo esto te suena familiar, es muy probable que hayas encontrado un buen ejemplar..

"clase de rubí CastingInviter EMAIL_REGEX = /\A([^@\s◆+)@((?:[-a-z0-9◆+.)+[a-z◆2 ,)\z/

attr_reader: mensaje,: invitados,: casting

def initialize (atributos = ) @message = atributos [: mensaje] || "@invitees = atributos [: invitadas] ||" @ remitente = atributos [: remitente] @casting = atributos [: casting] final

def válido? valido_mensaje? && valid_invitees? final def entregar si es válido? invitee_list.each do | email | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else failure_message = "Su mensaje # @casting no se pudo enviar. Los correos electrónicos de los invitados o el mensaje no son válidos" invitation = create_invitation (@sender) Mailer.invitation_notification (invitación, mensaje de fallo) fin final fin privado def invalid_invitees @invalid_invitees || = invitee_list.map do | item | a menos que item.match (EMAIL_REGEX) item end end.compact end def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,;] + /) end def valid_message? @ message.present? end def valid_invitees? invalid_invitees.empty? end 

def create_invitation (email) Invitation.create (casting: @casting, sender: @sender, invitee_email: email, estado: 'pendiente') final fin "

Feo amigo, ¿eh? ¿Puedes ver cuánta maldad está incluida aquí? Por supuesto, pongo un poco de cereza encima, pero tarde o temprano te encontrarás con un código como este. Pensemos en qué responsabilidades esta CastingInviter la clase tiene que hacer malabares.

  • Entregando email
  • Comprobación de mensajes válidos y direcciones de correo electrónico
  • Deshacerse del espacio en blanco
  • Dividir direcciones de correo electrónico en comas y puntos y comas

En caso de que todo esto se descargue en una clase que solo quiere entregar una llamada de casting a través de entregar? ¡Ciertamente no! Si su método de invitación cambia, puede esperar encontrarse con alguna cirugía de escopeta. CastingInviter no necesita conocer la mayoría de estos detalles. Eso es más responsabilidad de una clase que está especializada en tratar asuntos relacionados con el correo electrónico. En el futuro, encontrará muchas razones para cambiar su código aquí también..

Clase de extracto

Entonces, ¿cómo debemos tratar con esto? A menudo, extraer una clase es un patrón práctico de refactorización que se presentará como una solución razonable para problemas como clases grandes y complicadas, especialmente cuando la clase en cuestión tiene múltiples responsabilidades..

Los métodos privados a menudo son buenos candidatos para comenzar y también son fáciles de calificar. A veces necesitarás extraer incluso más de una clase de un chico tan malo, pero no lo hagas todo en un solo paso. Una vez que encuentre suficiente carne coherente que parece pertenecer a un objeto especializado propio, puede extraer esa funcionalidad en una nueva clase..

Crea una nueva clase y gradualmente mueve la funcionalidad de uno en uno. Mueva cada método por separado y cámbieles el nombre si ve una razón para hacerlo. Luego haga referencia a la nueva clase en la original y delegue la funcionalidad necesaria. Lo bueno es que tiene cobertura de prueba (¡con suerte!) Que le permite verificar si las cosas todavía funcionan correctamente en cada paso del camino. Apunta a poder reutilizar tus clases extraídas también. Es más fácil ver cómo se hace en acción, así que leamos un código:

"clase rubí CastingInviter

attr_reader: mensaje,: invitados,: casting

def initialize (atributos = ) @message = atributos [: mensaje] || "@invitees = atributos [: invitadas] ||" @casting = atributos [: casting] @sender = atributos [: remitente] fin

def válido? casting_email_handler.valid? fin

def entregar casting_email_handler.deliver final

privado

def casting_email_handler @casting_email_handler || = CastingEmailHandler.new (mensaje: mensaje, invitados: invitados, casting: casting, remitente: @sender) end end "

"ruby class CastingEmailHandler EMAIL_REGEX = /\A([^@\s◆+)@((?:[-a-z0-9◆+.)+[a-z◆2,)\z/

def initialize (attr = ) @message = attr [: message] || "@invitees = attr [: invitees] ||" @casting = attr [: casting] @sender = attr [: sender] end

def válido? valido_mensaje? && valid_invitees? fin

def entregar si es válido? invitee_list.each do | email | invitation = create_invitation (email) Mailer.invitation_notification (invitation, @message) end else else failure_message = "No se pudo enviar tu mensaje # @casting. Los correos electrónicos de los invitados o los mensajes no son válidos "invitation = create_invitation (@sender) Mailer.invitation_notification (invitation, failure_message) end end

privado

def invalid_invitees @invalid_invitees || = invitee_list.map do | item | a menos que item.match (EMAIL_REGEX) end end end.compact end

def invitee_list @invitee_list || = @ invitees.gsub (/ \ s + /, "). split (/ [\ n,;] + /) end

def valid_invitees? invalid_invitees.empty? fin

def valid_message? @ message.present? fin

def create_invitation (email) Invitation.create (casting: @casting, sender: @sender, invitee_email: email, estado: 'pendiente') final fin "

En esta solución, no solo verá cómo esta separación de preocupaciones afecta la calidad de su código, también se lee mucho mejor y se vuelve más fácil de digerir..

Aquí delegamos métodos a una nueva clase especializada en el envío de estas invitaciones por correo electrónico. Tiene un lugar dedicado que comprueba si los mensajes y los invitados son válidos y cómo deben ser entregados. CastingInviter No es necesario que sepamos nada sobre estos detalles, por lo que delegamos estas responsabilidades a una nueva clase. CastingEmailHandler.

El conocimiento de cómo entregar y verificar la validez de estos correos electrónicos de invitación de casting ahora está contenido en nuestra nueva clase extraída. ¿Tenemos más código ahora? Usted apuesta ¿Valió la pena separar las preocupaciones? ¡Bastante seguro! ¿Podemos ir más allá y refactorizar? CastingEmailHandler ¿algo mas? ¡Absolutamente! Golpear a ti mismo!

En caso de que te estés preguntando por el válido? método en CastingEmailHandler y CastingInviter, Este es para RSpec para crear un emparejador personalizado. Esto me permite escribir algo como:

ruby expect (casting_inviter) .to be_valid

Bastante útil, creo.

Existen más técnicas para tratar con grandes clases / objetos de Dios, y en el transcurso de esta serie aprenderá un par de maneras de refactorizar tales objetos..

No hay una receta fija para tratar estos casos; siempre depende, y es una decisión de caso por caso si necesita traer las armas grandes o si las técnicas de refactorización incremental más pequeñas obligan mejor. Lo sé, un poco frustrante a veces. Sin embargo, seguir el Principio de Responsabilidad Única (SRP) recorrerá un largo camino y es un buen ejemplo..

Método largo

Tener métodos que se hicieron un poco grandes es una de las cosas más comunes que encuentra como desarrollador. En general, usted quiere saber de un vistazo qué se supone que debe hacer un método. También debe tener un solo nivel de anidamiento o un nivel de abstracción. En definitiva, evita escribir métodos complicados..

Sé que esto suena difícil, y con frecuencia lo es. Una solución que aparece con frecuencia es extraer partes del método en una o más funciones nuevas. Esta técnica de refactorización se llama método de extracción-Es uno de los más sencillos pero no obstante muy efectivo. Como un buen efecto secundario, su código se vuelve más legible si nombra sus métodos de manera adecuada.

Echemos un vistazo a las especificaciones de características donde necesitarás mucho esta técnica. Recuerdo que me introduje en el método de extracción mientras escribía esas especificaciones de características y lo increíble que se sentía cuando se encendió la bombilla. Debido a que las características de características como esta son fáciles de entender, son un buen candidato para la demostración. Además, te encontrarás con escenarios similares una y otra vez cuando escribas tus especificaciones.

spec / features / some_feature_spec.rb

"ruby require 'rails_helper'

la característica 'M marca la misión como completa' hacer el escenario 'exitosamente' hacer visit_root_path fill_in 'Correo electrónico', con: '[email protected]' click_button 'Enviar' visita misiones_path click_on 'Create Mission' fill_in 'Mission Name', con: 'Project Moonraker 'click_button' Enviar '

dentro de "li: contiene ('Project Moonraker')" do click_on 'Mission completed' end expect (página) .para have_css 'ul.missions li.mission-name.completed', texto: 'Project Moonraker' end end "

Como puede ver fácilmente, hay muchas cosas sucediendo en este escenario. Vaya a la página de índice, inicie sesión y cree una misión para la configuración, y luego haga ejercicio marcando la misión como completa y finalmente verifique el comportamiento. No es ciencia espacial, pero tampoco es limpia y definitivamente no está compuesta para reutilizarla. Podemos hacerlo mejor que eso:

spec / features / some_feature_spec.rb

"ruby require 'rails_helper'

la característica 'M marca la misión como completada' hacer el escenario 'exitosamente' do sign_in_as '[email protected]' create_classified_mission_named 'Project Moonraker'

mark_mission_as_complete 'Project Moonraker' agent_sees_completed_mission 'Project Moonraker' end end end 

def create_classified_mission_named (mission_name) visitaissions_path click_on 'Create Mission' fill_in 'Mission Name', con: mission_name click_button 'Submit' end

def mark_mission_as_complete (mission_name) dentro de "li: contiene ('# mission_name')" do click_on 'Mission completed' end end

def agent_sees_completed_mission (mission_name) expect (page) .to have_css 'ul.missions li.mission-name.completed', texto: mission_name end

def sign_in_as (email) visita root_path fill_in 'Email', con: email click_button 'Enviar' fin "

Aquí extrajimos cuatro métodos que pueden reutilizarse fácilmente en otras pruebas ahora. Espero que quede claro que golpeamos tres pájaros de un tiro. La función es mucho más concisa, se lee mejor y está compuesta de componentes extraídos sin duplicación.

Imaginemos que ha escrito todo tipo de escenarios similares sin extraer estos métodos y que desea cambiar alguna implementación. Ahora desearía haberse tomado el tiempo para refactorizar sus pruebas y tener un lugar central para aplicar sus cambios.

Claro, hay una manera aún mejor de lidiar con características de características como esta: Objetos de página, por ejemplo, pero ese no es nuestro alcance para hoy. Supongo que eso es todo lo que necesitas saber sobre los métodos de extracción. Puede aplicar este patrón de refactorización en cualquier parte de su código, no solo en las especificaciones, por supuesto. En términos de frecuencia de uso, mi conjetura es que será su técnica número uno para mejorar la calidad de su código. Que te diviertas!

Lista larga de parámetros

Cerremos este artículo con un ejemplo de cómo puede reducir sus parámetros. Se vuelve tedioso bastante rápido cuando tienes que alimentar tus métodos más de uno o dos argumentos. ¿No sería bueno dejar caer un objeto en su lugar? Eso es exactamente lo que puede hacer si introduce una objeto parametrico.

Todos estos parámetros no solo son una molestia para escribir y mantener en orden, sino que también pueden conducir a la duplicación de código, y ciertamente queremos evitar eso siempre que sea posible. Lo que me gusta especialmente de esta técnica de refactorización es cómo afecta esto también a otros métodos internos. A menudo son capaces de deshacerse de una gran cantidad de basura de parámetros en la cadena alimentaria.

Vamos a repasar este sencillo ejemplo. M puede asignar una nueva misión y necesita un nombre de misión, un agente y un objetivo. M también puede cambiar el estado de doble 0 de los agentes, es decir, su licencia para matar.

"ruby class M def assign_new_mission (mission_name, agent_name, object, licence_to_kill: nil) print" Misión # mission_name ha sido asignada a # agent_name con el objetivo a # objectivo. "si licence_ to_kill imprime" se ha otorgado ". else print" La licencia para matar no se ha otorgado ". end end end end

m = M.new m.assign_new_mission ('Octopussy', 'James Bond', 'encontrar el dispositivo nuclear', licencia_to_kill: verdadero) # => Misión Octopussy ha sido asignada a James Bond con el objetivo de encontrar el dispositivo nuclear. La licencia para matar ha sido otorgada ".

Cuando miras esto y preguntas qué sucede cuando los “parámetros” de la misión crecen en complejidad, ya estás en algo. Ese es un punto difícil que solo puede resolver si pasa en un solo objeto que tiene toda la información que necesita. La mayoría de las veces, esto también le ayuda a evitar cambiar el método si el objeto del parámetro cambia por algún motivo..

"ruby class Mission attr_reader: mission_name,: agent_name,: object,: licence_to_kill

def initialize (mission_name: mission_name, agent_name: agent_name, object: object, licence_to_kill: licence_to_kill) @mission_name = mission_name @agent_name = agent_name @objective = object @licence_to_kill = licence_to_kill end

def assign print "La misión # mission_name ha sido asignada a # agent_name con el objetivo a # objectivo." if licence_to_kill print "La licencia para matar ha sido otorgada". else print "La licencia para matar no ha sido otorgada". fin extremo fin 

clase M def assign_new_mission (misión) mission.assign end end end

m = M.new mission = Mission.new (mission_name: 'Octopussy', agent_name: 'James Bond', objetivo: 'encontrar el dispositivo nuclear', licence_to_kill: true) m.assign_new_mission (mission) # => Mission Octopussy ha sido Asignado a James Bond con el objetivo de encontrar el dispositivo nuclear. La licencia para matar ha sido otorgada ".

Así que creamos un nuevo objeto., Misión, que se centra exclusivamente en proporcionar METRO con la información necesaria para asignar una nueva misión y proporcionar #assign_new_mission con un objeto de parámetro singular. No hay necesidad de pasar en estos parámetros molestos a ti mismo. En su lugar, le dice al objeto que revele la información que necesita dentro del propio método. Además, también extrajimos algunos comportamientos (la información sobre cómo imprimir) en el nuevo Misión objeto.

Porque deberia METRO ¿Necesitas saber sobre cómo imprimir las misiones asignadas? El nuevo #asignar También se benefició de la extracción al perder algo de peso porque no tuvimos que pasar el objeto de parámetro, por lo que no hay necesidad de escribir cosas como mission.mission_name, mission.agent_name y así. Ahora solo usamos nuestro attr_reader(s), que es mucho más limpio que sin la extracción. Usted cava?

Lo que también es útil sobre esto es que Misión podría recopilar todo tipo de métodos o estados adicionales que están bien encapsulados en un solo lugar y listos para que acceda a ellos.

Con esta técnica, terminará con métodos más concisos, que tienden a leer mejor y evita repetir el mismo grupo de parámetros en todo el lugar. ¡Muy buen trato! Deshacerse de grupos de parámetros idénticos también es una estrategia importante para el código DRY.

Intente buscar la extracción de algo más que sus datos. Si también puede colocar el comportamiento en la nueva clase, tendrá objetos que son más útiles; de lo contrario, también comenzarán a oler rápidamente..

Claro, la mayoría de las veces se encontrará con versiones más complicadas de eso (y sus pruebas también deberán adaptarse simultáneamente durante tales refactorizaciones), pero si tiene ese ejemplo simple en su haber, estará listo para la acción.

Voy a ver el nuevo Bond ahora. Escuchado que no es tan bueno, aunque ...

Actualización: Saw Specter. Mi veredicto: comparado con Skyfall, que fue MEH, imho-Specter fue wawawiwa!