Перейти к содержанию

Smalltalk в примерах/Основы

Материал из Викиучебника — открытых книг для открытого мира

Основы

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

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

Объекты

[править]

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

Каковы свойства объекта? Объект обычно содержит некоторую информацию, и он обычно имеет некоторое поведение --- он должен что-то делать. Наш объект ценная бумага содержит информацию: её название, её текущую цену, количество !!!, !!!. Она также имеет поведение: она должна давать ответ о новой цене и она должна накапливать общее количество сделок.

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

Если фондовая биржа передаёт информацию !!!, она посылает сообщение покупка: количество цена: цена объекту XYZ (мы будем подробнее говорить о сообщениях в Главе 2, Сообщения). Когда объект акция получает сообщение, он должен обновить свои данные --- в данном случае он должен обновить текущую цену, количество процентов последней сделки и общее количество сделок за день. Объект содержит собственные данные плюс сообщения которые он понимает.

Наш объект акция содержит данные: название акции, обозначение, текущую цену, объём последней сделки и объём продаж за день; и сообщения: имяАкции, обозначение, цена, последняяСделка, объёмСделок, и покупка:цена: (плюс некоторые другие). Данные хранятся в переменных экземпляра. Каждый объект акция имеет свою собственную копию переменных экземпляра, так что каждая акция может хранить различные значения цены, обозначения, и т.д.

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

[править]

Когда ты посылаешь сообщение покупка: количествоАкций цена: цена конкретному объекту акция, он обновляет переменные экземпляра цена, последняяСделка и объёмСделок. Не существует другого пути для изменения этих переменных. Ни один другой объект не может прочитать или изменить данные объекта XYZ. Это можно сделать только посылая объекту XYZ сообщение. Некоторые сообщения обновляют данные объекта акция, и некоторые сообщения возвращают значения. Но ни один объект не может получить доступ к данным напрямую. Данные инкапсулированы в объекте.

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

Классы

[править]

Класс как хранилище кода

[править]

Допустим что наша программа использует две УпорядочненныеСовокупности (совокупность которая хранит элементы в порядке их добавления). Одна хранит телефонные сообщения которые мы получили но на которые не ответили, и другая хранит действия которые мы должны сделать --- элементы действия. Между тем, нам нужны сообщения для добавления элемента в нашу совокупность, и извлечения первого элемента из совокупности.

Мы не должны писать методы добавить: и первый дважды, один раз для каждого объекта. Вместо этого мы нуждаемся в механизме для написания кода однажды и использования его для обеих УпорядоченныхСовокупностей. Вот откуда появляется концепция класса. Класс это просто проект или шаблон для того как объект должен выглядеть: какие переменные он содержит и какие сообщения он понимает. Мы определили класс УпорядоченнойСовокупности в котором мы написали код для сообщений добавить: и для первый. Таким образом вы можете рассматривать класс УпорядоченнаяСовокупность как хранилище для кода который выполняется когда вы посылаете сообщение. Написав код однажды все УпорядоченныеСовокупности (в нашем случае две) могут выполнять его.

Код хранится в методах. Когда объект получает сообщение он выполняет метод с тем же именем. Таким образом мы написали методы и они хранятся в классе.

Класс как фабрика

[править]

Следующий возникающий вопрос как наша УпорядоченнаяСовокупность создаётся. Как-никак она не появляется с помощью магии. Ответ состоит в том что мы просим класс УпорядоченнаяСовокупность создать два экземпляра УпорядоченнойСовокупности. Следующий код показывает как создаются, и присваиваются переменным, два экземпляра УпорядоченнойСовокупности (:= это оператор присвоения).

телефонныеСообщения := УпорядоченнаяСовокупность новый. действия := УпорядоченнаяСовокупность новый.

Таким образом наряду с тем что класс хранилище для кода он ещё является фабрикой создающей экземпляры себя. Точно так же как автомобильный завод производит машины фабрика (класс) УпорядоченнаяСовокупность производит УпорядоченныеСовокупности. Фабрика содержит проект для создания объектов, и шаблон на который похож объект - для данных содержащихся в нём.

