OO Design in Rails: dónde poner cosas

244

Realmente disfruto de Rails (aunque generalmente no tengo descanso), y disfruto de que Ruby sea muy OO. Aún así, la tendencia a crear grandes subclases de ActiveRecord y enormes controladores es bastante natural (incluso si usa un controlador por recurso). Si crearas mundos de objetos más profundos, ¿dónde colocarías las clases (y los módulos, supongo)? Estoy preguntando acerca de las vistas (¿en los Helpers mismos?), Controladores y modelos.

Lib está bien, y he encontrado algunas soluciones para que se vuelva a cargar en un entorno de desarrollo , pero me gustaría saber si hay una mejor manera de hacer esto. Realmente me preocupa que las clases crezcan demasiado. Además, ¿qué pasa con los motores y cómo encajan?

Dan Rosenstark
fuente

Respuestas:

384

Debido a que Rails proporciona una estructura en términos de MVC, es natural terminar usando solo los contenedores de modelo, vista y controlador que se proporcionan para usted. El idioma típico para principiantes (e incluso algunos programadores intermedios) es incluir toda la lógica de la aplicación en el modelo (clase de base de datos), controlador o vista.

En algún momento, alguien señala el paradigma "modelo gordo, controlador delgado", y los desarrolladores intermedios eliminan rápidamente todo de sus controladores y lo tiran al modelo, que comienza a convertirse en un nuevo bote de basura para la lógica de la aplicación.

Los controladores flacos son, de hecho, una buena idea, pero el corolario: poner todo en el modelo, no es realmente el mejor plan.

En Ruby, tienes un par de buenas opciones para hacer las cosas más modulares. Una respuesta bastante popular es simplemente usar módulos (generalmente escondidos lib) que contienen grupos de métodos, y luego incluir los módulos en las clases apropiadas. Esto ayuda en los casos en los que tiene categorías de funcionalidad que desea reutilizar en varias clases, pero donde la funcionalidad todavía está vinculada a las clases.

Recuerde, cuando incluye un módulo en una clase, los métodos se convierten en métodos de instancia de la clase, por lo que aún termina con una clase que contiene una tonelada de métodos, simplemente se organizan muy bien en varios archivos.

Esta solución puede funcionar bien en algunos casos; en otros casos, querrá pensar en usar clases en su código que no sean modelos, vistas o controladores.

Una buena manera de pensarlo es el "principio de responsabilidad única", que dice que una clase debería ser responsable de una sola (o pequeña cantidad) de cosas. Sus modelos son responsables de los datos persistentes de su aplicación a la base de datos. Sus controladores son responsables de recibir una solicitud y devolver una respuesta viable.

Si tiene conceptos que no encajan perfectamente en las cajas (persistencia, la petición / respuesta de gestión), es probable que quiera pensar acerca de cómo se podría modelar la idea de que se trate. Puede almacenar clases que no sean modelos en la aplicación / clases, o en cualquier otro lugar, y agregar ese directorio a su ruta de carga haciendo lo siguiente:

config.load_paths << File.join(Rails.root, "app", "classes")

Si está utilizando pasajero o JRuby, probablemente también desee agregar su ruta a las rutas de carga ansiosas:

config.eager_load_paths << File.join(Rails.root, "app", "classes")

La conclusión es que una vez que llegas a un punto en Rails donde te encuentras haciendo esta pregunta, es hora de reforzar tus habilidades de Ruby y comenzar a modelar clases que no son solo las clases MVC que Rails te da por defecto.

Actualización: esta respuesta se aplica a Rails 2.xy superior.

Yehuda Katz
fuente
D'oh No se me había ocurrido agregar un directorio separado para los no modelos. Puedo sentir un orden que viene ...
Mike Woodhouse
Yehuda, gracias por eso. Gran respuesta. Eso es exactamente lo que estoy viendo en las aplicaciones que heredé (y las que hago): todo en controladores, modelos, vistas y los ayudantes proporcionados automáticamente para los controladores y las vistas. Luego vienen los mixins de lib, pero nunca hay un intento de modelar OO real. Sin embargo, tienes razón: en "aplicaciones / clases, o en cualquier otro lugar". Solo quería verificar si hay alguna respuesta estándar que me falta ...
Dan Rosenstark 03 de
33
Con versiones más recientes, config.autoload_paths se predetermina a todos los directorios en la aplicación. Por lo tanto, no necesita cambiar config.load_paths como se describe anteriormente. Sin embargo, no estoy seguro acerca de eager_load_paths (todavía), y necesito investigar eso. ¿Alguien ya lo sabe?
Shyam Habarakada
Pasivo agresivo hacia Intermedios: P
Sebastian Patten
8
Sería bueno que Rails se enviara con esta carpeta de "clases" para fomentar el "principio de responsabilidad única" y permitir a los desarrolladores crear objetos que no estén respaldados por la base de datos. La implementación de "Preocupaciones" en Rails 4 (ver la respuesta de Simone) parece haberse ocupado de implementar módulos para compartir la lógica entre los modelos. Sin embargo, no se ha creado dicha herramienta para clases de Ruby simples que no estén respaldadas por la base de datos. Dado que Rails es muy obstinado, tengo curiosidad por el proceso de pensamiento detrás de NO incluir una carpeta como esta.
Ryan Francis
62

Actualización : El uso de Preocupaciones se ha confirmado como el nuevo valor predeterminado en Rails 4 .

Realmente depende de la naturaleza del módulo en sí. Por lo general, coloco extensiones de controlador / modelo en una carpeta / preocupaciones dentro de la aplicación.

# concerns/authentication.rb
module Authentication
  ...
end    

# controllers/application_controller.rb
class ApplicationController
  include Authentication
