Advertisement
  1. Code
  2. Ruby on Rails

Olores de Código Basico 01 de Ruby/Rails

Scroll to top
Read Time: 17 min
This post is part of a series called Ruby / Rails Code Smell Basics.
Ruby/Rails Code Smell Basics 02

() translation by (you can also view the original English article)

filefilefile

Temas

  • Aviso
  • Resistencia
  • Clase grande / Clase dios
  • Extracto de clase
  • Método largo
  • Lista de parámetros larga

Aviso

La breve serie de artículos siguiente está pensada para los desarrolladores y los arrancadores Ruby ligeramente experimentados por igual. Tuve la impresión de que los olores de código y sus refactorings pueden ser muy desalentadores e intimidantes para los principiantes, especialmente si no están en la posición afortunada de tener mentores que pueden convertir conceptos de programación mística en bombillas brillantes.

Habiendo caminado obviamente en estos zapatos yo, recordé que sentía innecesariamente niebla para entrar en los olores del código y refactorizaciones.

Por un lado, los autores esperan un cierto nivel de competencia y por lo tanto no se sienten super obligados a proporcionar 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 dan 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 les ayudará a diseñar mejor software antes en su carrera. Por lo menos espero que ayude a proporcionar junior peeps con una sólida ventaja.

Entonces, ¿de qué estamos hablando exactamente cuando la gente menciona los olores del código? ¿Siempre es un problema en su código? ¡No necesariamente! ¿Puedes evitarlos completamente? ¡No lo creo! ¿Quiere decir que los olores de código conducen a un código roto? Bueno, a veces ya veces no. ¿Debería ser mi prioridad arreglarlos de inmediato? Misma respuesta, me temo: a veces sí ya veces hay que fritar pescado más grande primero. ¿Estas loco? ¡Pregunta justa en este punto!

Antes de seguir buceando en este negocio entero maloliente, recuerde quitarle una cosa de todo esto: No trate de arreglar cada olor que encuentre, ¡sin duda es 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, los diferentes lenguajes de programación y marcos son propensos a diferentes tipos de olores, pero definitivamente hay muchas variedades genéticas comunes entre ellas. Mi intento de describir los olores del código es compararlos con síntomas médicos que le dicen que usted podría tener un problema. Pueden apuntar a todo tipo de problemas latentes y tienen una amplia variedad de soluciones si se diagnostican.

Afortunadamente, en general no son tan complicadas como tratar con el cuerpo humano y la psique, por supuesto. Es una comparación justa, sin embargo, porque algunos de estos síntomas deben ser tratados de inmediato, y algunos otros le dan tiempo suficiente para llegar a una solución que es mejor para el "paciente" bienestar general. Si tienes código de trabajo y te encuentras con algo maloliente, tendrás que tomar la decisión difícil si vale la pena el tiempo para encontrar una solución y si esa refactorización mejora la estabilidad de tu aplicación.

Dicho esto, si se tropieza con el código que se puede mejorar de inmediato, es un buen consejo para dejar el código detrás de un poco mejor que antes, incluso un poquito mejor suma considerablemente 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, como decidir dónde colocar código nuevo es un dolor o viene con una gran cantidad de efectos de ripple a través de su base de código, por ejemplo. Esto se llama resistencia.

Como 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, es definitivamente el momento de refactorizar y tomar la última parte de RED-green-REFACTOR más seriamente en el futuro.

Clase grande / clase dios

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

¿Seguramente recuerda el mantra "modelos gordos, controlador flaco"? Bueno, en realidad, flaco es bueno para todas estas clases, pero como una pauta 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 usuales con más frecuencia incluyen el modelo de usuario y cualquier problema (esperemos!) Que su aplicación intenta resolver, primero y más importante al menos. Una aplicación de todo podría agruparse en el modelo de Todos, una aplicación de compras en Productos, una aplicación de fotos en Fotos: se entiende .

La gente las llama clases de dios porque saben demasiado. Tienen demasiadas conexiones con otras clases, sobre todo porque alguien las estaba modelando perezosamente. Es un trabajo duro, sin embargo, mantener las clases de dios bajo control. Ellos hacen realmente fácil de volcar más responsabilidades sobre ellos, y como muchos héroes griegos atestiguan, se necesita un poco de habilidad para dividir y conquistar "dioses".