Сейчас давайте на момент вернёмся назад на один шаг и посмотрим на код выше по тексту который создаёт экземпляры УпорядоченнойСовокупности. Обратите внимание что мы посылаем сообщение новый УпорядоченнойСовокупности. Вспомните что мы просим объекты выполнять некоторые вещи посылая им сообщения. Данный код выглядит так как будто класс УпорядоченнаяСовокупность является объектом, и это действительно так. Не только наш собственный экземпляр класса УпорядоченнаяСовокупность является объектом, но и фабрика которая создаёт его. Мы называем классом объект являющийся фабрикой, и экземпляром каждый объект который создаёт фабрика. Так в нашем случае мы имеем класс УпорядоченнаяСовокупность который создаёт два экзэмпляра УпорядоченнойСовокупности.

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

Класс как абстракция

[править]

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

Наследование

[править]

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

Большая разница между СортированойСовокупностью и УпорядоченнойСовокупностью это то что при создании элементов они сортируются на основе правила которое мы можем определить. Однако, большая часть поведения подобна и поэтому большинство кода должно быть одинаковым. Досадно дублировать весь код УпорядоченнойСовокупности для СортированойСовокупности. Не только потому что надо сделать много работы, но и потому что надо также обновлять код СортированойСовокупности если вы сделаете изменения в УпорядоченнойСовокупности.

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

К счастью мы можем делать это используя наследование между классами. В объектно ориентированном мире, мы можем сказать что один класс является подклассом другого класса. Так в нашем примере, СортированаяСовокупность это подкласс УпорядоченнойСовокупности. Это позволяет СортированойСовокупности наследовать весь код и переменные экземпляра УпорядоченнойСовокупности. Например, если вы хотите перебрать все элементы в СортированойСовокупности, вы посылаете сообщение делать:, которое определено в УпорядоченнойСовокупности. СортированаяСовокупность наследует код и её экземпляры могут делать то же самое что могут делать экземпляры УпорядоченнойСовокупности.

Если вы не писали код для СортированойСовокупности, она должна наследовать всё что написано для УпорядоченнойСовокупности. Фактически, если вы не изменили некоторое поведение, совсем не имеет смысла создавать новый класс. К счастью, СортированаяСовокупность имеет некоторые отличия в поведении. Есть два типа различий в поведении. Первое, некоторые сообщения должны делать различные вещи. Например, отправка сообщения добавить: должна добавить объект в конец УпорядоченнойСовокупности, но в случае СортированойСовокупности объект надо добавить в позицию основанную на правиле сортировки для совокупности. Если мы сделаем:

упорядоченнаяСовокупность := УпорядоченнаяСовокупность новый. упорядоченнаяСовокупность добавить: 'xyz'. упорядоченнаяСовокупность добавить: 'def'. упорядоченнаяСовокупность добавить: 'abc'.

и рассмотрим совокупность, строки должны быть в следующем порядке 'xyz', 'def', 'abc', в порядке в котором мы добавили их. С другой стороны, если мы сделаем:

сортированаяСовокупность := СортированаяСовокупность новый. сортированаяСовокупность добавить: 'xyz'. сортированаяСовокупность добавить: 'def'. сортированаяСовокупность добавить: 'abc'.

и рассмотрим совокупность, строки будут в следующем порядке 'abc', 'def', 'xyz', в правильной отсортированной последовательности для строк. Таким образом, в СортированойСовокупности мы не должны наследовать код суперкласса для добавить:. Вместо этого мы пишем свой собственный код для метода добавить: и подменяем код определённый в УпорядоченнойСовокупности.

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


Полиморфизм

[править]

Помните что сообщения add: различны для OrderedCollection и для SortedCollection? Есть другие типы совокупностей таких как Множество, Мешок, СвязаныйСписок каждая из которых определяет свою собственную версию добавить:.

