Advertisement
  1. Code
  2. Ruby

Fundamentos de los antipatrones: Modelos Rails

Scroll to top
Read Time: 24 min

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

¿Anti qué? Probablemente suene mucho más complicado de lo que es. A lo largo de las dos últimas décadas, los programadores han sido capaces de identificar una útil selección de patrones de "diseño" que aparecen con frecuencia en sus soluciones de código. Mientras resolvían problemas similares, eran capaces de "clasificar" soluciones que les evitaban reinventar la rueda todo el tiempo. Es importante señalar que estos patrones deben considerarse más como descubrimientos que como inventos de un grupo de desarrolladores avanzados.

Si esto es bastante nuevo para ti y te ves más en el lado de los principiantes en todo lo relacionado con Ruby/Rails, entonces este está escrito exactamente para ti. Creo que lo mejor es que lo veas como una rápida inmersión en un tema mucho más profundo cuyo dominio no se producirá de la noche a la mañana. No obstante, creo firmemente que empezar a adentrarse en esto desde el principio beneficiará enormemente a los principiantes y a sus mentores.

Los antipatrones, como su nombre indica, representan prácticamente lo contrario de los patrones. Son descubrimientos de soluciones a problemas que definitivamente debes evitar. A menudo representan el trabajo de codificadores sin experiencia que no saben lo que no saben todavía. Peor aún, podrían ser el resultado de una persona perezosa que simplemente ignora las mejores prácticas y los marcos de trabajo de las herramientas sin una buena razón, o cree que no los necesita. Lo que pueden esperar ganar en ahorro de tiempo al principio, al elaborar soluciones rápidas, perezosas o sucias, les va a perseguir a ellos o a algún lamentable sucesor más adelante en el ciclo de vida del proyecto.

No subestimes las implicaciones de estas malas decisiones: te van a acosar, pase lo que pase.

Temas

  • Modelos de grasa
  • Falta el conjunto de pruebas
  • Modelos voyeuristas
  • Ley de Deméter
  • Espagueti SQL

Modelos gordos

Estoy seguro de que escuchaste la canción "Modelos gordos, controladores flacos" montones de veces cuando empezaste con Rails. Bien, ¡ahora olvídate de eso! Por supuesto, la lógica de negocio debe resolverse en la capa del modelo, pero no deberías sentirte inclinado a meter todo allí sin sentido sólo para evitar cruzar las líneas hacia el territorio del controlador.

Aquí hay un nuevo objetivo al que deberías apuntar: "Modelos delgados, controladores delgados". Te preguntarás: "Bueno, ¿y cómo deberíamos organizar el código para conseguirlo, después de todo, es un juego de suma cero?". ¡Buena pregunta! El nombre del juego es composición, y Ruby está bien equipado para ofrecerte muchas opciones para evitar la obesidad de los modelos.

En la mayoría de las aplicaciones web (Rails) respaldadas por bases de datos, la mayor parte de tu atención y trabajo se centrará en la capa del modelo, siempre que trabajes con diseñadores competentes que sean capaces de implementar sus propias cosas en la vista, quiero decir. Tus modelos tendrán inherentemente más "gravedad" y atraerán más complejidad.

La cuestión es cómo pretende gestionar esa complejidad. Active Record te ofrece, sin duda, mucha cuerda para ahorcarte al tiempo que te hace la vida increíblemente fácil. Es un enfoque tentador diseñar tu capa del modelo siguiendo simplemente el camino de mayor conveniencia inmediata. Sin embargo, una arquitectura preparada para el futuro requiere mucha más consideración que cultivar clases enormes y meter todo en objetos de Active Record.

El verdadero problema con el que te enfrentas aquí es la complejidad, innecesaria, diría yo. Las clases que acumulan toneladas de código se vuelven complejas solo por su tamaño. Son más difíciles de mantener, difíciles de analizar y entender, y cada vez más difíciles de cambiar porque su composición probablemente carece de desacoplamiento. Estos modelos a menudo superan su capacidad recomendada para manejar una sola responsabilidad y están bastante desbordados. En el peor de los casos, se convierten en camiones de basura, manejando toda la basura que se les arroja con desidia.

