Перейти к основному содержимому

Объектно-ориентированная модель Smalltalk

Разработчику Архитектору
Сначала — общие понятия (раздел 4 "Код")

Если ООП для вас новое, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделезачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.

Ниже — живая ООП-модель 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 + 43 + 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 в классе
ИнтерфейсПротокол (группа селекторов) или соглашение, что объект понимает нужные сообщения
Вызов методаОтправка сообщения — выбор метода по селектору и получателю
thisself — получатель текущего сообщения
Наследованиеsubclass: / цепочка superclass
ПолиморфизмОдин селектор — разные реализации; утиная типизация (duck typing) — объект подходит, если отвечает на нужные сообщения
МетаклассОбъект класса Person class — тоже объект, создаёт экземпляры Person

Класс, метакласс и объект

Класс как объект

Object subclass: #Person
instanceVariableNames: 'name age'
classVariableNames: ''
poolDictionaries: ''
category: 'MyApp-Model'

Разбор:

  • Object subclass: #PersonPerson наследует от 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 (модель сообщений, см. полиморфизм).

Селекторы, протоколы и методы

Виды селекторов

ТипПример селектораВызов
УнарныйprintStringx 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 — одиночное наследование классов. Иерархия линейна: DogAnimalObject.

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 принято:

  1. Композиция — объект содержит другие объекты и пересылает им сообщения.
  2. Traits — переиспользуемые наборы методов, подключаемые к классу без цепочки super.
  3. Блоки — объекты с замыканием на контекст (анонимные фрагменты поведения).
"Упрощённое делегирование"
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 / PharoJava / 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 MethodsubclassResponsibility в суперклассе.

Чек-лист по ООП в Pharo

  1. Объяснить разницу между Person new и person initialize.
  2. Найти метакласс Person в Inspector.
  3. Написать иерархию Shape / Circle с полиморфным area.
  4. Реализовать BankAccount с инвариантом баланса без публичных полей (см. инкапсуляцию).
  5. Перехватить неизвестное сообщение через doesNotUnderstand: (эксперимент в Playground).

Дальше по языку: справочник Smalltalk, синтаксис, зачем объекты.


Учебные примеры ООП

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

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

Чертёж класса Figure и конкретные объекты — круг и квадрат.

Код ITЗагрузка примера кода…


Банковский счёт

Инкапсуляция: скрытое поле баланса и методы deposit/withdraw.

Код ITЗагрузка примера кода…


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

Родитель Animal и дочерние Cat и Dog с общим eat() и своим speak().

Код ITЗагрузка примера кода…


Смартфон

Состояние объекта: заряд батареи, звонки и подзарядка.

Код ITЗагрузка примера кода…


Студент

Список оценок, средний балл и проходной порог.

Код ITЗагрузка примера кода…


Корзина покупок

Взаимодействие Product, Cart и Order при оформлении заказа.

Код ITЗагрузка примера кода…


Автомобиль

Пробег, расход топлива и напоминание о техобслуживании.

Код ITЗагрузка примера кода…


Пользователь

Скрытый пароль, вход в систему и публикация сообщений.

Код ITЗагрузка примера кода…