Design OO no Rails: Onde colocar coisas

244

Estou realmente gostando do Rails (mesmo que geralmente não possua REST) ​​e gosto do Ruby sendo muito OO. Ainda assim, a tendência de criar enormes subclasses ActiveRecord e grandes controladores é bastante natural (mesmo se você usar um controlador por recurso). Se você fosse criar mundos de objetos mais profundos, onde você colocaria as classes (e módulos, suponho)? Estou perguntando sobre visualizações (nos próprios auxiliares?), Controladores e modelos.

Lib está bem, e eu encontrei algumas soluções para recarregar em um ambiente de desenvolvimento , mas eu gostaria de saber se há uma maneira melhor de fazer isso. Estou realmente preocupado com as aulas que estão crescendo demais. Além disso, o que dizer dos motores e como eles se encaixam?

Dan Rosenstark
fonte

Respostas:

384

Como o Rails fornece estrutura em termos de MVC, é natural acabar usando apenas o modelo, a exibição e os contêineres de controlador que são fornecidos para você. O idioma típico para iniciantes (e até alguns programadores intermediários) é incluir toda a lógica do aplicativo no modelo (classe de banco de dados), controlador ou exibição.

Em algum momento, alguém aponta o paradigma "modelo gordo, controlador fino" e os desenvolvedores intermediários rapidamente tiram tudo de seus controladores e jogam no modelo, que começa a se tornar uma nova lixeira para a lógica de aplicativos.

Controladores magros são, de fato, uma boa idéia, mas o corolário - colocar tudo no modelo, não é realmente o melhor plano.

No Ruby, você tem algumas boas opções para tornar as coisas mais modulares. Uma resposta bastante popular é usar apenas módulos (geralmente escondidos lib) que contêm grupos de métodos e depois incluí-los nas classes apropriadas. Isso ajuda nos casos em que você tem categorias de funcionalidade que deseja reutilizar em várias classes, mas onde a funcionalidade ainda está nocionalmente anexada às classes.

Lembre-se, quando você incluir um módulo em uma classe, os métodos tornam-se métodos de instância da classe, para que você ainda acabar com uma classe que contém uma tonelada de métodos, eles estão apenas organizou muito bem em vários arquivos.

Essa solução pode funcionar bem em alguns casos - em outros casos, você vai querer pensar em usar classes em seu código que não sejam modelos, visualizações ou controladores.

Uma boa maneira de pensar sobre isso é o "princípio de responsabilidade única", que diz que uma classe deve ser responsável por um único (ou pequeno número) de coisas. Seus modelos são responsáveis ​​pela persistência de dados do seu aplicativo no banco de dados. Seus controladores são responsáveis ​​por receber uma solicitação e retornar uma resposta viável.

Se você tem conceitos que não se encaixam perfeitamente em essas caixas (persistência, pedido / resposta de gestão), você provavelmente vai querer pensar sobre como você seria modelar a idéia em questão. Você pode armazenar classes não modelo em app / classes ou em qualquer outro lugar e adicionar esse diretório ao seu caminho de carregamento, fazendo:

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

Se você estiver usando passageiro ou JRuby, provavelmente também desejará adicionar seu caminho aos caminhos de carga ansiosos:

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

O ponto principal é que, quando você chega a um ponto no Rails em que se pergunta: é hora de reforçar seus chops Ruby e começar a modelar classes que não são apenas as classes MVC que o Rails fornece por padrão.

Atualização: Esta resposta se aplica ao Rails 2.xe superior.

Yehuda Katz
fonte
D'oh. A adição de um diretório separado para não-modelos não me ocorreu. Eu posso sentir um arrumado-up chegando ...
Mike Woodhouse
Yehuda, obrigado por isso. Ótima resposta. É exatamente isso que estou vendo nos aplicativos que herdo (e aqueles que eu faço): tudo em controladores, modelos, visualizações e ajudantes fornecidos automaticamente para controladores e visualizações. Depois vêm os mixins da lib, mas nunca há uma tentativa de fazer a modelagem OO real. Você está certo: em "apps / classes, ou em qualquer outro lugar". Só queria verificar se há alguma resposta padrão que estou perdendo ...
Dan Rosenstark
33
Nas versões mais recentes, o config.autoload_paths assume como padrão todos os diretórios no aplicativo. Portanto, você não precisa alterar o config.load_paths conforme descrito acima. No entanto, ainda não tenho certeza sobre o eager_load_paths, e preciso analisar isso. Alguém já sabe?
Shyam Habarakada 14/03
Passiva agressivo em relação a produtos intermediários: P
Sebastian Patten
8
Seria bom se o Rails fosse enviado com essa pasta "classes" para incentivar o "princípio de responsabilidade única" e permitir que os desenvolvedores criassem objetos que não são suportados pelo banco de dados. A implementação "Preocupações" no Rails 4 (veja a resposta de Simone) parece ter se encarregado de implementar módulos para compartilhar lógica entre modelos. No entanto, nenhuma ferramenta foi criada para classes Ruby simples que não são suportadas por banco de dados. Dado que o Rails é muito opinativo, estou curioso sobre o processo de pensamento por trás de NÃO incluir uma pasta como essa?
Ryan Francis
62