Podemos hacerlo mejor. Si crees que la complejidad no es un gran problema, después de todo, eres especial, inteligente y todo, ¡piensa de nuevo! La complejidad es el asesino en serie de proyectos más notorio que existe, y no tu amistoso "Defensor de la oscuridad".

Los "modelos más delgados" consiguen una cosa que la gente avanzada en el negocio de la codificación (probablemente muchas más profesiones que el código y el diseño) aprecia y por la que todos deberíamos esforzarnos absolutamente: la simplicidad. O al menos más de ella, lo cual es un compromiso justo si la complejidad es difícil de erradicar.

¿Qué herramientas ofrece Ruby para facilitarnos la vida en ese sentido y permitirnos recortar la grasa de nuestros modelos? Sencillo, otras clases y módulos. Identificas un código coherente que podrías extraer en otro objeto y así construir una capa de modelo que consiste en agentes de tamaño razonable que tienen sus propias responsabilidades únicas y distintivas.

Piensa en ello en términos de un artista con talento. En la vida real, esa persona podría ser capaz de rapear, romper, escribir letras y producir sus propias melodías. En la programación, prefieres la dinámica de una banda, aquí con al menos cuatro miembros distintos, en la que cada persona se encarga del menor número de cosas posible. Quieres construir una orquesta de clases que pueda manejar la complejidad del compositor, no una clase de maestro genio microgestionador de todos los oficios.

one man bandone man bandone man band

Veamos un ejemplo de un modelo gordo y juguemos con un par de opciones para manejar su obesidad. El ejemplo es ficticio, por supuesto, y al contar esta tonta historia espero que sea más fácil de digerir y seguir para los novatos.

Tenemos una clase de Spectre que tiene demasiadas responsabilidades y, por lo tanto, ha crecido innecesariamente. Además de estos métodos, creo que es fácil imaginar que un espécimen de este tipo ya acumulaba muchas otras cosas como las que representan los tres puntitos. Spectre va camino de convertirse en una clase divina. (¡Las probabilidades de volver a formular una frase así con sensatez son bastante bajas!)

1
class Spectre < ActiveRecord::Base
2
  has_many :spectre_members
3
  has_many :spectre_agents
4
  has_many :enemy_agents
5
  has_many :operations
6
7
  ...
8
9
  def turn_mi6_agent(enemy_agent)
10
    puts "MI6 agent #{enemy_agent.name} turned over to Spectre"
11
  end
12
13
  def turn_cia_agent(enemy_agent)
14
    puts "CIA agent #{enemy_agent.name} turned over to Spectre"
15
  end
16
17
  def turn_mossad_agent(enemy_agent)
18
    puts "Mossad agent #{enemy_agent.name} turned over to Spectre"
19
  end
20
21
  def kill_double_o_seven(spectre_agent)
22
    spectre_agent.kill_james_bond
23
  end
24
25
  def dispose_of_cabinet_member(number)
26
    spectre_member = SpectreMember.find_by_id(number)
27
28
    puts "A certain culprit has failed the absolute integrity of this fraternity. The appropriate act is to smoke number #{number} in his chair. His services won’t be greatly missed"
29
    spectre_member.die
30
  end
31
32
  def print_assignment(operation)
33
    puts "Operation #{operation.name}’s objective is to #{operation.objective}."
34
  end
35
36
  private
37
38
  def enemy_agent
39
    #clever code
40
  end
41
42
  def spectre_agent
43
    #clever code
44
  end
45
46
  def operation
47
    #clever code
48
  end
49
50
  ...
51
52
end
53

Spectre convierte a varios tipos de agentes enemigos, delega el asesinato de 007, interroga a los miembros del gabinete de Spectre cuando fallan, y también imprime asignaciones operativas. Un caso claro de microgestión y definitivamente una violación del "principio de responsabilidad única". Los métodos privados también se acumulan rápidamente.

Esta clase no necesita conocer la mayoría de las cosas que hay actualmente en ella. Dividiremos esta funcionalidad en un par de clases y veremos si la complejidad de tener un par de clases/objetos más vale la pena la liposucción.

1
class Spectre < ActiveRecord::Base
2
  has_many :spectre_members
3
  has_many :spectre_agents
4
  has_many :enemy_agents
5
  has_many :operations
6
7
  ...
8
9
  def turn_enemy_agent
