К концептуальной модели объектно-ориентированного программирования

Руби и проблема «одного и многих»

Каким образом мы можем использовать такой термин, как «собака», для обозначения стольких разных существ в мире? У нас есть чихуахуа, мопсы, пудели, золотистые ретриверы, мастифы и этот список можно продолжить. Есть ли какая-то универсальная сущность, которая позволяет нам сгруппировать определенные отдельные существа в одну отдельную категорию?

Эта проблема универсальности и частностей, также известная как проблема «Единого и Многого», не нова. Платон задавал тот же вопрос тысячи лет назад. Он выдвинул целую теорию по этому поводу, теорию, которую мы называем «теорией форм» Платона.

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

Считайте этот пост первой попыткой новичка в мире программирования попытаться концептуализировать отношения между классами и объектами. Это не столько попытка «уточнить» или «последнее слово», сколько попытка стимулировать обсуждение; так что критика и предложения приветствуются. (Между прочим, нижеприведенное обсуждение игнорирует функции метапрограммирования Ruby).

Первые принципы

Давайте начнем с представления классов Ruby как универсальных форм, а объектов Ruby как отдельных существ. Класс определяет сущность, которая предопределяет или предопределяет конкретные объекты, экземпляры которых создаются из этого класса; более конкретно, класс содержит определенные поведения и атрибуты ¹, которые управляют тем, что любой конкретный объект способен делать, и областью состояния любого конкретного объекта. (выделенные курсивом слова выделяются, потому что они являются семантически загруженными терминами в рамках парадигмы ООП или, по крайней мере, ее реализации в Ruby).

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

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

Классы и объекты:

Класс (формальная сущность)

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

Начнем с примера. Ниже мы определяем класс Robot, в котором мы обрисовываем суть того, что значит быть объектом Robot. В простом случае, описанном ниже, объект-робот может «разговаривать» (поведение) и имеет атрибут «имя».

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

Объект (реальное существование)

Объект или экземпляр - это конкретная реализация класса. Например, мы можем создать один или любое количество объектов Robot из нашего класса Robot. Ниже мы приводим два экземпляра.

Обратите внимание: поскольку мы определили метод конструктора initialize с параметром name, мы должны передать аргумент методу new, когда мы вызываем его в нашем классе Robot, чтобы создать экземпляр нового объекта Robot.

Каждый из созданных выше объектов Robot уникален, каждый имеет свой уникальный идентификатор объекта, который мы можем выявить, вызвав метод object_id для каждого из наших объектов.

После создания каждый объект также генерирует свое собственное уникальное состояние. Под состоянием мы понимаем совокупность всех переменных экземпляра, принадлежащих объекту. В случае с нашими объектами Robot выше, каждый объект обладает экземпляром атрибута «name»; то есть переменная экземпляра @name. Таким образом, набор переменных экземпляра (то есть состояния) для каждого из наших объектов Robot учитывается соответствующей переменной экземпляра @name каждого объекта.

Переменные экземпляра отслеживают состояние объекта. Точнее, переменные экземпляра отслеживают информацию о состоянии объекта .² Например, переменная экземпляра @name, принадлежащая нашему r2d2 объекту Robot, ссылается на значение "R2D2". Эта часть данных или информации составляет то, что составляет состояние объекта, и это то, что отслеживает переменная экземпляра @name. Он отличается от значений, связанных с переменной экземпляра @name, принадлежащей другому нашему объекту Robot, c3p0.

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

В самом деле, мы можем даже сказать, что объекты инкапсулируют состояние. Например, в настоящее время у нас нет прямого доступа к переменной экземпляра @name и значению, на которое она ссылается, для любого из наших объектов Robot. Мы не можем ни смотреть на это, ни манипулировать им. Это неприкасаемое или личное. Мы можем попытаться получить к нему доступ и просмотреть, выполнив что-то вроде следующего…

… Но вместо объекта r2d2, позволяющего просматривать значение, на которое ссылается его переменная экземпляра @name, мы получаем следующую ошибку ...

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

Поведение, атрибуты и соответствующие им экземпляры

Поведение и методы экземпляра

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

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

Этот путь поиска метода - это путь, который будет проходить наша программа при поиске любого метода экземпляра, вызываемого для объекта класса Robot. Первым в списке будет искать класс Robot, затем класс Object, модуль Kernel и, наконец, класс BasicObject. Если поведение определено в любом из классов / модулей по этому пути, то метод экземпляра, соответствующий этому поведению, будет доступен для объектов класса Robot. В этом смысле мы можем рассматривать методы экземпляра как экземпляры поведения, определенного либо в классе, либо где-то внутри его иерархии наследования классов.

Давайте вызовем метод экземпляра talk для наших объектов Robot, чтобы просмотреть "разговорное" поведение, определенное в нашем классе Robot.

