Olores de Código Basico 01 de Ruby/Rails
() translation by (you can also view the original English article)



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!