10
    Interrogator.new(enemy_agent).turn
11
  end
12
13
  private 
14
15
  def enemy_agent
16
    self.enemy_agents.last
17
  end
18
end
19
20
class Interrogator
21
  attr_reader :enemy_agent
22
  
23
  def initialize(enemy_agent)
24
    @enemy_agent = enemy_agent
25
  end
26
27
  def turn
28
    enemy_agent.turn
29
  end
30
end
31
32
class EnemyAgent < ActiveRecord::Base
33
  belongs_to :spectre
34
  belongs_to :agency
35
36
  def turn
37
    puts 'After extensive brainwashing, torture and hoards of cash…'
38
  end
39
end
40
41
class MI6Agent < EnemyAgent
42
  def turn
43
    super
44
    puts "MI6 agent #{name} turned over to Spectre"
45
  end
46
end
47
48
class CiaAgent < EnemyAgent
49
  def turn
50
    super
51
    puts "CIA agent #{name} turned over to Spectre"
52
  end
53
end
54
55
class MossadAgent < EnemyAgent
56
  def turn
57
    super
58
    puts "Mossad agent #{name} turned over to Spectre"
59
  end
60
end
61
62
class NumberOne < ActiveRecord::Base
63
  def dispose_of_cabinet_member(number)
64
    spectre_member = SpectreMember.find_by_id(number)
65
66
    puts "A certain culprit has failed the absolute integrity of this fraternity. The appropriate act is to smoke number #{number} in his chair. His services won’t be greatly missed"
67
    spectre_member.die
68
  end
69
end
70
71
class Operation < ActiveRecord::Base
72
  has_many :spectre_agents
73
  belongs_to :spectre
74
75
  def print_assignment
76
    puts "Operation #{name}’s objective is to #{objective}."
77
  end
78
end
79
80
class SpectreAgent < ActiveRecord::Base
81
  belongs_to :operation
82
  belongs_to :spectre
83
84
  def kill_james_bond
85
    puts "Mr. Bond, I expect you to die!"
86
  end
87
end
88
89
class SpectreMember < ActiveRecord::Base
90
  belongs_to :spectre
91
  
92
  def die
93
    puts "Nooo, nooo, it wasn’t meeeeeeeee! ZCHUNK!"
94
  end
95
end
96

Creo que la parte más importante a la que debes prestar atención es cómo utilizamos una clase Ruby simple como Interrogator para manejar el giro de los agentes de diferentes agencias. Los ejemplos del mundo real podrían representar un convertidor que, por ejemplo, transforme un documento HTML en un pdf y viceversa. Si no necesitas toda la funcionalidad de las clases de Active Record, ¿por qué utilizarlas si una simple clase de Ruby también puede servir? Un poco menos de cuerda para ahorcarnos.

La clase Spectre deja el desagradable asunto de convertir a los agentes a la clase Interrogador y solo delega en ella. Ésta tiene ahora la única responsabilidad de torturar y lavar el cerebro a los agentes capturados.

Hasta aquí todo bien. Pero, ¿por qué hemos creado clases distintas para cada agente? Muy sencillo. En lugar de extraer directamente los diversos métodos de giro como turn_mi6_agent a Interrogator, les dimos un mejor hogar en su propia clase respectiva.

Como resultado, podemos hacer un uso efectivo del polimorfismo y no nos preocupamos por los casos individuales de los agentes de giro. Solo le decimos a estos diferentes objetos agentes que giren, y cada uno de ellos sabe lo que tiene que hacer. El Interrogador no necesita saber los detalles de cómo gira cada agente.

Como todos estos agentes son objetos de Active Record, creamos uno genérico, EnemyAgent, que tiene un sentido general de lo que significa girar un agente, y encapsulamos esa parte para todos los agentes en un solo lugar subclasificándolo. Hacemos uso de esta herencia suministrando los métodos de turn de los diversos agentes con super, y por lo tanto tenemos acceso al negocio de lavado de cerebro y tortura, sin duplicación. Las responsabilidades únicas y la no duplicación son un buen punto de partida para seguir adelante.

