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

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

Взаимоотношения объектов — это концепция, используемая для описания того, как разные классы, создающие объекты, могут взаимодействовать друг с другом с другими созданными нами объектами. Как и в реальной жизни, то, как объекты относятся друг к другу, зависит от отношения каждого объекта друг к другу. Как программисты, мы контролируем, как мы строим эти отношения, которые мы будем называть доменом моделью. Существует несколько основных способов, которыми эти модели соотносятся друг с другом. Я собираюсь углубиться в отношения принадлежит и имеет-многие.

Когда мы думаем о мире покемонов, у нас есть тренер, которым может быть Эш, Брок, Гэри или любой другой тренер покемонов. У индивидуального тренера, скажем, Эша в этом примере, много покемонов: Пикачу, Чаризард, Бульбазавр и многие другие. У каждого из этих отдельных покемонов есть свои движения, например, движения Пикачу могут включать Гром, Удар молнии и Быструю атаку.

В качестве примера я решил сфокусировать нашу доменную модель на популярном Эше Кетчуме, и прямо сейчас у Эша есть следующие три покемона: Пикачу, Чаризард и Бульбазавр. Я также буду использовать слова «экземпляр класса» и «объект» для обозначения одного и того же, поскольку их часто можно использовать взаимозаменяемо.

Ash — это отдельный объект нашего класса Trainer, и объект Ash имеет свой собственный уникальный набор атрибутов, характерных для этого класса. Что я подразумеваю под объектом? Воспринимайте класс Trainer как схему, позволяющую создавать уникальные экземпляры (или копии) класса Trainer, которые мы можем называть объектами, поэтому Ash является экземпляром класса Trainer. У каждого экземпляра класса есть атрибуты, которые делают его уникальным, например, какие атрибуты есть у всех тренеров (кроме покемонов)? Некоторыми общими атрибутами могут быть имя, возможное прозвище, количество покеболов, уровень тренера, тип личности, предметы и количество денег в кармане тренера, и это лишь некоторые из них.

Для простоты я сосредоточусь на нескольких атрибутах Trainer для нашего объекта Ash, который, как вы помните, является одним экземпляром (или объектом) Trainer с уникальными свойствами и атрибутами.

Теперь давайте создадим объект тренера Эша из класса тренера в IRB следующим образом:

Исходя из вышеизложенного, мы можем вернуть нам имя Эша следующим образом:

И мы также можем вернуть уровень объекта Ash следующим образом:

Что касается моделирования нашего объекта Ash, созданного из класса Trainer, в реальном мире, это не очень реалистично. У тренера гораздо больше атрибутов, чем просто имя и уровень. Один из самых важных атрибутов, которого не хватает тренеру покемонов, — это покемон тренера. Как мы видим, наша программа не знает, кто такие покемоны наших объектов Эша, и даже не знает, как вызывать покемонов.

В мире покемонов покемон принадлежит тренеру, а у тренера много покемонов. Как мы можем смоделировать этого покемона, принадлежащего тренеру, с помощью нашего кода? Давайте дадим атрибут покемона отдельному тренеру.

Отношение «принадлежит»

Теперь, когда у нас есть сеттер и геттер для атрибута покемона тренера, давайте снова просмотрим данные в IRB или Pry:

Допустим, мы хотим узнать имя покемона.

Похоже, что «строка», которую мы присвоили этому атрибуту покемона объекта Ash, не имеет доступа к методу name.

Вместо того, чтобы задавать атрибут покемона Эша в виде строки, мы можем установить для него что-то более полезное, например, сложный объект. Мы можем установить его равным объекту Pokémon. Поэтому вместо того, чтобы метод pokemon=() равнялся строке, давайте создадим класс Pokemon и назначим отдельный объект pokemon атрибутам pokemon экземпляра Ash.

Точно так же, как мы смогли установить атрибут pokemon равным строке «Pikachu», мы можем установить атрибут равным экземпляру класса Pokemon, хранящемуся в переменной pikachu.

Теперь мы можем запросить тип.

Теперь отношения между Покемоном и Тренером практически завершены. Мы установили связь «принадлежит». У покемона есть один тренер (в нашей модели предметной области), поэтому мы говорим, что покемон принадлежит тренеру. Мы установили отношения, предоставив объекту Trainer сеттер и геттер для Pokemon.

Если мы посмотрим на блок-схему покемонов, то увидим, что всего три экземпляра покемонов принадлежат объекту Эша. Они связаны горизонтально, потому что все элементы в классе будут иметь одинаковые типы атрибутов, но значения или особенности этих атрибутов будут меняться в зависимости от покемона.

Отношения «имеет много»

Чтобы создать функциональность «имеет много» в нашем классе тренеров и позволить ему иметь много покемонов, нам нужно немного изменить класс.

