Когда использовать вложенные классы и классы, вложенные в модули?

148

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

class Foo
  class Bar
    # do some useful things
  end
end

А также классы, вложенные в такие модули:

module Baz
  class Quux
    # more code
  end
end

Либо документация и статьи немногочисленны, либо я недостаточно осведомлен по этому вопросу, чтобы нащупать правильные условия поиска, но, похоже, я не могу найти много информации по теме.

Может ли кто-нибудь предоставить примеры или ссылки на сообщения о том, почему и когда будут использоваться эти методы?

cmhobbs
источник

Ответы:

141

В других языках ООП есть внутренние классы, которые не могут быть созданы без привязки к классу верхнего уровня. Например, в Java

class Car {
    class Wheel { }
}

только методы в Carклассе могут создавать Wheels.

У Ruby такого поведения нет.

В Ruby,

class Car
  class Wheel
  end
end

отличается от

class Car
end

class Wheel
end

только имя класса WheelVS. Car::Wheel. Это различие в названии может показать программистам, что Car::Wheelкласс может представлять только колесо автомобиля, а не колесо общего назначения. Вложенность определений классов в Ruby - это вопрос предпочтения, но он служит цели в том смысле, что более строго обеспечивает соблюдение контракта между двумя классами и тем самым передает больше информации о них и их использовании.

Но для интерпретатора Ruby это разница только в названии.

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

module ActiveRecord
  class Base
  end
end

отличается от

module ActionMailer
  class Base
  end
end

Хотя это не единственное использование классов, вложенных внутри модулей, обычно оно наиболее распространено.

Пан Томакос
источник
5
@rubyprince, я не понимаю, что вы имеете в виду, говоря об установлении связи между Car.newи Car::Wheel.new. Вам определенно не нужно инициализировать Carобъект для инициализации Car::Wheelобъекта в Ruby, но Carкласс должен быть загружен и выполнен, Car::Wheelчтобы его можно было использовать.
Пан Томакос 02
31
@Pan, вы путаете внутренние классы Java и классы Ruby с пространством имен. Нестатический вложенный класс Java называется внутренним классом и существует только внутри экземпляра внешнего класса. Есть скрытое поле, которое позволяет внешние ссылки. Внутренний класс Ruby просто имеет пространство имен и никоим образом не «привязан» к включающему классу. Он эквивалентен статическому (вложенному) классу Java . Да, за ответ проголосовало много, но он не совсем точный.
DigitalRoss,
7
Я понятия не имею, как этот ответ получил 60 голосов, не говоря уже о том, чтобы его приняли OP. Здесь буквально нет ни одного верного утверждения. В Ruby нет вложенных классов, таких как Beta или Newspeak. Там нет абсолютно никакого отношения между Carи Car::Wheel. Модули (и, следовательно, классы) - это просто пространства имен для констант, в Ruby нет таких вещей, как вложенный класс или вложенный модуль.
Jörg W Mittag
4
Единственное различие между ними - постоянное разрешение (которое является лексическим и, следовательно, очевидно, разным, потому что два фрагмента лексически различны). Однако между этими двумя классами нет абсолютно никакой разницы. Есть просто два совершенно не связанных между собой класса. Период. Ruby не имеет вложенных / внутренних классов. Ваше определение вложенных классов является правильным, но это просто не относится к Ruby, как вы можете тривиальным проверить: Car::Wheel.new. Бум. Я только что построил Wheelобъект, который не вложен в Carобъект.
Jörg W Mittag
11
Этот пост вводит в заблуждение, если не читать всю ветку комментариев.
Натан
50

В Ruby определение вложенного класса похоже на определение класса в модуле. На самом деле он не вызывает ассоциации между классами, он просто создает пространство имен для констант. (Имена классов и модулей являются константами.)

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

class A; class B; end; end
A::B.new

Преимущества те же, что и для модулей: инкапсуляция, группировка кода, используемого только в одном месте, и размещение кода ближе к тому месту, где он используется. В большом проекте может быть один внешний модуль, который повторяется снова и снова в каждом исходном файле и содержит множество определений классов. Когда все различные фреймворки и библиотечные коды делают это, тогда они вносят только одно имя на верхний уровень, уменьшая вероятность конфликтов. Прозаика, конечно, но они поэтому и используются.

Использование класса вместо модуля для определения внешнего пространства имен может иметь смысл в однофайловой программе или скрипте, или если вы уже используете для чего-то класс верхнего уровня, или если вы действительно собираетесь добавить код для связывания классов вместе в истинном стиле внутреннего класса . Ruby не имеет внутренних классов, но ничто не мешает вам создать примерно такое же поведение в коде. Ссылка на внешние объекты из внутренних по-прежнему потребует расставления точек в экземпляре внешнего объекта, но вложенность классов предполагает, что это то, что вы могли бы делать. Тщательно модульная программа всегда может сначала создавать включающие классы, и их можно разумно разложить на вложенные или внутренние классы. Вы не можете вызвать newмодуль.

Вы можете использовать общий шаблон даже для сценариев, где пространство имен не очень необходимо, просто для развлечения и практики ...

#!/usr/bin/env ruby