Atualização : O uso de preocupações foi confirmado como o novo padrão no Rails 4 .

Realmente depende da natureza do próprio módulo. Normalmente, coloco extensões de controlador / modelo em uma pasta / preocupações dentro do aplicativo.

# 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 é minha escolha preferida para bibliotecas de uso geral. Eu sempre tenho um espaço para nome do projeto na lib, onde coloco todas as bibliotecas específicas do aplicativo.

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

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

As extensões principais do Ruby / Rails geralmente ocorrem nos inicializadores de configuração, para que as bibliotecas sejam carregadas apenas uma vez no boostrap do Rails.

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

Para fragmentos de código reutilizáveis, geralmente crio (micro) plugins para poder reutilizá-los em outros projetos.

Os arquivos auxiliares geralmente contêm métodos auxiliares e às vezes classes quando o objeto se destina a ser usado por auxiliares (por exemplo, Construtores de formulários).

Esta é uma visão geral realmente geral. Forneça mais detalhes sobre exemplos específicos se desejar obter sugestões mais personalizadas. :)

Simone Carletti
fonte
Coisa bizarra. Não consigo fazer com que este require_dependency RAILS_ROOT + "/ lib / my_module" funcione com algo fora do diretório lib. Definitivamente, ele executa e reclama se o arquivo não for encontrado, mas não o recarrega.
9119 Dan Rosenstark
O Ruby exige apenas carregar as coisas uma vez. Se você deseja carregar algo incondicionalmente, use load.
Chuck
Além disso, me parece bastante incomum que você queira carregar um arquivo duas vezes durante a vida de uma instância de aplicativo. Você está gerando código à medida que avança?
Chuck
Por que você usa require_dependency em vez de require? Observe também que, se você seguir as convenções de nomenclatura, não precisará usar o exigir. Se você criar o MyModule em lib / my_module, poderá invocar o MyModule sem a necessidade anterior (mesmo que a utilização da exigência seja mais rápida e, às vezes, mais legível). Observe também que o arquivo em / lib é carregado apenas uma vez no bootstrap.
Simone Carletti
1
Uso de preocupações é preocupante
bbozo
10

... a tendência de criar enormes subclasses ActiveRecord e enormes controladores é bastante natural ...

"enorme" é uma palavra preocupante ... ;-)

Como seus controladores estão se tornando enormes? É algo que você deve considerar: idealmente, os controladores devem ser finos. Escolhendo uma regra geral do nada, sugiro que se você tiver regularmente mais de, digamos, 5 ou 6 linhas de código por método de controlador (ação), seus controladores provavelmente serão gordos demais. Existe duplicação que poderia passar para uma função auxiliar ou um filtro? Existe lógica de negócios que poderia ser introduzida nos modelos?

Como seus modelos são enormes? Você deve procurar maneiras de reduzir o número de responsabilidades em cada classe? Existem comportamentos comuns que você pode extrair em mixins? Ou áreas de funcionalidade que você pode delegar para as classes auxiliares?

EDIT: Tentando expandir um pouco, espero não distorcer nada muito mal ...

Ajudantes: moram app/helperse são usados ​​principalmente para simplificar as visualizações. Eles são específicos do controlador (também disponíveis para todas as visualizações desse controlador) ou geralmente estão disponíveis ( module ApplicationHelperem application_helper.rb).

Filtros: digamos que você tenha a mesma linha de código em várias ações (muitas vezes, recuperação de um objeto usando params[:id]ou similar). Essa duplicação pode ser abstraída primeiro para um método separado e, em seguida, para fora das ações, declarando um filtro na definição de classe, como before_filter :get_object. Veja a Seção 6 no Guia ActionController Rails Deixe a programação declarativa ser sua amiga.

Refatorar modelos é um pouco mais religioso. Os discípulos do tio Bob sugerem, por exemplo, que você siga os cinco mandamentos do SOLID . Joel & Jeff podem recomendar uma abordagem mais, mais "pragmática", embora pareçam ser um pouco mais reconciliados posteriormente. Encontrar um ou mais métodos dentro de uma classe que operam em um subconjunto claramente definido de seus atributos é uma maneira de tentar identificar classes que podem ser refatoradas fora do seu modelo derivado do ActiveRecord.

Os modelos Rails não precisam ser subclasses de ActiveRecord :: Base, a propósito. Ou, dito de outra forma, um modelo não precisa ser um análogo de uma tabela ou mesmo relacionado a qualquer coisa armazenada. Melhor ainda, desde que você nomeie seu arquivo de app/modelsacordo com as convenções do Rails (chame #underscore no nome da classe para descobrir o que o Rails procurará), o Rails o encontrará sem que requireseja necessário.

Mike Woodhouse
fonte
É verdade, Mike, e obrigado pela sua preocupação ... Eu herdei um projeto no qual havia alguns métodos em controladores que eram enormes. Dividi-os em métodos menores, mas o próprio controlador ainda é "gordo". Então, o que estou procurando são todas as minhas opções para descarregar coisas. Suas respostas são "funções auxiliares", "filtros", "modelos", "mixins" e "classes auxiliares". Então, onde posso colocar essas coisas? Posso organizar uma hierarquia de classes que é carregada automaticamente em um ambiente de desenvolvimento?
217 Dan Rosenstark