Las otras clases de Registro Activo asumen diversas responsabilidades de las que Spectre no necesita preocuparse. El "Número Uno" suele encargarse él mismo de interrogar a los miembros fallidos del gabinete de Spectre, así que ¿por qué no dejar que un objeto dedicado se encargue de la electrocución? Por otro lado, los miembros de Spectre que fracasan saben cómo morir ellos mismos al ser fumados en su silla por NumberOneOperation ahora imprime sus asignaciones por sí misma, sin necesidad de hacer perder el tiempo a Spectre con cacahuetes como ese.

Por último, pero no por ello menos importante, matar a James Bond suele intentarlo un agente sobre el terreno, por lo que kill_james_bond es ahora un método en SpectreAgent. Goldfinger lo habría manejado de forma diferente, por supuesto, supongo que hay que jugar con el láser si se tiene uno.

Como puedes ver claramente, ahora tenemos diez clases donde antes solo teníamos una. ¿No es demasiado? Puede serlo, sin duda. Es una cuestión con la que tendrás que luchar la mayor parte del tiempo cuando repartas esas responsabilidades. Sin duda, puedes pasarte de la raya. Pero verlo desde otro punto de vista puede ayudar:

  • ¿Hemos separado las preocupaciones? ¡Por supuesto!
  • ¿Tenemos clases ligeras y delgadas que se adaptan mejor a las responsabilidades singulares? ¡Seguro que sí!
  • ¿Contamos una "historia", pintamos una imagen más clara de quién está involucrado y es responsable de ciertas acciones? ¡Eso espero!
  • ¿Es más fácil digerir lo que hace cada clase? ¡Por supuesto!
  • ¿Hemos reducido el número de métodos privados? ¡Sí!
  • ¿Representa esto una mejor calidad de la programación orientada a objetos? Dado que hemos utilizado la composición y nos hemos referido a la herencia solo cuando era necesario para configurar estos objetos, ¡por supuesto!
  • ¿Se siente más limpio? ¡Sí!
  • ¿Estamos mejor equipados para cambiar nuestro código sin hacer un desastre? ¡Claro que sí!
  • ¿Mereció la pena? ¿Qué opinas?

No quiero decir que haya que tachar estas preguntas de la lista cada vez, pero estas son las cosas que probablemente deberías empezar a preguntarte mientras adelantas tus modelos.

Diseñar modelos flacos puede ser difícil, pero es una medida esencial para mantener tus aplicaciones sanas y ágiles. Estas tampoco son las únicas formas constructivas de tratar con modelos gordos, pero son un buen comienzo, especialmente para los novatos.

Falta el conjunto de pruebas

Este es probablemente el antipatrón más obvio. Viniendo del lado de las pruebas, tocar una aplicación madura que no tiene cobertura de pruebas puede ser una de las experiencias más dolorosas de encontrar. Si quieres odiar al mundo y a tu propia profesión por encima de todo, sólo tienes que dedicar seis meses a un proyecto de este tipo, y aprenderás cuánto de misántropo hay potencialmente en ti. Es una broma, por supuesto, pero dudo que te haga más feliz y que quieras volver a hacerlo. Tal vez una semana sirva también. Estoy bastante seguro de que la palabra tortura aparecerá en tu mente más a menudo de lo que crees.

Si las pruebas no formaban parte de tu proceso hasta ahora y ese tipo de dolor se siente como algo normal en tu trabajo, tal vez deberías considerar que las pruebas no son tan malas, ni son tu enemigo. Cuando tus niveles de alegría relacionados con el código son más o menos constantes por encima de cero y puedes cambiar sin miedo tu código, entonces la calidad general de tu trabajo será mucho mayor en comparación con el resultado que está contaminado por la ansiedad y el sufrimiento.

¿Estoy sobrestimando? ¡Realmente no lo creo! Quieres tener una cobertura de pruebas muy extensa, no solo porque es una gran herramienta de diseño para escribir solo el código que realmente necesitas, sino también porque necesitarás cambiar tu código en algún momento en el futuro. Estarás mucho mejor equipado para comprometerte con tu código, y mucho más confiado, si tienes un arnés de pruebas que ayude y guíe las refactorizaciones, el mantenimiento y las extensiones. Seguro que se producirán en el futuro, no hay duda de ello.