El problema con ellos es que se vuelven más difíciles y difíciles de entender, especialmente para los nuevos miembros del equipo, más difícil de cambiar, y reutilizarlos se convierte cada vez menos en una opción más gravedad que han acumulado. Oh sí, tienes razón, tus pruebas son innecesariamente más difíciles de escribir también. En resumen, no hay realmente una ventaja de tener grandes clases, y las clases de dios en particular.

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

  • ¡Tienes que desplazarte!
  • Toneladas de métodos privados?
  • ¿Su clase tiene siete o más métodos en él?
  • Difícil decir lo que su clase realmente hace-¡de forma concisa!
  • ¿Su clase tiene muchas razones para cambiar cuando el código evoluciona?

Además, si usted mira a su clase y piensa "Eh? ¡Ew! ", Puede ser que también te encuentres con algo. Si todo lo que suena familiar, las posibilidades son buenas que te has encontrado un buen ejemplar.

1
class CastingInviter
2
  EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
3
4
  attr_reader :message, :invitees, :casting
5
6
  def initialize(attributes = {})
7
    @message  = attributes[:message]  || ''
8
	  @invitees = attributes[:invitees] || ''
9
	  @sender   = attributes[:sender]
10
	  @casting  = attributes[:casting]
11
	end
12
13
	def valid?
14
	  valid_message? && valid_invitees?
15
	end
16
17
	def deliver
18
    if valid?
19
      invitee_list.each do |email|
20
        invitation = create_invitation(email)
21
        Mailer.invitation_notification(invitation, @message)
22
      end
23
	  else
24
	    failure_message  = "Your #{ @casting } message couldn’t be sent. Invitees emails or message are invalid"
25
	    invitation = create_invitation(@sender)
26
      Mailer.invitation_notification(invitation, failure_message )
27
	  end
28
	end
29
30
	private
31
32
	def invalid_invitees
33
	  @invalid_invitees ||= invitee_list.map do |item|
34
      unless item.match(EMAIL_REGEX)
35
	      item
36
	    end
37
	  end.compact
38
	end
39
40
	def invitee_list
41
	  @invitee_list ||= @invitees.gsub(/\s+/, '').split(/[\n,;]+/)
42
	end
43
44
	def valid_message?
45
	  @message.present?
46
	end
47
48
	def valid_invitees?
49
	  invalid_invitees.empty?
50
	end
51
52
  def create_invitation(email)
53
    Invitation.create(
54
      casting:       @casting,
55
      sender:        @sender,
56
      invitee_email: email,
57
      status:        'pending'
58
    )
59
  end
60
end

Amigo feo, ¿eh? ¿Puedes ver cuánta nastiness está incluido aquí? Por supuesto que poner un poco de cereza en la parte superior, pero se ejecutará en el código de este tipo tarde o temprano. Pensemos en las responsabilidades que tiene esta clase CastingInviter para hacer.   

  • Envío de correo electrónico
  • Comprobación de mensajes y direcciones de correo electrónico válidos
  • Deshacerse de los espacios en blanco
  • División de direcciones de correo electrónico en comas y puntos y comas

¿Debería todo esto ser descargado en una clase que sólo quiere entregar una llamada de casting a través de  deliver? ¡Ciertamente no! Si su método de invitación cambia usted puede esperar para ejecutar en alguna shotgun surgery. CastingInviter no necesita conocer la mayoría de estos detalles. Eso es más la responsabilidad de alguna clase que está especializada en tratar con cosas relacionadas con el correo electrónico. En el futuro, encontrarás muchas razones para cambiar tu código aquí también.

Extracto de clase

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

Métodos privados suelen ser buenos candidatos para comenzar con-y marcas fáciles también. A veces necesitarás extraer incluso más de una clase de un niño 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 que puede extraer esa funcionalidad en una nueva clase.