Вызов метода talk для каждого из двух различных объектов Robot выводит на экран одно и то же сообщение: I'm a robot, and I can talk.. Само сообщение не имеет отношения к нашему обсуждению, и действительно, мы могли бы определить «разговорное» поведение для выполнения любого количества вещей. Здесь важно понять, что каждый из наших объектов Robot способен выполнять указанное поведение только при вызове talk метода экземпляра, потому что мы уже определили это поведение в нашем Robot классе - сущность предшествует существованию.

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

Атрибуты и переменные экземпляра

Атрибуты имеют одно необходимое свойство и два условных свойства. Вы не можете определить атрибут без необходимого свойства, присущего этому определению, но вы можете определить атрибут без двух случайных свойств.

Необходимое свойство

Необходимым свойством любого атрибута является функция предопределения состояния того или иного объекта. Состояние каждого объекта получает свою универсальную форму или сущность из атрибутов, определенных в классе объекта и любых классах в иерархии наследования. Таким образом, состояние объекта должно и действительно отслеживает атрибуты, определенные в классе объекта и иерархии наследования классов3. Это означает, что объект может иметь только переменные экземпляра, которые были предопределены атрибутами, определенными в его соответствующем классе и иерархии наследования. .

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

Это приводит к очень важному и, возможно, спорному моменту, касающемуся якобы переменных, имена которых начинаются с @. Например, в приведенном выше определении класса Robot у нас есть @name. Казалось бы, мы имеем дело с переменной экземпляра, но внешний вид может вводить в заблуждение. Вот важный момент, который Дэвид Фланаган и Юкихиро Мацумото делают о переменных экземпляра в своей книге Язык программирования Ruby: «Все объекты Ruby имеют набор переменных экземпляра. Они не определяются классом объекта - они просто создаются, когда им присваивается значение ». ⁴ Классы не определяют переменные экземпляра, они определяют атрибуты. Переменные экземпляра не существуют до создания объекта и присвоения им значения.

Различие между атрибутами и переменными экземпляра важно, потому что может возникнуть соблазн использовать термины «атрибут» и «переменная экземпляра» как взаимозаменяемые. Это нормально, пока сохраняется различие между тем, что такое класс и как он функционирует, и тем, что такое объект и как он функционирует. Но если мы хотим избежать двусмысленности, было бы лучше говорить о @name как о переменной экземпляра в контексте ее принадлежности к определенному объекту, а о @name как о указателе атрибута внутри контекст определения класса. В нашем определении класса Robot наличие указателя атрибута @name предопределяет существование переменной экземпляра @name, наследуемой каждому объекту класса Robot. Очевидно, контекст имеет значение.

Тот факт, что контекст имеет значение, не должен вызывать столько споров. Сам язык программирования Ruby не имеет абсолютной суверенной власти над использованием символа@symbol. По крайней мере, на английском языке он заменяет «at» в определенных контекстах и ​​используется в качестве префикса для дескрипторов Instagram людьми, которые понятия не имеют, что такое переменная экземпляра (представьте, что мы начали называть их переменными экземпляра Instagram) . Таким образом, говорить о @name как о значении атрибута в контексте определения класса и как о переменной экземпляра в контексте любого конкретного объекта класса не должно быть проблематичным. Конечно, «указатель атрибута» кажется громоздким.

Условные свойства

Два случайных свойства атрибутов являются поведениями, что означает, что существует некоторое совпадение между атрибутами и поведениями (мы объясним это более подробно ниже). Эти два случайных свойства соответствуют двум типам методов доступа Ruby: 1) методам получения и 2) методам установки. Чтобы проиллюстрировать, что мы подразумеваем под методами получения и установки, давайте определим поведение в нашем Robot классе, которое будет соответствовать каждому из них.

Оба метода получения и установки, определенные выше, выглядят как поведения, и, как уже упоминалось выше, так оно и есть. Но они также атрибуты; или, более конкретно, они являются случайными свойствами нашего атрибута «name». Каждое свойство является условным, и хотя мы можем определить любое из них без уже определенного атрибута, после определения любого из них атрибут обязательно определяется.

Например, давайте полностью переопределим наш Robot класс без initialize метода конструктора, но с поведением, соответствующим методу name получателя.

Опять же, может показаться, что мы просто определили два поведения в нашем классе. Но первое поведение, определение name метода получения, также является атрибутом, потому что оно содержит строку кода, которая удовлетворяет необходимому свойству любого атрибута. Состояние любого объекта класса Robot будет предопределено атрибутом 'name', определенным в нем, так что совокупность состояния любого объекта Robot будет состоять из переменной экземпляра @name.

