AlterEGO Labs official blog

About ruby, rails and another amazing stuffs…

Коллизия имен класса и модуля в Ruby

Вот такая вот ситуация

В основе статьи лежит ситуация, с которой я и мои коллеги столкнулись при работе над одним проектом.

Представим себе следующее. У меня есть модель Item:

Листинг 0 - Модель Item app/models/item.rb
1
2
class Item < ActiveRecord::Base
end

Затем нам дали задачу на новую фичу: пользователи могут создавать списки и добавлять туда элементы. Мы сразу видим модель List:

Листинг 1 - Модель List app/models/list.rb
1
2
class List < ActiveRecord::Base
end

Но для хранения связи между элементом и листом мы решили использовать модель не ListItem, а положить ее в namespace List, т.е. логически выделить эту часть подсистемы, т.к. мы предполагаем, что будут еще связанные модели. И получается следующее:

Листинг 2 - Модель List::Item app/models/list/item.rb
1
2
3
4
module List
  class Item < ActiveRecord::Base
  end
end

Как бы все идет хорошо до тех пор, пока вы не захотите использовать List::Item модель где-нибудь в коде приложения:

Листинг 3 - Использование List::Item в rails console
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Loading development environment (Rails 4.0.4)
2.1.2 :001 > List::Item.new
TypeError: List is not a module
  from /Users/sergio/Work/railsapps/namespace_collision_demo/app/models/list/item.rb:1:in `<top (required)>'
 from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:424:in `load'
  from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:424:in `block in load_file'
 from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:616:in `new_constants_in'
  from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:423:in `load_file'
 from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:324:in `require_or_load'
  from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:463:in `load_missing_constant'
 from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:184:in `const_missing'
  from (irb):1
  from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/railties-4.0.4/lib/rails/commands/console.rb:90:in `start'
 from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/railties-4.0.4/lib/rails/commands/console.rb:9:in `start'
  from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/railties-4.0.4/lib/rails/commands.rb:62:in `<top (required)>'
 from bin/rails:4:in `require'
  from bin/rails:4:in `<main>'

Есть несколько путей решения этой проблемы.

Решение 0 - Внести название модуля в название класса

Я до конца не понимаю почему, но вот если изменить код модели List::Item на следующий:

Листинг 4 - Решение 0 для модели List::Item app/models/list/item.rb
1
2
class List::Item < ActiveRecord::Base
end

то все рабоет нормально:

Листинг 5 - Результат решения 0
1
2
2.1.2 :001 > List::Item.new
 => #<List::Item id: nil, created_at: nil, updated_at: nil>

Решение 1 - Заменить module на класс

Изучая объектную модель ruby мы обнаружили одну интересную вещь: класс - это тот же модуль, но с дополнительным функционалом!

Листинг 6 - Отношения Class и Module
1
2
3
4
2.1.2 :013 > List::Item.class
 => Class
2.1.2 :014 > List::Item.class.superclass
 => Module

Значит Class наследует весь функционал Module и он также может выступать как контейнер. Давайте попробуем это.

Листинг 7 - Решение 1 для модели List::Item app/models/list/item.rb
1
2
3
4
class List
  class Item < ActiveRecord::Base
  end
end

И правда - все работает отлично:

Листинг 8 - Результат решения 1
1
2
2.1.2 :001 > List::Item.new
 => #<List::Item id: nil, created_at: nil, updated_at: nil>

Решение 2 - К черту всю эту возню с объектной моделью!

В данном случае предлагается поступить следующим образом: выделить namespace и ложить туда все модели, которые логически связаны. В соответствии с нашим примером будет следующее:

Листинг 9 - Модель Lists::List app/models/lists/list.rb
1
2
3
4
module Lists
  class List < ActiveRecord::Base
  end
end
Листинг 10 - Модель Lists::Item app/models/lists/item.rb
1
2
3
4
module Lists
  class Item < ActiveRecord::Base
  end
end

Однозначным плюсом данного подхода является то, что все модели лежат в одной папке и если вам нужно выпилить данный кусок функционала, то вот оно все в папке лежит - не нужно шарится среди несколько десятков моделей в папке app/models.

Comments