class A
  class Realwork_A
    ...
  end
  class Realwork_B
    ...
  end

  def run
    ...
  end
  
  self
end.new.run
DigitalRoss
источник
15
Пожалуйста, хорошенько, пожалуйста, не называйте это внутренним классом. Это не. Класс неB находится внутри класса . Константа в пространстве имен внутри класса , но нет абсолютно никакой связи между объектом , на который ссылается (в данном случае как раз случается быть класс) и класса , на который ссылается . A BABA
Jörg W Mittag
2
Хорошо, "внутренняя" терминология удалена. Хорошая точка зрения. Для тех, кто не следует приведенному выше аргументу, причина спора заключается в том, что когда вы делаете что-то подобное, скажем, в Java, объекты внутреннего класса (и здесь я использую термин канонически) содержат ссылку на на внешний класс и переменные внешнего экземпляра можно ссылаться с помощью методов внутреннего класса. Ничего из этого не происходит в Ruby, если вы не свяжете их с кодом. И вы знаете, если бы этот код присутствовал в, кхм, включающем классе, то я уверен, что вы могли бы разумно назвать Bar внутренним классом.
DigitalRoss,
1
Для меня это утверждение больше всего помогает мне при выборе между модулем и классом: You can't call new on a module.- так что в общих чертах, если я просто хочу создать пространство имен для некоторых классов и мне никогда не нужно создавать экземпляр внешнего «класса», тогда Я бы использовал внешний модуль. Но если я хочу создать экземпляр обертывающего / внешнего «класса», я бы сделал его классом, а не модулем. По крайней мере, для меня это имеет смысл.
FireDragon
@FireDragon Или другой вариант использования может заключаться в том, что вам нужен класс, являющийся Factory, от которого наследуются подклассы, а ваша фабрика отвечает за создание экземпляров подклассов. В этой ситуации ваш Factory не может быть модулем, потому что вы не можете наследовать от модуля, поэтому это родительский класс, который вы не создаете (и можете 'пространство имен' его
дочерними элементами,
15

Вероятно, вы захотите использовать это, чтобы сгруппировать свои классы в модуль. Что-то вроде пространства имен.

например, гем Twitter использует для этого пространства имен:

Twitter::Client.new

Twitter::Search.new

Так что оба класса Clientи Searchнаходятся под Twitterмодулем.

Если вы хотите проверить исходники, код для обоих классов можно найти здесь и здесь .

Надеюсь это поможет!

Пабло Фернандес
источник
3
Ссылка на обновление драгоценного камня Twitter: github.com/sferik/twitter/tree/master/lib/twitter
kode
6

Есть еще одно различие между вложенными классами и вложенными модулями в Ruby до версии 2.5, которую другие ответы не смогли охватить, и я считаю, что здесь следует упомянуть. Это процесс поиска.

Вкратце: из-за поиска констант верхнего уровня в Ruby до версии 2.5 Ruby может в конечном итоге искать ваш вложенный класс в неправильном месте ( Objectв частности), если вы используете вложенные классы.

В Ruby до версии 2.5:
СтруктураX вложенного класса : предположим, у вас есть класс с вложенным классом Yили X::Y. И тогда у вас также есть класс верхнего уровня с именем Y. Если X::Yне загружен, то при вызове происходит следующее X::Y:

Не найдя Yв X, Ruby попытается найти его в предках X. посколькуXэто класс, а не модуль, у него есть предки, среди которых есть [Object, Kernel, BasicObject]. Таким образом, он пытается искать Yин Object, где он находит его успешно.

Все же это верхний уровень Yи нет X::Y. Вы получите это предупреждение:

warning: toplevel constant Y referenced by X::Y


Структура вложенного модуля: предположим, что в предыдущем примере Xэто модуль, а не класс.

Модуль имеет только самого себя в качестве предка: X.ancestorsбудет производить [X].

В этом случае Ruby не сможет искать Yв одном из предков Xи выбросит файл NameError. X::YПосле этого Rails (или любой другой фреймворк с автозагрузкой) попытается загрузиться .

См. Эту статью для получения дополнительной информации: https://blog.jetbrains.com/ruby/2017/03/why-you-should-not-use-a-class-as-a-namespace-in-rails-applications/

В Ruby 2.5:
удален поиск констант верхнего уровня.
Вы можете использовать вложенные классы, не опасаясь столкнуться с этой ошибкой.

Тимур Нугманов
источник
3

В дополнение к предыдущим ответам: модуль в Ruby - это класс

$ irb
> module Some end
=> nil
> Some.class
=> Module
> Module.superclass
=> Object
демас
источник
11
Точнее было бы сказать, что «класс в Ruby - это модуль»!
Тим Диггинс
2
Все в Ruby может быть объектом, но вызов модуля классом кажется неправильным: irb (main): 005: 0> Class.ancestors.reverse => [BasicObject, Kernel, Object, Module, Class]
Chad M
и вот мы подошли к определению «есть»
anbizcad 01
Обратите внимание, что модули не могут быть созданы. Т.е. из модуля нельзя создавать объекты. Итак, модули, в отличие от классов, не имеют метода new. Итак, хотя вы можете сказать, что модули - это класс (нижний регистр), это не то же самое, что класс (верхний регистр) :)
rmcsharry