Este es también el punto en el que un conjunto de pruebas empieza a dar la segunda vuelta de los dividendos, porque la mayor velocidad con la que se pueden hacer de forma segura estos cambios de calidad no se puede conseguir ni de lejos en las aplicaciones que están hechas por personas que piensan que escribir pruebas es una tontería o lleva demasiado tiempo.

Modelos voyeuristas

Se trata de modelos que son súper entrometidos y quieren reunir demasiada información sobre otros objetos o modelos. Esto contrasta con una de las ideas más fundamentales de la programación orientada a objetos: la encapsulación. Más bien queremos esforzarnos por conseguir clases y modelos autocontenidos que gestionen sus asuntos internos por sí mismos en la medida de lo posible. En términos de conceptos de programación, estos modelos voyeuristas básicamente violan el "Principio del Menor Conocimiento", también conocido como la "Ley de Deméter", como se quiera pronunciar.

Ley de Demeter

¿Por qué es un problema? Es una forma de duplicación, muy sutil, y también conduce a un código mucho más frágil de lo previsto.

La Ley de Demeter es prácticamente el olor a código más fiable que siempre puedes atacar sin preocuparte por los posibles inconvenientes.

Supongo que llamar a éste "ley" no es tan pretencioso como podría sonar al principio. Híncale el diente a este olor, porque lo vas a necesitar mucho en tus proyectos. Básicamente establece que en términos de objetos, puedes llamar a los métodos del amigo de tu objeto pero no al amigo de tu amigo.

Esta es una forma común de explicarlo, y todo se reduce a no usar más que un solo punto para tus llamadas a métodos. Por cierto, está totalmente bien usar más puntos o llamadas a métodos cuando se trata de un solo objeto que no intenta llegar más allá. Algo como @weapons.find_by_name('Poison dart').formula está bien. Los buscadores pueden acumular bastantes puntos a veces. Encapsularlos en métodos dedicados es, sin embargo, una buena idea.

Violaciones de la Ley de Demeter

Veamos un par de malos ejemplos de las clases anteriores:

1
@operation.spectre_agents.first.kill_james_bond
2
3
@spectre.operations.last.spectre_agents.first.name
4
5
@spectre.enemy_agents.last.agency.name
6

Para que te hagas una idea, aquí tienes otras ficticias:

1
@quartermaster.gizmos.non_lethal.favorite
2
3
@mi6.operation.agent.favorite_weapon
4
5
@mission.agent.name
6

Plátanos, ¿verdad? No tiene buena pinta, ¿verdad? Como puedes ver, estas llamadas a métodos se meten demasiado en los asuntos de otros objetos. La consecuencia negativa más importante y obvia es el cambio de un montón de estas llamadas a métodos por todas partes si la estructura de estos objetos tiene que cambiar, lo que hará eventualmente, porque la única constante en el desarrollo de software es el cambio. Además, tiene un aspecto realmente desagradable, nada agradable a la vista. Cuando no sabes que es un enfoque problemático, Rails te permite llevarlo muy lejos de todos modos, sin gritar. Mucha cuerda, ¿recuerdas?

Entonces, ¿qué podemos hacer al respecto? Al fin y al cabo, queremos obtener esa información de alguna manera. Por un lado, podemos componer nuestros objetos para que se ajusten a nuestras necesidades y, por otro, podemos hacer un uso inteligente de la delegación para que nuestros modelos sean delgados. Vamos a sumergirnos en algo de código para mostrar lo que quiero decir.

1
class SpectreMember < ActiveRecord::Base
2
  has_many :operations
3
  has_many :spectre_agents
4
  
5
  ...
6
7
end
8
9
class Operation < ActiveRecord::Base
10
  belongs_to :spectre_member
11
12
  ...
13
14
end
15
16
class SpectreAgent < ActiveRecord::Base
17
  belongs_to :spectre_member
18
19
  ...
20
21
end
22
23
@spectre_member.spectre_agents.all
24
@spectre_member.operations.last.print_assignment
25
@spectre_member.spectre_agents.find_by_id(1).name
26
27
@operation.spectre_member.name
28
@operation.spectre_member.number
29
@operation.spectre_member.spectre_agents.first.name
30
31
@spectre_agent.spectre_member.number
32
1
class SpectreMember < ActiveRecord::Base
2
  has_many :operations
