Объектно-ориентированная модель Smalltalk
Если ООП для вас новое, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделе — зачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.
Ниже — живая ООП-модель Smalltalk в Pharo.
Теория и модель Smalltalk
Smalltalk — эталон сообщений и идей Алана Кэя (шесть принципов) — всё — объект, вычисление — обмен сообщениями, иерархия классов с корнем.
| Понятие ООП | В Pharo / Smalltalk |
|---|---|
| Сообщение | receiver selector argument — отправка имени метода объекту |
| Класс | объект, порождающий экземпляры |
| Инкапсуляция | инстанс-переменные, протоколы методов |
| Полиморфизм | ответ на сообщение зависит от класса получателя |
| Метакласс (с ST-80) | класс — тоже объект; метакласс (объект, описывающий сам класс) хранит методы класса (new, initialize) |
С Smalltalk-80 метаклассы делают буквальной формулу "всё — объекты": экземпляры, их классы и классы классов участвуют в одной модели сообщений. Подробнее о метаклассах — в справочнике, об общей идее — в абстракции и инкапсуляции.
Определения без привязки к языку — раздел 4-08-oop.
Кратко для новичка:
- Класс в Browser — объект-шаблон;
Person newотправляет сообщение классу создать экземпляр. - Селектор — имя сообщения (
name:,attack:); метод — ответ на селектор. - Переменные экземпляра скрыты; снаружи — только сообщения (геттеры и сеттеры).
- Наследование —
subclass:;superобращается к реализации в суперклассе. - Полиморфизм — один селектор
areaуCircleиRectangleв общем цикле.
В Pharo класс создают в Class Browser, методы группируют по протоколам; ниже — пример Fighter фрагментами по вкладкам браузера.
ООП в Smalltalk
Интерактивная схема — класс и объект (псевдокод; в Smalltalk "всё — объект"). Полный разбор принципов: ООП в разделе "Код и разработка".
КЛАСС Кот
поля: имя, возраст
метод мяукнуть()
КОНЕЦ
объект barsik := новый Кот(имя="Барсик", возраст=3)
barsik.мяукнуть()
Play ITЗагрузка интерактивного демо…
Пример класса
В Pharo класс создаётся в Class Browser (шаблон "New class"), а каждый метод — в отдельной вкладке протокола. Ниже — фрагменты, которые вводят по очереди в соответствующих вкладках.
Объявление класса (панель определения класса):
Object subclass: #Fighter
instanceVariableNames: 'name intel agility strength health'
classVariableNames: ''
package: 'Game-Entities'
Инициализация (протокол initialization):
initialize
super initialize.
name := 'Боец'.
intel := 10.
agility := 10.
strength := 10.
health := 100.
^ self
Поведение (протокол combat):
damage
^ intel + agility + strength
attack: aTarget
| dmg |
dmg := self damage.
Transcript show: name, ' → ', aTarget name, ': ', dmg printString; cr.
aTarget health: aTarget health - dmg
Доступ к состоянию (протокол accessing):
Код ITЗагрузка примера кода…
Запуск в Playground (после сохранения методов — Ctrl+S):
Код ITЗагрузка примера кода…
Выражение Object subclass: #Fighter создаёт класс-подкласс Object. instanceVariableNames перечисляет переменные экземпляра (у каждого объекта свои значения). package: — пакет в Pharo (аналог модуля в других языках).
Сообщение new создаёт экземпляр; дальше обычно вызывают initialize явно или переопределяют new, чтобы он вызывал initialize сам. Цепочка warrior name: 'Воин'; intel: 5; ... — каскад: каждый сеттер возвращает self (через yourself или неявно).
Метод attack: посылается объекту warrior, аргумент mage — другой Fighter. Состояние меняется через сеттер health:; низкоуровневый instVarAt:put: оставляют для отладки в Inspector.
Результат смотрите в окне Transcript. Любую строку из Playground можно выполнить по отдельности (Ctrl+G / Do it) и сразу увидеть эффект в живой системе.
Философия — всё объект и всё сообщение
Smalltalk — один из немногих языков, где ООП — единственная модель вычисления, без отдельного процедурного слоя. В Java есть static void main, в Python — функции верхнего уровня, в C — свободные функции. В Pharo любое выражение — отправка сообщения объекту:
| В других языках | В Smalltalk |
|---|---|
3 + 4 | 3 + 4 — сообщение + числу 3 с аргументом 4 |
if (x > 0) | x > 0 ifTrue: [ ... ] — условие как сообщение булеву значению |
Person.new() | Person new — сообщение new классу Person |
list.get(0) | list first — сообщение first коллекции |
Приоритет операторов не зашит в язык отдельным слоем: даже + и * — это сообщения объектам Number. Отсюда единая объектная модель: класс, число, блок кода, исключение — всё объекты с методами.
Общая теория без синтаксиса — введение в ООП, зачем объекты.
Терминология Smalltalk и общая ООП
| Понятие ООП | Термин в Smalltalk / Pharo |
|---|---|
| Класс (шаблон) | Class — объект, экземпляр метакласса |
| Объект (экземпляр) | Instance — обычный объект с собственным состоянием |
| Поле экземпляра | Instance variable (name, health) — приватны для объекта |
| Поле класса | Class variable (Count) — одно на иерархию классов |
| Переменная пула | Class instance variable / Shared pool — общие именованные ячейки |
| Метод | Method — ответ на селектор (имя сообщения, по которому объект выбирает реализацию) |
| Конструктор | Сообщения new, new:, initialize, переопределение new в классе |
| Интерфейс | Протокол (группа селекторов) или соглашение, что объект понимает нужные сообщения |
| Вызов метода | Отправка сообщения — выбор метода по селектору и получателю |
this | self — получатель текущего сообщения |
| Наследование | subclass: / цепочка superclass |
| Полиморфизм | Один селектор — разные реализации; утиная типизация (duck typing) — объект подходит, если отвечает на нужные сообщения |
| Метакласс | Объект класса Person class — тоже объект, создаёт экземпляры Person |
Класс, метакласс и объект
Класс как объект
Object subclass: #Person
instanceVariableNames: 'name age'
classVariableNames: ''
poolDictionaries: ''
category: 'MyApp-Model'
Разбор:
Object subclass: #Person— Person наследует отObject(корень иерархии в Pharo).instanceVariableNames:— имена переменных экземпляра (у каждогоPersonсвоиnameиage).classVariableNames:— переменные, общие для класса и всех подклассов (редко в учебном коде).category:— группировка в браузере классов (не влияет на семантику).- Символ
#Person— имя класса как литерал; строка в кавычках'name'— список имён полей через пробел.
Класс Person после определения — обычный объект. Его класс — метакласс Person class (объект, который создаёт экземпляры Person и хранит методы класса). Цепочка "всё есть объект" буквальна:
Person class "→ метакласс Person"
(Person class) class "→ метакласс метакласса"
Person superclass "→ Object"
Person allSelectors "→ все селекторы методов экземпляра"
Person allInstances size "→ число живых экземпляров Person в образе"
В Inspector (инспектор объекта) можно кликнуть любой объект и увидеть класс, переменные, методы — это основа рефлексии и отладки в Pharo.
Подробнее о метаклассах — справочник Smalltalk.
Создание экземпляра
| person |
person := Person new.
person initialize. "если нужна явная инициализация"
Разбор:
Person new— сообщение классуPerson. Класс (метакласс) создаёт пустой экземпляр и возвращает его.initialize— обычный метод, который вы реализуете в протоколеinitialization. По соглашению его вызывают послеnew, чтобы задать начальное состояние.- Переменная
personхранит ссылку на объект; присваивание:=копирует ссылку, не сам объект.
Часто переопределяют new, чтобы initialize вызывался автоматически:
Person class >> new
^ super new initialize
Разбор:
Person class >>— определение метода класса (сообщение, посылаемое самому классуPerson).super new— создание экземпляра через реализацию родительского метакласса.initializeв конце — гарантия, что каждый новыйPersonполучит начальное состояние.
Фабричные сообщения и параметризованное создание
Person class >> named: aString age: anInteger
^ self new
name: aString;
age: anInteger;
yourself
Разбор:
named:age:— ключевой селектор с двумя аргументами (двоеточия в имени).- Цепочка
name: ...; age: ...; yourself— каскад: каждый сеттер возвращаетself,yourselfявно возвращает получателя для присваивания. - Фабричное сообщение читается как
Person named: 'Анна' age: 30.
Запись в переменные и состояние объекта
| warrior mage |
warrior := Fighter new.
warrior
name: 'Воин';
health: 100;
intel: 8.
mage := Fighter new.
mage name: 'Маг'; mage health: 80.
warrior attack: mage. "сообщение attack: с аргументом mage"
Разбор:
- Локальные переменные объявляются в начале метода или Playground:
| warrior mage |. - Состояние экземпляра меняется только сообщениями его методов (или сеттерам внутри класса) — снаружи поля
healthнапрямую недоступны. warrior attack: mage— отправка селектораattack:объектуwarriorс одним аргументомmage(модель сообщений, см. полиморфизм).
Селекторы, протоколы и методы
Виды селекторов
| Тип | Пример селектора | Вызов |
|---|---|---|
| Унарный | printString | x printString |
| Бинарный | + | 3 + 4 |
| Ключевой (один аргумент) | name: | p name: 'Анна' |
| Ключевой (несколько) | name:age: | Person named: 'А' age: 20 |
Имя метода в коде совпадает с селектором. setName: aName — параметр aName локален; в сообщении пишут только name: 'Анна'.
Протоколы методов (группы в классе)
В Browser методы группируют по протоколам (accessing, initialization, combat, testing). Это организация кода внутри класса, а не отдельный тип как interface в Java. Для контракта поведения используют:
- абстрактные методы —
self subclassResponsibilityв теле; - traits (в Pharo) — композиция поведения без наследования;
- соглашение, что объект понимает сообщения
drawиbounds.
Shape >> draw
self subclassResponsibility
Подклассы обязаны реализовать draw, иначе при вызове получите понятное исключение.
Методы класса и метода экземпляра
"Метод экземпляра — в протоколе instance side"
Person >> greet
^ 'Привет, я ', name
"Метод класса — в протоколе class side (Person class)"
Person class >> description
^ 'Класс для моделирования людей'
Разбор:
- Методы экземпляра видят
selfкак конкретного человека. - Методы класса видят
selfкак объект-классPerson(удобно для фабрик и счётчиков).
Доступность и инкапсуляция
Интерактивная схема — инкапсуляция (псевдокод). Подробнее: Инкапсуляция.
Play ITЗагрузка интерактивного демо…
В Smalltalk нет ключевых слов public / private / protected на уровне полей:
- Переменные экземпляра доступны только методам того же класса (и подклассов для унаследованных имён). Из другого класса обращаются через геттер:
person name. - Инкапсуляция полей обеспечивается языком (см. инкапсуляцию).
- Инкапсуляция поведения — договорённость: публичный API класса — набор селекторов без префикса; внутренние методы иногда помечают категорией
privateили префиксом в имени.
BankAccount >> balance
^ balance
BankAccount >> deposit: anAmount
anAmount > 0 ifTrue: [ balance := balance + anAmount ].
^ self
BankAccount >> withdraw: anAmount
(balance >= anAmount) ifTrue: [ balance := balance - anAmount ]
ifFalse: [ self error: 'Недостаточно средств' ].
^ self
Разбор:
balance— переменная экземпляра; снаружи читают только через геттерbalance.- Прямой записи
account instVarAt: 1 put: -1000в прикладном коде избегают (это отладочный API). - Инвариант (правило, которое состояние объекта всегда соблюдает), например "баланс не отрицательный", реализуют в методе
withdraw:— в поведении класса.
По сравнению с Java/C#: там private на поле плюс публичные геттеры; в Smalltalk поля скрыты языком, а методы по умолчанию доступны всем — дисциплина API остаётся на совести автора.
Наследование
Интерактивная схема — наследование (псевдокод). Подробнее: Наследование.
Play ITЗагрузка интерактивного демо…
Smalltalk — одиночное наследование классов. Иерархия линейна: Dog → Animal → Object.
Object subclass: #Animal
instanceVariableNames: 'name'
classVariableNames: ''
package: 'Zoo'.
Animal subclass: #Dog
instanceVariableNames: 'breed'
classVariableNames: ''
package: 'Zoo'.
Разбор:
Dogнаследует методы и переменныеAnimal, добавляетbreed.- Поиск метода: сначала класс
Dog, затемAnimal, затемObject. - Множественное наследование классов отсутствует; вместо него — traits и композиция (объект содержит другой объект и пересылает сообщения).
Переопределение и super
Animal >> speak
^ '...'
Dog >> speak
^ 'Гав!'
Dog >> describe
^ super speak, ' Я собака породы ', breed
Разбор:
Dog >> speakпереопределяет селекторspeakизAnimal.super speak— отправкаspeakсуперклассу, не рекурсия вDog.- Конкатенация строк через запятую — это сообщения
,объектамString.
Абстрактные классы
Класс с методами subclassResponsibility рассчитан на подклассы — аналог абстрактного метода в Java (см. наследование).
Полиморфизм
Интерактивная схема — полиморфизм (псевдокод). Подробнее: Полиморфизм.
Play ITЗагрузка интерактивного демо…
Полиморфизм подтипов — один селектор, разные классы:
| shapes |
shapes := { Circle new. Rectangle new. Triangle new }.
shapes do: [ :each | Transcript show: each area; cr ]
Разбор:
- Достаточно, чтобы каждый элемент отвечал на
area— отдельныйinterface Shapeне обязателен. - Цикл
do:посылаетareaкаждому объекту; диспетчеризация динамическая (по классу получателя). - Это утиная типизация: объект подходит, если понимает сообщение
area.
Полиморфизм через блоки (стратегия без иерархии):
comparator value: a value: b
^ [ a name ] value < [ b name ] value
Блок [ ... ] — объект; value — отправка ему сообщения для вычисления.
Перехват неизвестных сообщений
Если объект не находит метод, вызывается doesNotUnderstand: — можно перехватить неизвестное сообщение (прокси, отладка, DSL). Это мощный, но опасный механизм метапрограммирования.
Абстракция
Интерактивная схема — абстракция (псевдокод). Подробнее: Абстракция.
Play ITЗагрузка интерактивного демо…
Абстракция в Smalltalk — набор сообщений, которые клиент может послать, без раскрытия представления данных:
"Клиенту всё равно, Circle или Rectangle — важен селектор area"
totalArea: aCollectionOfShapes
^ aCollectionOfShapes inject: 0 into: [ :sum :shape |
sum + shape area ]
Класс Morphic / UI в Pharo строится на том, что виджеты понимают общие сообщения (drawOn:, bounds, handleEvent:), а не на глубокой иерархии как в старых toolkit.
Композиция, traits и делегирование
Вместо глубокого наследования на все случаи в Pharo принято:
- Композиция — объект содержит другие объекты и пересылает им сообщения.
- Traits — переиспользуемые наборы методов, подключаемые к классу без цепочки
super. - Блоки — объекты с замыканием на контекст (анонимные фрагменты поведения).
"Упрощённое делегирование"
Forwarding >> methodMissing: aMessage
^ delegate perform: aMessage selector withArguments: aMessage arguments
Блоки, замыкания и итерация
| numbers evens |
numbers := #( 1 2 3 4 5 ).
evens := numbers select: [ :n | n even ].
evens do: [ :n | Transcript show: n; cr ]
Разбор:
[ :n | n even ]— блок (объект типаBlockClosure);:n— параметр.select:— полиморфное сообщение коллекции; блок — критерий.- Блоки захватывают окружение (замыкание) — основа коллекционных API и UI-колбэков.
Сравнение с Java, C# и Python
| Тема | Smalltalk / Pharo | Java / C# |
|---|---|---|
| Вызов | Сообщение | Вызов метода |
| Типы | Динамические; опционально Gradual typing в будущих ветках | Статические |
| Интерфейс | Протокол селекторов, утиная типизация | interface |
| Поля | Скрыты в языке | private + accessors |
| Класс | Объект (метакласс) | Описание в метаданных JVM/CLR |
| Среда | Образ (image) + Browser | Файлы + IDE |
| Ошибки | MessageNotUnderstood в рантайме | Часто на этапе компиляции |
С Python Smalltalk роднит идея "всё есть объект", но в Python есть модули и функции вне классов; в Pharo вычисление строится только на сообщениях.
Типичные ошибки новичка
| Ошибка | Что происходит | Как правильно |
|---|---|---|
Забыли initialize после new | Поля nil, сеттеры падают | Переопределить new или вызывать initialize |
Путают . и ; в каскаде | Неверный возврат из цепочки | ; для каскада, . — точка с запятой в конце выражения |
Пишут instVarAt: в приложении | Ломают инкапсуляцию | Только геттеры/сеттеры |
| Ожидают статическую типизацию | MessageNotUnderstood в рантайме | Тесты, протоколы, assert в initialize |
| Глубокая иерархия | Хрупкий super | Композиция и traits |
Паттерны, родившиеся в Smalltalk
Многие паттерны GoF (обзор) описаны на примерах Smalltalk:
- Model–View–Controller (MVC) — изначально для Pharo UI.
- Strategy — передача блока или объекта с общим селектором.
- Composite — коллекции, которые
drawделегируют детям. - Template Method —
subclassResponsibilityв суперклассе.
Чек-лист по ООП в Pharo
- Объяснить разницу между
Person newиperson initialize. - Найти метакласс
Personв Inspector. - Написать иерархию
Shape/Circleс полиморфнымarea. - Реализовать
BankAccountс инвариантом баланса без публичных полей (см. инкапсуляцию). - Перехватить неизвестное сообщение через
doesNotUnderstand:(эксперимент в Playground).
Дальше по языку: справочник Smalltalk, синтаксис, зачем объекты.
Учебные примеры ООП
Небольшие самодостаточные программы, которые показывают классы, объекты, инкапсуляцию, наследование и взаимодействие нескольких типов на одной предметной области.
Класс и объект
Чертёж класса Figure и конкретные объекты — круг и квадрат.
Код ITЗагрузка примера кода…
Банковский счёт
Инкапсуляция: скрытое поле баланса и методы deposit/withdraw.
Код ITЗагрузка примера кода…
Наследование
Родитель Animal и дочерние Cat и Dog с общим eat() и своим speak().
Код ITЗагрузка примера кода…
Смартфон
Состояние объекта: заряд батареи, звонки и подзарядка.
Код ITЗагрузка примера кода…
Студент
Список оценок, средний балл и проходной порог.
Код ITЗагрузка примера кода…
Корзина покупок
Взаимодействие Product, Cart и Order при оформлении заказа.
Код ITЗагрузка примера кода…
Автомобиль
Пробег, расход топлива и напоминание о техобслуживании.
Код ITЗагрузка примера кода…
Пользователь
Скрытый пароль, вход в систему и публикация сообщений.
Код ITЗагрузка примера кода…