Crear una nueva clase y mover gradualmente la funcionalidad de uno a uno. Mueva cada método por separado y cambie el nombre si ve una razón. A continuación, haga referencia a la nueva clase en la original y delegue la funcionalidad necesaria. Lo bueno es que tengas cobertura de prueba (¡ojalá!) Que te permita comprobar si las cosas siguen funcionando correctamente cada paso del camino. Trate de ser capaz de reutilizar sus clases extraídas también. Es más fácil ver cómo se hace en acción, así que leamos un poco de código:

1
class CastingInviter
2
3
  attr_reader :message, :invitees, :casting
4
5
  def initialize(attributes = {})
6
    @message  = attributes[:message]  || ''
7
    @invitees = attributes[:invitees] || ''
8
    @casting  = attributes[:casting]
9
    @sender   = attributes[:sender]
10
  end
11
  
12
  def valid?
13
    casting_email_handler.valid?
14
  end
15
  
16
  def deliver
17
    casting_email_handler.deliver
18
  end
19
  
20
  private
21
  
22
  def casting_email_handler
23
    @casting_email_handler ||= CastingEmailHandler.new(
24
      message:  message, 
25
      invitees: invitees, 
26
      casting:  casting, 
27
      sender:   @sender
28
    )
29
  end
30
end
1
class CastingEmailHandler 
2
  EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
3
4
  def initialize(attr = {})
5
    @message  = attr[:message]  || ''
6
    @invitees = attr[:invitees] || ''
7
    @casting  = attr[:casting]
8
    @sender   = attr[:sender]
9
  end
10
11
  def valid?
12
    valid_message? && valid_invitees?
13
  end
14
15
  def deliver
16
    if valid?
17
      invitee_list.each do |email|
18
        invitation = create_invitation(email)
19
        Mailer.invitation_notification(invitation, @message)
20
      end
21
    else
22
      failure_message  = "Your #{ @casting } message couldn’t be sent. Invitees emails or message are invalid"
23
      invitation = create_invitation(@sender)
24
      Mailer.invitation_notification(invitation, failure_message )
25
    end
26
  end
27
28
  private
29
30
  def invalid_invitees
31
    @invalid_invitees ||= invitee_list.map do |item|
32
      unless item.match(EMAIL_REGEX)
33
        item
34
      end
35
    end.compact
36
  end
37
38
  def invitee_list
39
    @invitee_list ||= @invitees.gsub(/\s+/, '').split(/[\n,;]+/)
40
  end
41
  
42
  def valid_invitees?
43
    invalid_invitees.empty?
44
  end
45
  
46
  def valid_message?
47
    @message.present?
48
  end
49
50
  def create_invitation(email)
51
    Invitation.create(
52
      casting:       @casting,
53
      sender:        @sender,
54
      invitee_email: email,
55
      status:        'pending'
56
    )
57
  end
58
end

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

Aquí delegamos métodos a una nueva clase que está especializada en tratar con la entrega de estas invitaciones por correo electrónico. Usted tiene un lugar dedicado que comprueba si los mensajes y los invitados son válidos y cómo deben ser entregados. CastingInviter no necesita saber nada sobre estos detalles, así 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á todo contenido en nuestra nueva clase extraída. ¿Tenemos más código ahora? ¡Usted apuesta! ¿Vale la pena separar las preocupaciones? ¡Bastante seguro! ¿Podemos ir más allá y refactorizar CastingEmailHandler un poco más? ¡Absolutamente! ¡Aproveche!

En caso de que usted se está preguntando sobre el valid? en CastingEmailHandler y CastingInviter, éste es para RSpec crear un matcher personalizado. Esto me permite escribir algo como:

1
expect(casting_inviter).to be_valid

Muy práctico, creo.

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

No hay una receta fija para tratar con estos casos, siempre depende, y es una llamada de juicio caso por caso si necesita traer las armas grandes o si las técnicas de refactorización incremental más pequeñas lo obligan mejor. Lo sé, un poco frustrante a veces. Seguir el Principio de Responsabilidad Única (SRP) será un largo camino, sin embargo, y es una buena nariz para seguir.

Método largo