Особенность этого атрибута «name», который отличает его от атрибута «name», который мы определили в нашем первом определении класса Robot, заключается в том, что этот атрибут «name» обладает поведенческим свойством, заключающимся в том, что он может быть публично просмотрен. Помните, что когда мы попытались просмотреть значение, связанное с нашей переменной экземпляра @name для объекта r2d2 Robot, добавив к этому объекту name выше, было возвращено сообщение об ошибке «undefined method». Но с name методом получения мы можем получить прямой доступ к переменной экземпляра @name объекта и просмотреть значение, связанное с ней.

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

Давайте проверим их с помощью одного из наших объектов Robot.

Успех! Мы можем как просматривать, так и изменять значение, связанное с переменной экземпляра @name объекта c3p0.

Методы получения и установки настолько распространены, что Ruby имеет встроенный способ определения их внутри класса. Мы можем использовать attr_reader для определения метода получения, attr_writer для определения метода установки или attr_accessor для одновременного определения метода получения и установки. Каждый из этих методов принимает в качестве аргумента символ. Вот как выглядел бы наш код, если бы мы заменили определения наших name метода-получателя и name= метода-установщика одним attr_accessor.

И мы не потеряли ни одной функциональности ...

… Поскольку мы можем просматривать и изменять значение, связанное с переменной экземпляра @name нашего объекта c3p0.

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

Инкапсуляция

Здесь может быть полезно подумать о локальных переменных Ruby. Предположим, мы просто инициализируем локальную переменную с именем robot_name и присваиваем ей значение "Buzz".

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

Получаем то, что хотели. Что если мы попытаемся изменить значение, связанное с локальной переменной robot_name, переназначив ему новое значение?

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

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

Примечание об отслеживании состояния и атрибутов

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

Теперь давайте создадим новый экземпляр или объект класса Robot.

Если мы вызовем метод p для нашего объекта alexa Robot, будет возвращено следующее ...

Мы видим, что у объекта alexa есть переменная экземпляра @name, связанная со значением "Alexa". Эта переменная экземпляра отслеживает состояние объекта, но состояние объекта отслеживает атрибуты, определенные в классе для объекта. Поясним.

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

Не создавая никаких новых объектов Robot, давайте посмотрим, что произойдет, когда мы снова вызовем метод p для нашего объекта alexa.

Хммм… на первый взгляд может показаться, что наш объект не имеет переменной экземпляра @musical, которая соответствовала бы недавно определенному атрибуту «музыкальный» в определении нашего класса. Но это не означает, что состояние объекта не знает о недавно добавленном «музыкальном» атрибуте. Действительно, он знает, поэтому мы можем запустить следующий фрагмент кода, не вызывая исключения или ошибки.

Возвращаемое значение nil может показаться неинтересным, поскольку мы еще не присвоили значение какой-либо переменной экземпляра @musical. Однако этот небольшой пример имеет огромное значение, поскольку он демонстрирует, что каким-то образом нечто, имеющее отношение к нашему alexa объекту, осознает существование чего-то вроде «музыкального» атрибута. Это что-то и есть состояние объекта. Состояние отслеживает все атрибуты, определенные в классе. Если это не так, мы должны ожидать, что alexa.musical вернет сообщение об ошибке вместо nil, как это сделала бы неинициализированная локальная переменная, если бы мы попытались сослаться на нее.

Этот пример предлагает еще один важный момент, который необходимо сделать. Причина того, что наш вызов метода p в alexa не привел ни к какому «музыкальному» атрибуту, ни к соответствующей переменной экземпляра @musical, заключается в том, что переменная @musical пока не инициализирована. Все неинициализированные переменные экземпляра ссылаются на nil. Переменная экземпляра @musical на данный момент имеет определенное скрытое существование. Это не несуществующее, но его существование является разновидностью нулевого существования, или nil существования.

Чтобы немного воплотить этот пример в жизнь, предположим, что у нас есть робот по имени Алекса, и Алекса была создана с потенциалом для музыкальных способностей. В настоящее время Алекса не играет ни на каких инструментах и ​​не поет, но у нее есть скрытые музыкальные способности, которые существуют в потенциале. Ее музыкальные способности не имеют положительного существования. Ее музыкальные способности - не пустяк, как это было бы сказано для рока, но они имеют своего рода нулевое или нулевое существование (говорить о музыкальных способностях рока - значит говорить чепуху). Тем не менее, даже в мире чисел ноль все еще имеет значение.

Но теперь допустим, что Алекса начинает учиться игре на гитаре. Внезапно музыкальные способности Алексы обрели позитив - теперь у нее есть музыкальные способности. Это как если бы занятия на гитаре инициализировали музыкальный потенциал Алексы и вывели его из нулевого экзистенциального состояния, сделав его реальной реальностью, а не просто формальной возможностью. Если бы мы попытались воспроизвести этот пример с помощью Ruby, мы могли бы определить несколько «практических» поведений, относящихся к различным музыкальным инструментам в нашем Robot классе.