3
  has_many :spectre_agents
4
  
5
  ...
6
7
  def list_of_agents
8
    spectre_agents.all
9
  end
10
11
  def print_operation_details
12
    operation = Operation.last
13
    operation.print_operation_details
14
  end
15
end
16
17
class Operation < ActiveRecord::Base
18
  belongs_to :spectre_member
19
20
  ...
21
22
  def spectre_member_name
23
    spectre_member.name
24
  end
25
26
  def spectre_member_number
27
    spectre_member.number
28
  end
29
30
  def print_operation_details
31
    puts "This operation’s objective is #{objective}. The target is #{target}"
32
  end
33
end
34
35
class SpectreAgent < ActiveRecord::Base
36
  belongs_to :spectre_member
37
38
  ...
39
40
  def superior_in_charge
41
    puts "My boss is number #{spectre_member.number}"
42
  end
43
end
44
45
@spectre_member.list_of_agents
46
@spectre_member.print_operation_details
47
48
@operation.spectre_member_name
49
@operation.spectre_member_number
50
51
@spectre_agent.superior_in_charge
52

Esto es definitivamente un paso en la dirección correcta. Como puedes ver, hemos empaquetado la información que queríamos adquirir en un montón de métodos de envoltura. En lugar de llegar a muchos objetos directamente, abstraemos estos puentes y dejamos que los respectivos modelos hablen con sus amigos sobre la información que necesitamos.

La desventaja de este enfoque es tener todos estos métodos envolventes adicionales por ahí. A veces está bien, pero realmente queremos evitar mantener estos métodos en un montón de lugares si un objeto cambia.

Si es posible, el lugar dedicado para que cambien es en su objeto, y solo en su objeto. Contaminar los objetos con métodos que tienen poco que ver con su propio modelo también es algo a tener en cuenta, ya que esto siempre es un peligro potencial para diluir las responsabilidades individuales.

Podemos hacerlo mejor. En la medida de lo posible, deleguemos las llamadas a los métodos directamente a sus objetos responsables e intentemos reducir los métodos envolventes todo lo que podamos. Rails sabe lo que necesitamos y nos proporciona el práctico método de la clase delegate para decirle a los amigos de nuestro objeto qué métodos necesitamos que sean llamados.

Vamos a profundizar en algo del ejemplo de código anterior y ver dónde podemos hacer un uso adecuado de la delegación.

1
class Operation < ActiveRecord::Base
2
  belongs_to :spectre_member
3
 
4
  delegate :name, :number, to: :spectre_member, prefix: true
5
6
  ...
7
8
# def spectre_member_name
9
#   spectre_member.name
10
# end
11
12
# def spectre_member_number
13
#   spectre_member.number
14
# end
15
16
  ...
17
18
end
19
20
@operation.spectre_member_name
21
@operation.spectre_member_number
22
23
24
25
class SpectreAgent < ActiveRecord::Base
26
  belongs_to :spectre_member
27
28
  delegate :number, to: :spectre_member, prefix: true
29
30
  ...
31
32
  def superior_in_charge
33
    puts "My boss is number #{spectre_member_number}"
34
  end
35
36
  ...
37
38
end
39

Como puedes ver, pudimos simplificar un poco las cosas usando la delegación de métodos. Nos deshicimos de Operation#spectre_member_name y Operation#spectre_member_number por completo, y SpectreAgent ya no necesita llamar a number en spectre_member; number se delega directamente a su clase "origen" SpectreMember.

En caso de que esto sea un poco confuso al principio, ¿cómo funciona exactamente? Le dices a delegate a qué :method_name debe delegar to: qué :class_name (múltiples nombres de métodos también están bien). La parte del prefix: true es opcional.

En nuestro caso, antepuso el nombre de la clase receptora en forma de serpiente antes del nombre del método y nos permitió llamar a operation.spectre_member_name en lugar del potencialmente ambiguo operation.name, si no hubiéramos utilizado la opción del prefijo. Esto funciona muy bien con las asociaciones belongs_to y has_one.

En el lado has_many de las cosas, sin embargo, la música se detendrá y te encontrarás con problemas. Estas asociaciones te proporcionan un proxy de colección que te lanzará NameErrors o NoMethodErrors cuando delegues métodos a estas "colecciones".