Мы начали встраивать функциональность в наши классы Trainer, чтобы позволить ему общаться с другими классами и получать много покемонов. Как? Посмотрите на @pokemons = [] в методе инициализации. Что это делает? Каждый раз, когда инициализируется новый класс Trainer (скажем, наш объект Ash), атрибут pokemon представляет собой массив, в котором хранится множество объектов pokemon. Думайте об этом как о месте, где находятся все покемоны Эша. Объектная часть наиболее важна — это не просто цепочка покемонов, а объекты покемонов.

Взгляните на приведенный выше код, и вы увидите, что мы добавили атрибут тренера к классу Pokemon, который, в свою очередь, является сеттером и геттером. Но он не был включен в метод инициализации. Зачем нам это делать? Ответ: кто выбирает, когда добавляются покемоны? Покемон или тренер? Тренер, конечно.

Как показать это с помощью кода? Мы можем перейти к классу Trainer и добавить метод для этого.

Давайте сосредоточимся на методе add_pokemon.

Так в чем здесь аргумент покемонов? Это объект покемонов, созданный из класса покемонов. Мы создали этот метод, который принимает аргумент pokemon, чтобы тренер мог решить, когда добавлять объект Pokémon в свой массив @pokemons. В мире покемонов это будет, когда они поймают покемона.

Хорошо, мы можем посмотреть на следующую строку, которая у нас есть.

@pokemons ‹‹ покемон

Как уже говорилось, pokemon представляет объект pokemon, а @pokemon — это массив, в котором мы решили хранить все объекты pokemon. Как объекты покемонов узнают, с каким игроком у них отношения? Через следующую строку.

У нас есть…

pokemon.trainer = я

Давайте разберем это

pokémon — это аргумент, который мы передали с объектом pokémon.

pokémon.trainer использует установщик и геттер метода train =() для присвоения значения самому себе.

Что такое я?

Self — это объект, получающий вызов метода. Если метод является методом экземпляра, self всегда является экземпляром, с которым мы работаем прямо сейчас. Если метод является методом класса, self будет классом, над которым действует метод.

Возьмем эту информацию о том, что такое «я», вернемся к нашему примеру.

pokemon.trainer = я

Первый вопрос: где мы? Мы находимся в классе Trainer и читаем созданный нами метод, который является методом экземпляра. Мы не хотим, чтобы этот метод работал для класса Trainer в целом, потому что мы хотим убедиться, что конкретный тренер имеет доступ к своему покемону. Кроме того, мы пытаемся добавить покемона к объекту тренера по имени Эш. Следовательно, self относится к объекту Ash.

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

В строке 1 мы создаем объект Trainer и присваиваем его переменной Ash. Этот объект Ash имеет атрибут имени «Ash» и атрибут уровня тренера «10».

В строке 2 мы создаем объект Pokemon и присваиваем его переменной pikachu. Эта переменная пикачу содержит объект пикачу, который имеет атрибут имени «Пикачу» и атрибут типа «Электрик».

В строке 3 мы вызываем метод add_pokemon. Мы вызываем add_pokemon для конкретного экземпляра тренера, которым является Эш, и передаем объект пикачу в качестве аргумента.

В строке 4 мы проверяем, сработало ли это, и видим, что переменная Ash, содержащая объект Ash, имеет объект покемона внутри массива, который является объектом пикачу;

В строках 5–8 мы решили создать два новых объекта покемонов и назвали их «чаризард» и «бульбазавр», каждый со своими атрибутами. В строках 7 и 8 мы решили добавить каждый из объектов в метод add_pokemon, и это удалось.

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

Что дальше?

Хорошо, отлично, похоже, метод работает! Помните, что покемон принадлежит тренеру, а у тренера много покемонов, и эта связь была продемонстрирована. Надеюсь, что эта статья помогла вам лучше понять отношения «многие» и «принадлежит» между двумя объектами в Ruby. В идеале в мире покемонов было бы больше отношений с другими классами, чтобы по-настоящему захватить мир покемонов.

Если вы хотите увидеть приключенческую игру Pokemon CLI, в которой используются эти основы, зайдите сюда, чтобы проверить репозиторий. Некоторые ключевые особенности включают следующее:

  • Возможность ловить покемонов и добавлять их в свой список покемонов
  • Возможность посмотреть список пойманных покемонов
  • Показать сумку
  • Посмотреть текущее количество покедолларов
  • Магазин — V1 — в будущих версиях планируется добавить полностью функционирующий магазин
  • Выйти — сохранить файл сохранения
  • Start Battle — V1 — — в будущих версиях планируется добавить полностью функционирующую боевую систему.

Не забудьте поставить 👏, если вам понравилось читать!