Tener los métodos que consiguió un poco grande es una de las cosas más comunes que usted encuentra como desarrollador. En general, usted quiere saber de un vistazo lo que se supone que un método debe hacer. También debe tener sólo un nivel de anidamiento o un nivel de abstracción. En resumen, evite escribir métodos complicados.

Sé que esto suena duro, y lo es a menudo. Una solución que surge frecuentemente es extraer partes del método en una o más funciones nuevas. Esta técnica de refactorización se llama el extracto de método -es uno de los más simples pero no obstante muy eficaz. Como un efecto secundario agradable, su código se vuelve más legible si usted nombra sus métodos apropiadamente.

Echemos un vistazo a especificaciones de características donde necesitará esta técnica mucho. Recuerdo haber introducido al extracto de método al escribir tales especificaciones de la característica y cómo él se sentía cuando la bombilla prendió. Debido a que las especificaciones de características como esta son fáciles de entender, son un buen candidato para la demostración. Además, se encontrará con escenarios similares una y otra vez cuando escriba sus especificaciones.

spec/features/some_feature_spec.rb

1
require 'rails_helper'
2
3
feature 'M marks mission as complete' do
4
  scenario 'successfully' do
5
    visit_root_path
6
    fill_in      'Email', with: 'M@mi6.com'
7
    click_button 'Submit'
8
    visit missions_path
9
    click_on     'Create Mission' 
10
    fill_in      'Mission Name', with: 'Project Moonraker'
11
    click_button 'Submit'
12
13
    within "li:contains('Project Moonraker')" do
14
      click_on 'Mission completed'
15
    end
16
17
    expect(page).to have_css 'ul.missions li.mission-name.completed', text: 'Project Moonraker'
18
  end
19
end

Como se puede ver fácilmente, hay mucho que hacer en este escenario. Vaya a la página de índice, inicie sesión y cree una misión para la configuración y, a continuación, realice ejercicio marcando la misión como completa y, finalmente, verifique el comportamiento. No ciencia de cohetes, pero también no limpio y definitivamente no está compuesto para la reutilización. Podemos hacerlo mejor que eso:

spec/features/some_feature_spec.rb

1
require 'rails_helper'
2
3
feature 'M marks mission as complete' do
4
  scenario 'successfully' do
5
    sign_in_as 'M@mi6.com'
6
    create_classified_mission_named 'Project Moonraker'
7
  
8
    mark_mission_as_complete        'Project Moonraker'
9
  
10
    agent_sees_completed_mission    'Project Moonraker'
11
  end
12
end
13
  
14
def create_classified_mission_named(mission_name)
15
	visit missions_path
16
	click_on     'Create Mission' 
17
	fill_in      'Mission Name', with: mission_name
18
	click_button 'Submit'
19
	end
20
21
def mark_mission_as_complete(mission_name)
22
	within "li:contains('#{mission_name}')" do
23
	  click_on 'Mission completed'
24
	end
25
end
26
27
def agent_sees_completed_mission(mission_name)
28
	expect(page).to have_css 'ul.missions li.mission-name.completed', text: mission_name
29
end
30
31
def sign_in_as(email)
32
	visit root_path
33
	fill_in      'Email', with: email
34
	click_button 'Submit'
35
end

Aquí hemos extraído cuatro métodos que pueden ser fácilmente reutilizados en otras pruebas ahora. Espero que sea claro que golpeamos tres pájaros con una piedra. La característica es mucho más concisa, se lee mejor, y se compone de componentes extraídos sin duplicación.

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

Claro, hay una manera aún mejor de manejar especificaciones de características como este -Page Objects, por ejemplo- pero eso no es nuestro alcance para hoy. Supongo que eso es todo lo que necesita saber sobre métodos de extracción. Puede aplicar este patrón de refactorización en todas partes de su código, no sólo en las especificaciones, por supuesto. En términos de frecuencia de uso, mi conjetura es que será su número una técnica para mejorar la calidad de su código. ¡Que te diviertas!

Lista de parámetros larga

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

Todos estos parámetros son no sólo un dolor de escribir y mantener en orden, pero también puede conducir a la duplicación de código y, sin duda, queremos evitar que siempre que sea posible. Lo que más me gusta de esta técnica de refactorización es cómo esto afecta a otros métodos dentro también. A menudo son capaces de deshacerse de un montón de basura de parámetros en la cadena alimentaria.