Espagueti SQL

Para terminar este capítulo sobre los antipatrones de los modelos en Rails, me gustaría dedicar un poco de tiempo a lo que hay que evitar cuando se trata de SQL. Las asociaciones de Active Record proporcionan opciones que facilitan sustancialmente la vida cuando se es consciente de lo que debes evitar. Los métodos de búsqueda son un tema completo por sí mismos, y no los cubriremos en toda su profundidad, pero quería mencionar algunas técnicas comunes que le ayudarán incluso cuando escriba métodos muy simples.

Las cosas que deberían preocuparnos se hacen eco de la mayor parte de lo que hemos aprendido hasta ahora. Queremos tener métodos que revelen la intención, simples y con nombres razonables para encontrar cosas en nuestros modelos. Vamos a sumergirnos en el código.

1
class Operation < ActiveRecord::Base
2
3
  has_many :agents
4
5
  ...
6
7
end
8
9
class Agent < ActiveRecord::Base
10
11
  belongs_to :operation
12
13
  ...
14
15
end
16
17
class OperationsController < ApplicationController
18
19
  def index
20
    @operation = Operation.find(params[:id])
21
    @agents = Agent.where(operation_id: @operation.id, licence_to_kill: true)
22
  end
23
end
24

Parece inofensivo, ¿no? Solo estamos buscando un grupo de agentes que tengan licencia para matar para nuestra página de operaciones. Piénsalo de nuevo. ¿Por qué debería el OperationsController indagar en el interior del Agent? Además, ¿es esto realmente lo mejor que podemos hacer para encapsular un buscador en el Agent?

Si piensas que podrías añadir un método de clase como Agent.find_licence_to_kill_agents que encapsule la lógica del buscador, definitivamente estás dando un paso en la dirección correcta, aunque no lo suficiente.

1
class Agent < ActiveRecord::Base
2
3
  belongs_to :operation
4
5
  def self.find_licence_to_kill_agents(operation)
6
    where(operation_id: operation.id, licence_to_kill: true)
7
  end
8
  ...
9
10
end
11
12
class OperationsController < ApplicationController
13
14
  def index
15
    @operation = Operation.find(params[:id])
16
    @agents = Agent.find_licence_to_kill_agents(@operation)
17
  end
18
end
19

Tenemos que comprometernos un poco más que eso. En primer lugar, esto no es utilizar las asociaciones a nuestro favor, y la encapsulación también es subóptima. Las asociaciones como has_many tienen la ventaja de que podemos añadirlas al array de proxies que nos devuelven. Podríamos haber hecho esto en su lugar:

1
class Operation < ActiveRecord::Base
2
3
  has_many :agents
4
5
  def find_licence_to_kill_agents
6
    self.agents.where(licence_to_kill: true)
7
  end
8
  ...
9
10
end
11
12
class OperationsController < ApplicationController
13
14
  def index
15
    @operation = Operation.find(params[:id])
16
    @agents = @operation.find_licence_to_kill_agents
17
  end
18
end
19

Esto funciona, seguro, pero también es otro pequeño paso en la dirección correcta. Sí, el controlador es un poco mejor, y hacemos un buen uso de las asociaciones de modelos, pero todavía debe sospechar por qué Operation se preocupa de la implementación de encontrar un determinado tipo de Agent. Esta responsabilidad corresponde al propio modelo de Agent.

Los ámbitos con nombre son muy útiles para ello. Los ámbitos definen métodos de clase encadenables, muy importantes, para tus modelos y, por tanto, te permiten especificar consultas útiles que puedes utilizar como llamadas a métodos adicionales sobre tus asociaciones de modelos. Los dos siguientes enfoques para el Agent de alcance son indiferentes.

1
class Agent < ActiveRecord::Base
2
  belongs_to :operation
3
4
  scope :licenced_to_kill, -> { where(licence_to_kill: true) }
5
end
6
7
class Agent < ActiveRecord::Base
8
  belongs_to :operation
9
10
  def self.licenced_to_kill
11
    where(licence_to_kill: true)
12
  end
13
end
14
15
class OperationsController < ApplicationController
16
17
  def index
18
    @operation = Operation.find(params[:id])
19
    @agents = @operation.agents.licenced_to_kill