end



# concerns/configurable.rb
module Configurable
  ...
end    

class Model 
  include Indexable
end 

# controllers/foo_controller.rb
class FooController < ApplicationController
  include Indexable
end

# controllers/bar_controller.rb
class BarController < ApplicationController
  include Indexable
end

/ lib es mi opción preferida para bibliotecas de uso general. Siempre tengo un espacio de nombres de proyecto en lib donde pongo todas las bibliotecas específicas de la aplicación.

/lib/myapp.rb
module MyApp
  VERSION = ...
end

/lib/myapp/CacheKey.rb
/lib/myapp/somecustomlib.rb

Las extensiones de núcleo de Ruby / Rails generalmente tienen lugar en los inicializadores de configuración para que las bibliotecas solo se carguen una vez en el arranque de Rails.

/config/initializer/config.rb
/config/initializer/core_ext/string.rb
/config/initializer/core_ext/array.rb

Para fragmentos de código reutilizables, a menudo creo (micro) complementos para poder reutilizarlos en otros proyectos.

Los archivos de ayuda generalmente contienen métodos de ayuda y, a veces, clases cuando el objeto está destinado a ser usado por los ayudantes (por ejemplo, los Constructores de formularios).

Esta es una visión general muy general. Proporcione más detalles sobre ejemplos específicos si desea obtener sugerencias más personalizadas. :)

Simone Carletti
fuente
Cosa extraña No puedo hacer que este require_dependency RAILS_ROOT + "/ lib / my_module" funcione con algo fuera del directorio lib. Definitivamente se ejecuta y se queja si no se encuentra el archivo, pero no lo vuelve a cargar.
Dan Rosenstark 01 de
Ruby's requiere que solo cargue cosas una vez. Si quieres cargar algo incondicionalmente, usa load.
Chuck
Además, me parece bastante inusual que desee cargar un archivo dos veces durante la vida útil de una instancia de aplicación. ¿Estás generando código a medida que avanzas?
Chuck
¿Por qué utiliza require_dependency en lugar de require? También tenga en cuenta que si sigue las convenciones de nomenclatura, no necesita usar require en absoluto. Si crea MyModule en lib / my_module, puede invocar MyModule sin necesidad previa (incluso si usar require debe ser más rápido y, a veces, más legible). También tenga en cuenta que el archivo en / lib solo se carga una vez en bootstrap.
Simone Carletti el
1
El uso de las preocupaciones es preocupante
bbozo
10

... la tendencia a crear grandes subclases de ActiveRecord y enormes controladores es bastante natural ...

"enorme" es una palabra preocupante ... ;-)

¿Cómo se están volviendo enormes tus controladores? Eso es algo que debe mirar: idealmente, los controladores deben ser delgados. Escogiendo una regla general de la nada, sugeriría que si regularmente tiene más de, por ejemplo, 5 o 6 líneas de código por método de controlador (acción), entonces sus controladores probablemente sean demasiado gordos. ¿Hay duplicación que podría pasar a una función auxiliar o un filtro? ¿Existe una lógica de negocios que pueda ser introducida en los modelos?

¿Cómo llegan a ser enormes tus modelos? ¿Debería buscar formas de reducir el número de responsabilidades en cada clase? ¿Hay algún comportamiento común que puedas extraer en mixins? ¿O áreas de funcionalidad que puede delegar en clases auxiliares?

EDITAR: tratando de expandirse un poco, con suerte no distorsionar nada demasiado ...

Ayudantes: viven app/helpersy se utilizan principalmente para simplificar las vistas. Son específicos del controlador (también disponibles para todas las vistas para ese controlador) o generalmente disponibles ( module ApplicationHelperen application_helper.rb).

Filtros: supongamos que tiene la misma línea de código en varias acciones (con bastante frecuencia, recuperación de un objeto usando params[:id]o similar). Esa duplicación se puede abstraer primero a un método separado y luego fuera de las acciones por completo declarando un filtro en la definición de clase, como before_filter :get_object. Consulte la Sección 6 de la Guía de rieles de ActionController. Deje que la programación declarativa sea su amiga.

Refactorizar modelos es algo más religioso. Los discípulos del tío Bob sugerirán, por ejemplo, que sigas los cinco mandamientos de SOLID . Joel y Jeff pueden recomendar un enfoque más, "pragmático", aunque posteriormente parecieron un poco más reconciliados . Encontrar uno o más métodos dentro de una clase que operan en un subconjunto claramente definido de sus atributos es una forma de tratar de identificar las clases que podrían reestructurarse a partir de su modelo derivado de ActiveRecord.

Los modelos Rails no tienen que ser subclases de ActiveRecord :: Base, por cierto. O para decirlo de otra manera, un modelo no tiene que ser un análogo de una tabla, o incluso estar relacionado con algo almacenado. Aún mejor, siempre que nombre su archivo de app/modelsacuerdo con las convenciones de Rails (llame a #underscore en el nombre de la clase para averiguar qué buscará Rails), Rails lo encontrará sin que requiresea ​​necesario.

Mike Woodhouse
fuente
Es cierto en todos los aspectos, Mike, y gracias por su preocupación ... Heredé un proyecto en el que había algunos métodos en los controladores que eran enormes. Los he desglosado en métodos más pequeños, pero el controlador en sí todavía está "gordo". Entonces, lo que estoy buscando son todas mis opciones para descargar cosas. Sus respuestas son "funciones auxiliares", "filtros", "modelos", "mixins" y "clases auxiliares". Entonces, ¿dónde puedo poner estas cosas? ¿Puedo organizar una jerarquía de clases que se carga automáticamente en un entorno de desarrollo?
Dan Rosenstark 01 de