Veamos 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 es capaz de cambiar el estado de doble 0 de los agentes, es decir, su licencia para matar.

1
class M
2
  def assign_new_mission(mission_name, agent_name, objective, licence_to_kill: nil)
3
    print "Mission #{mission_name} has been assigned to #{agent_name} with the objective to #{objective}."
4
    if licence_to_kill
5
      print " The licence to kill has been granted."
6
    else
7
      print "The licence to kill has not been granted."
8
    end
9
  end
10
end
11
12
m = M.new
13
m.assign_new_mission('Octopussy', 'James Bond', 'find the nuclear device', licence_to_kill: true)
14
# => Mission Octopussy has been assigned to James Bond with the objective to find the nuclear device. The licence to kill has been granted. 

Cuando miras esto y pregunta qué sucede cuando los "parámetros" de la misión crecen en complejidad, ya estás en algo. Ese es un punto de dolor que sólo se puede resolver si se pasa en un solo objeto que tiene toda la información que necesita. Más a menudo que no, esto también le ayuda a mantenerse alejado de cambiar el método si el objeto de parámetro cambia por alguna razón.

1
class Mission
2
  attr_reader :mission_name, :agent_name, :objective, :licence_to_kill
3
4
  def initialize(mission_name: mission_name, agent_name: agent_name, objective: objective, licence_to_kill: licence_to_kill)
5
	  @mission_name    = mission_name
6
	  @agent_name      = agent_name
7
	  @objective       = objective
8
	  @licence_to_kill = licence_to_kill
9
	end
10
11
	def assign
12
	  print "Mission #{mission_name} has been assigned to #{agent_name} with the objective to #{objective}."
13
	  if licence_to_kill
14
	    print " The licence to kill has been granted."
15
	  else
16
	    print " The licence to kill has not been granted."
17
	  end
18
	end
19
end
20
21
class M
22
  def assign_new_mission(mission)
23
	  mission.assign
24
	end
25
end
26
27
m = M.new
28
mission = Mission.new(mission_name: 'Octopussy', agent_name: 'James Bond', objective: 'find the nuclear device', licence_to_kill: true)
29
m.assign_new_mission(mission)
30
# => Mission Octopussy has been assigned to James Bond with the objective to find the nuclear device. The licence to kill has been granted.

Así que creamos un nuevo objeto, Mission, que se centra únicamente en proporcionar a M 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 usted mismo. En su lugar, le dices al objeto que revele la información que necesitas dentro del método mismo. Además, también extraímos un cierto comportamiento -la información de cómo imprimir- en el nuevo objeto Mission.

¿Por qué M necesita saber cómo imprimir las misiones? El nuevo #assign también se benefició de la extracción al perder algo de peso porque no necesitamos pasar el parámetro objeto, por lo que no es necesario escribir cosas como mission.mission_name, mission.agent_name y así sucesivamente. Ahora solo usamos nuestro attr_reader (s), que es mucho más limpio que sin la extracción. ¿Entiendes?

Lo que también es útil es que Mission puede recopilar todo tipo de métodos o estados adicionales que están bien encapsulados en un lugar y listos para que usted pueda acceder.

Con esta técnica se terminan con métodos que son más concisos, tienden a leer mejor, y evitar la repetición del mismo grupo de parámetros en todo el lugar. ¡Bastante buena oferta! Deshacerse de grupos idénticos de parámetros también es una estrategia importante para el código DRY.

Trate de extraer más que sólo sus datos. Si puedes colocar el comportamiento en la nueva clase también, tendrás objetos que son más útiles, de lo contrario empezarán a oler rápidamente también.

Claro, la mayor parte del tiempo se encontrará con versiones más complicadas de eso -y sus pruebas también necesitarán ser adaptadas simultáneamente durante tales refactorizaciones- pero si usted tiene ese ejemplo simple bajo su cinturón, estará listo para la acción.

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

Actualización: Espectro de la sierra. Mi veredicto: en comparación con Skyfall-que fue MEH imho-Spectre fue wawawiwa!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.