Давайте инициализируем музыкальные возможности alexa, вызвав методы practice_guitar и practice_piano.

Состояние alexa было преобразовано, поскольку переменная экземпляра @musical была инициализирована и ей были присвоены новые значения. Обратите внимание, что вызов метода practice_guitar инициализирует переменную экземпляра @musical; вызов метода musical getter в тернарном операторе возвращает значение nil, связанное с переменной экземпляра @musical, и это значение falsey заставляет тернарный оператор оценивать значение false, что, в свою очередь, приводит к разделу self.musical = ['I play guitar'] кода справа от выполняемого оператора : вместо раздела self.musical << 'I play guitar' кода.

Однако, поскольку переменная экземпляра @musical уже была инициализирована, когда мы вызываем метод practice_piano на alexa, метод получения musical возвращает ["guitar"], которое является истинным значением, и, следовательно, тернарный оператор оценивает значение true. Это приводит к выполнению раздела кода self.musical << 'I play piano' вместо раздела кода self.musical = ['I play piano'].

Скрытые музыкальные способности объекта alexa теперь реализованы через практику игры на гитаре и фортепиано. Она все тот же робот, но ее состояние изменилось - она ​​такая же, но другая. Мы могли бы продолжить преобразование состояния alexa, вызвав метод practice_singing на alexa, и переменная экземпляра @musical будет отслеживать это изменение. Таким образом, в то время как состояние отслеживает атрибуты объекта, переменные экземпляра отслеживают состояние объекта.

Заключительные замечания

Подводя итог, вот список некоторых из основных затронутых вопросов:

  1. Классы определяют сущность объектов, состоящую из атрибутов и поведения.
  2. Объекты создаются из классов и предопределены определением класса.
  3. Состояние объекта отслеживает атрибуты класса, а переменные экземпляра объекта отслеживают его состояние.
  4. Поведение класса предопределяет методы экземпляра, доступные каждому конкретному объекту класса.
  5. Атрибуты класса предопределяют переменные экземпляра, относящиеся к каждому конкретному объекту класса.
  6. Атрибуты могут обладать двумя случайными поведенческими свойствами, и случайность этих двух свойств является предварительным условием инкапсуляции.

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

Мы также не особо много говорили о концепции наследования, за исключением упоминания иерархии наследования классов. Но, основываясь на общей модели, изложенной здесь, мы можем сказать, что когда один класс наследуется от другого, он наследует как атрибуты, так и поведение. Основываясь на проведенном нами различии между атрибутами и переменными экземпляра, должно быть очевидно, что классы не наследуют переменные экземпляра. Переменные экземпляра уникальны для своих конкретных объектов и не существуют вне их.

И последнее. Мы подчеркнули, что в парадигме ООП в Ruby сущность предшествует существованию. Однако может быть небольшое исключение - утиная печать. Если он ходит как утка и крякает как утка, значит, это должна быть утка. Независимо от того, из какого класса создается конкретный объект, если он может вести себя аналогично тому, как ведут себя объекты, созданные из других классов, то такие объекты можно рассматривать как общий тип для определенных приложений. Таким образом, в определенном смысле «утиная печать» свидетельствует о том, что иногда поведение объекта определяет, что он собой представляет. Таким образом, экзистенциалисты правы: существование действительно может предшествовать сущности.

Конечно, Платон утверждал, что даже поведение должно быть кодифицировано до того, как какое-либо существо сможет его выполнять. Объект не может вести себя противоречащим его сущности; если поведение не определено в классе объекта или иерархии наследования классов, объект не сможет вести себя подобным образом. Но приятно, что даже Ruby дает нам динамизм и двусмысленность, присущие реальному миру.

Вдохновение для написания этого сообщения в блоге было вызвано вопросами, которые возникли, когда я готовился к тесту по ООП в Launch School, онлайн-школе программной инженерии, основанной на совершенствовании программного обеспечения, и последующим обсуждением, которое я провел с сокурсниками и техническими специалистами. . Спасибо всем, кто участвовал в этом обсуждении.

Примечания

  1. Запустить школу. Объектно-ориентированное программирование. « Классы определяют объекты ». Доступ 18 января 2020 г.
  2. Запустить школу. Объектно-ориентированное программирование. Состояния и поведения. По состоянию на 18 января 2020 г.
  3. Там же.
  4. Фланаган, Дэвид и Юкихиро Мацумото. Язык программирования Ruby. Калифорния: O’Reilly Media, 2008 г. (стр. 240).
  5. Запустить школу. Объектно-ориентированное программирование. Почему объектно-ориентированное программирование?. Доступ 18 января 2020 г.