20
  end
21
end
22

Eso es mucho mejor. En caso de que la sintaxis de los ámbitos sea nueva para ti, no son más que lambdas (apiladas), no es muy importante que las mires de inmediato, por cierto, y son la forma correcta de llamar a los ámbitos desde Rails 4. Agent se encarga ahora de gestionar sus propios parámetros de búsqueda, y las asociaciones pueden limitarse a arropar lo que necesitan encontrar.

Este enfoque te permite realizar consultas como llamadas SQL individuales. Personalmente, me gusta utilizar el ámbito de aplicación por su explicitud. Los ámbitos también son muy útiles para encadenar dentro de métodos de búsqueda bien nombrados, de esta manera aumentan la posibilidad de reutilizar el código y de DRY-ing. Digamos que tenemos algo un poco más complicado:

1
class Agent < ActiveRecord::Base
2
  belongs_to :operation
3
4
  scope :licenced_to_kill, -> { where(licence_to_kill: true) }
5
  scope :womanizer, -> { where(womanizer: true) }
6
  scope :bond, -> { where(name: 'James Bond') }
7
  scope :gambler, -> { where(gambler: true) }
8
end
9

Ahora podemos utilizar todos estos ámbitos para construir consultas más complejas.

1
class OperationsController < ApplicationController
2
3
  def index
4
    @operation = Operation.find(params[:id])
5
    @double_o_agents = @operation.agents.licenced_to_kill
6
  end
7
8
  def show
9
    @operation = Operation.find(params[:id])
10
    @bond = @operation.agents.womanizer.gambler.licenced_to_kill
11
  end
12
13
  ...
14
end
15

Claro, eso funciona, pero me gustaría sugerirte que vayas un paso más allá.

1
class Agent < ActiveRecord::Base
2
  belongs_to :operation
3
4
  scope :licenced_to_kill, -> { where(licence_to_kill: true) }
5
  scope :womanizer, -> { where(womanizer: true) }
6
  scope :bond, -> { where(name: 'James Bond') }
7
  scope :gambler, -> { where(gambler: true) }
8
9
  def self.find_licenced_to_kill
10
    licenced_to_kill
11
  end
12
13
  def self.find_licenced_to_kill_womanizer
14
    womanizer.licenced_to_kill
15
  end
16
17
  def self.find_gambling_womanizer
18
    gambler.womanizer
19
  end
20
 
21
  ...
22
23
end
24
25
class OperationsController < ApplicationController
26
27
  def index
28
    @operation = Operation.find(params[:id])
29
    @double_o_agents = @operation.agents.find_licenced_to_kill
30
  end
31
32
  def show
33
    @operation = Operation.find(params[:id])
34
    @bond = @operation.agents.find_licenced_to_kill_womanizer
35
    #or
36
    @bond = @operation.agents.bond
37
  end
38
39
  ...
40
41
end
42

Como puedes ver, a través de este enfoque cosechamos los beneficios de la encapsulación adecuada, las asociaciones de modelos, la reutilización del código y la denominación expresiva de los métodos, y todo ello mientras realizamos consultas SQL individuales. No más código espagueti, ¡impresionante!

Si te preocupa violar la Ley de Demeter, te alegrará saber que, como no estamos añadiendo puntos metiéndolos en el modelo asociado, sino encadenándolos solo en su propio objeto, no estamos cometiendo ningún delito de Demeter.

Reflexiones finales

Desde la perspectiva de un principiante, creo que has aprendido mucho sobre el mejor manejo de los modelos Rails y cómo modelarlos de forma más robusta sin llamar a un verdugo.

Pero no te engañes pensando que no hay mucho más que aprender sobre este tema en particular. Te he presentado unos cuantos antipatrones que creo que los novatos pueden entender y manejar fácilmente para protegerse desde el principio. Si no sabes lo que no sabes, hay mucha cuerda disponible para echártela al cuello.

Aunque este fue un sólido comienzo en este tema, no sólo hay más aspectos de los modelos AntiPatterns en Rails sino también más matices que tendrás que explorar también. Estos eran los aspectos básicos, muy esenciales e importantes, y deberías sentirte satisfecho por no haber esperado hasta mucho más tarde en tu carrera para descubrirlos.

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.