Это означает что вы можете использовать совокупность и не заботиться о том какого она типа; вы просто посылаете сообщение добавить: и она добавляет объект в себя правильным образом. Другим примером может быть окно которое показывает графические объекты. Вместо того чтобы знать о том как изображать окружность и квадрат, окно должно просто послать сообщение графическому объекту ( например: графическийОбъект изобразиСебяНа: сам). графическийОбъект может быть квадратом или окружностью, но для окна это не важно. Оно просто посылает одинаковые сообщения независимо от типа объекта, и полагает что графическийОбъект знает как изобразить себя. На процедурном языке вы можете написать приблизительно так:

если( графическиОбъект квадрат? )

    изобразитьКвадрат( графическийОбъект )

иначе если( графическийОбъект окружность? )

    изобразитьОкружность( графическийОбъект )

Используя полиморфизм, вы можете просто написать:

графическийОбъект изобразитьСебяНа: мне.

Рассмотрим другой пример, мы имеем объект Офис который видит объекты Работник приходящих на работу. Один подход для объекта Офис спросить объект работник какого типа он работник. Если это объект Программист, объект Офис должен сказать ему начать программировать (программист начниПрограммировать). Если это служащий, объект Офис должен сказать ему отвечай на телефонные звонки (служащий отвечайНаТелЗвонки). Если это мэнеджер, объект Офис должен сказать ему работай с бумагами (мэнеджер работайСБумагами).

Такой подход неудобен из за того что надо иметь код единственная задача которого проверять какого типа объект Работник просто спрашивая его, и надо изменять данный код когда добавляется новый тип работника. Лучший подход для объекта Офис не интересоваться типом работника, а просто говорить ему делать работу(работник принимайсяЗаРаботу). Если объект Программист получает это сообщение, он должен начать программировать; если объект Служащий получает это сообщение, он должен начать отвечать на телефонные звонки; если объект Мэнеджер получает это сообщение, он должен начать работать с бумагами. Сейчас когда вы добавите новый тип работника, вы просто должны быть уверены что он отвечает на сообщение принимайсяЗаРаботу и делает соответствующую работу. Мы передали ответственность тем к кому она относится.

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

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

Абстрактные суперклассы

[править]

Сейчас давайте расширим идею наследования. СортированаяСовокупность это подкласс УпорядоченнойСовокупности. Другие подклассы УпорядоченнойСовокупности включают РегулярнуюСовокупность и СвязанныйСписок. УпорядоченныеСовокупности, Массивы и СвязанныеСписки имеют некоторое общее поведение которое определено в классе РегулярнаяСовокупность. Однако, вы не можете создать экземпляр РегулярнойСовокупности (вы можете, но вы получите ошибку если попытаетесь что-либо сделать с ней).

Класс УпорядоченнаяСовокупность существует как место для хранения кода который является общим для упомянутых классов. Общее поведение отделяется помещается в абстрактный суперкласс, который не может иметь экземпляров --- т.е. не должен создавать экземпляры себя. Например методы копироватьОт:до: и наличие---: оба написаны в РегулярнойСовокупности и наследуются её подклассами.

Таким образом, абстрактный суперкласс это класс который не имеет своих экземпляров, но который существует как хранилище для общего кода. Абстрактный суперкласс РегулярнаяСовокупность сам имеет абстрактный суперкласс, Совокупность. Совокупность это также суперкласс для Множества и Мешка, совокупностей для которых нет понятия порядка. Совокупность обеспечивает поведение которое является общим для всех совокупностей, такое как пустая?, собрать:, делать: и включает:. (Некоторые подклассы переопределяют эти методы для осуществления подходящего поведения. Однако, многие подклассы наследуют поведение напрямую.)

Резюме

[править]
  • Объекты инкапсулируют данные и поведение (код).
  • Класс это фабрика которая содержит план для создания экземпляров. Он является хранилищем для кода который выполняется его экземплярами.
  • Классы располагаются в иерархии наследования которая позволяет объектам наследовать поведение (и код) от других классов находящихся выше по иерархии.
  • Работа выполняется путём посылания сообщений другим объектам, говорящих им сделать что-либо или возвратить что-либо.
  • Многие различные объекты понимают одинаковые сообщения но делают различные вещи когда они получают их. То есть, они выполняют различные методы (полиморфизм).