Объектно-ориентированное программирование — итоги
Кратко — что стоит унести из раздела "Объектно-ориентированное программирование". Если пункт кажется туманным — откройте указанную главу или оглавление.
ООП на практике — одна история
Так, мы разобрались, что есть абстракция, инкапсуляция, наследование, полиморфизм. Как всё это работает на практике и зачем всё это нужно? Новичок может вообще не понять сути, ибо… а почему бы просто тупо не писать всё в одном классе? Давайте по порядку.
-
Если будем писать всё в одном коде — будут одинаковые названия методов, переменных и других элементов (или они станут слишком длинными и сложными для чтения), поэтому придётся всё группировать в какие-то логические блоки кода.
-
Если мы будем их все писать в одном файле, то когда код вырастет до тысяч строк, нам придётся мучаться. Поэтому, чтобы решить проблемы из пунктов 1 и 2 — давайте всё сгруппируем в блоки кода — классы. Чтобы всё не хранить в одном файле, каждый класс можно положить в отдельный одноимённый файл.
-
Чтобы код из одного файла мог работать с кодом из другого файла (читать свойства, вызывать методы), мы должны сделать их публичными, чтобы они были видимы и доступны другим.
-
Но если какой-то элемент класса не используется никем, кроме самого класса, то «публиковать» его и делать видимым — излишнее. Поэтому мы делаем их приватными или даже защищёнными.
-
А если нужно, чтобы кому-то понадобился приватный элемент, мы используем геттеры и сеттеры — заготовленные методы. Сеттер для изменения данных, геттер для получения. Если не хотим изменять, то убираем сеттер, оставляем геттер.
-
Представим, что нам нужна логика издавания звуков животными. Ок, сделали кошку и собаку. Потом добавили ещё и кролика. Теперь у нас есть три «зверька» — кошка, собака, кролик. Они все могут издавать какие-то звуки — кошка мяукает, собака гавкает, кролик пищит. Но что если мы решим добавить к логике мяукания ещё и логику прыгания? Да, мы добавим кошке, добавим собаке. А кролику логику прыгания добавить забыли. Программа успешно скомпилировалась, всё работает, собака и кошка прыгают, но лишь на тестировании заметили — а почему кролик не прыгает?
-
Поэтому мы переделаем всё. Мы сделаем интерфейс IAnimal, который описывает, что все его животные должны иметь определённые свойства (имя, возраст) и определённые методы (издавать звук и прыгать). Потом мы сделаем абстрактный класс Animal, который может содержать логику — звуки издают все по-разному, но прыгают все примерно одинаково, поэтому мы реализуем базовый метод
Jump(). Cat, Dog и Rabbit — три конкретных класса-наследника, которые обязаны реализовать требования родителя. И если в Rabbit забудем прыжок — программа выдаст ошибку компиляции! Эти требования и есть контракт. -
Чтобы переопределить прыжок для собаки (ведь она прыгает выше других), мы реализуем метод с другой внутренней логикой — это переопределённый метод. Аналогично можно скорректировать прыжок и для кошки.
-
Собака может приносить мяч, а кошка вылизываться. Это уникальные методы конкретных классов — кроме базовых наследуемых элементов, у класса могут быть свои.
-
Но это лишь классы — чертежи. Чтобы они стали конкретными, нужно создать объекты через конструктор.
-
Кошка может позвать собаку, чтобы она принесла мячик — тогда кошка вызывает метод объекта собаки: сначала
new Dog("Шарик"), затемШарик.ПринестиМяч().
Так всё это и работает. Теперь мы можем создавать новые виды животных, а компилятор подскажет, что любое животное должно как минимум иметь имя, возраст, уметь издавать звуки и прыгать.
FAQ — Часто задаваемые вопросы
Типичные ошибки и путаницы при первом ООП-проекте. Здесь — практические ответы и ссылки на главы; формулировки для самопроверки — в чек-листе.
Вопрос. Сделал один класс на всё приложение ("God object") — как понять, что пора дробить?
Ответ. Признаки — файл на тысячи строк, десятки несвязанных методов, изменение "про заказы" ломает "про отчёты". Делите по зонам ответственности предметной области. Подробнее здесь — Сложность и декомпозиция, Введение в ООП.
Вопрос. Преподаватель требует "всё в классах", а задача — посчитать сумму массива.
Ответ. ООП уместен, когда есть устойчивая модель объектов и срок жизни кода. Для разового расчёта достаточно функции; навязанный класс-утилита без состояния только мешает. Подробнее здесь — Введение в ООП, Парадигмы.
Вопрос. Два объекта с одинаковыми полями — == в Java/C# даёт false. Почему?
Ответ. Переменная часто хранит ссылку, а не копию объекта: сравниваются адреса, а не содержимое. Для значений по полям переопределяйте equals/Equals или сравнивайте нужные поля явно. Подробнее здесь — Введение в ООП.
Вопрос. Изменил объект через одну переменную — "сломалось" в другой части программы.
Ответ. Обе переменные указывают на один экземпляр. Нужны копии (клон, конструктор копирования) или неизменяемые объекты, если разделять состояние нельзя. Подробнее здесь — Введение в ООП.
Вопрос. На каждое поле написал геттер и сеттер — код раздулся, пользы мало.
Ответ. Инкапсуляция — про инварианты и границы, а не про пару методов на поле. Скрывайте то, что нельзя менять снаружи; оставляйте осмысленные операции ("забронировать", "отменить"). Подробнее здесь — Инкапсуляция.
Вопрос. public поля "для простоты" — коллега меняет цену в минус.
Ответ. Открытое поле не может отклонить неверное значение. Используйте закрытое поле + свойство/метод с проверкой или тип, который не допускает некорректное состояние. Подробнее здесь — Инкапсуляция.
Вопрос. Наследовался от класса ради одного метода — иерархия разрослась.
Ответ. Наследование связывает типы жёстко. Для переиспользования поведения чаще подходит композиция (поле-делегат, стратегия). Подробнее здесь — Наследование, Полиморфизм.
Вопрос. Вызов метода у родителя, а выполнился код потомка — "магия", не понимаю цепочку.
Ответ. Это динамическое связывание: реальный тип объекта в runtime выбирает переопределённую версию. Статический тип переменной может быть базовым. Подробнее здесь — Полиморфизм.
Вопрос. Перегрузил метод, компилятор ругается "неоднозначный вызов".
Ответ. Несколько перегрузок подходят одновременно — уточните типы аргументов, приведите литералы (1.0 vs 1) или переименуйте методы с разным смыслом. Подробнее здесь — Полиморфизм.
Вопрос. Путаю переопределение (override) и перегрузку (overload).
Ответ. Перегрузка — разные сигнатуры в одном классе, выбор на этапе компиляции. Переопределение — та же сигнатура в иерархии, выбор по фактическому типу объекта. Подробнее здесь — Полиморфизм.
Вопрос. Абстрактный класс или интерфейс — что выбрать в Java/C#?
Ответ. Интерфейс задаёт роль "умеет" (can-do) — контракт без обязательной общей реализации. Абстрактный класс объединяет родственные типы "является" (is-a) и может хранить поля с общим кодом. Подробная таблица — Абстракция, раздел про абстрактный класс и интерфейс.
Вопрос. Забыл вызвать super() в конструкторе потомка — странные нули в полях.
Ответ. Сначала должен отработать конструктор базового класса, иначе поля родителя не инициализированы. В некоторых языках вызов вставляется автоматически, в других — обязателен явно. Подробнее здесь — Введение в ООП.
Вопрос. null / None при вызове метода — падение, хотя "объект был".
Ответ. Ссылка пустая: объект не создан или уже освобождён. Проверяйте до вызова, используйте optional-типы, фабрики, которые не отдают пустоту без договорённости. Подробнее здесь — Введение в ООП.
Вопрос. Строковые константы вместо enum — опечатка в статусе заказа прошла в прод.
Ответ. Enum ограничивает допустимый набор, ловится компилятором/линтером, удобен в switch. Строки оставляйте для внешних API с явной валидацией. Подробнее здесь — Перечисления.
Вопрос. Храню всё в ArrayList без типа — ClassCastException в runtime.
Ответ. Используйте обобщённые коллекции (List<Order>), чтобы ошибка типа всплыла при компиляции. Сырые коллекции — наследие старых API. Подробнее здесь — Коллекции.
Вопрос. Удалил объект из списка в цикле foreach — ConcurrentModificationException.
Ответ. Менять коллекцию во время обхода итератором нельзя. Соберите элементы на удаление отдельно, используйте removeIf или итерируйте с итератором remove(). Подробнее здесь — Коллекции.
Вопрос. Память растёт, хотя объекты "уже не нужны" — утечка через события?
Ответ. Подписчик держит ссылку на издателя (или наоборот). Отписывайтесь при уничтожении формы/контроллера, используйте слабые ссылки там, где это поддерживает платформа. Подробнее здесь — Введение в ООП, Сборка мусора.
Вопрос. Финализатор/destructor не вызывается вовремя — файл всё ещё занят.
Ответ. Освобождение ресурсов полагайте на using, try-with-resources, dispose, а не на сборщик мусора. GC не гарантирует момент вызова финализатора. Подробнее здесь — Введение в ООП.
Вопрос. "Сокрытие" и "инкапсуляция" в учебнике — одно слово?
Ответ. В ряде школ сокрытие (private) — способ достичь инкапсуляции (целостность объекта), но инкапсуляция шире: объединение данных и поведения. Подробнее здесь — Инкапсуляция.
Вопрос. Интерфейс с одним методом — зачем не просто функция?
Ответ. Контракт позволяет подставлять разные реализации в полиморфный код и тестировать через заглушки; в языках с функциональными типами роль близка к типу функции. Подробнее здесь — Абстракция, Зависимости.
Вопрос. Go/Rust "без классического ООП" — значит раздел бесполезен?
Ответ. Идеи инкапсуляции, контрактов, композиции переносятся на структуры, трейты, интерфейсы. Синтаксис другой, вопросы границ модулей те же. Подробнее здесь — О разделе.
Вопрос. Скопировал объект — изменил копию, оригинал тоже изменился (вложенные списки).
Ответ. Поверхностная копия делит вложенные ссылки. Нужна глубокая копия или неизменяемые вложенные структуры. Подробнее здесь — Введение в ООП.
Вопрос. Статический метод вызываю как obj.method() — работает, но путает.
Ответ. Статика принадлежит типу, не экземпляру; вызов через объект вводит в заблуждение. Пишите ClassName.method() для ясности. Подробнее здесь — Введение в ООП.
Вопрос. Паттерн Singleton везде — тесты стали зависимыми друг от друга.
Ответ. Глобальное состояние ломает изоляцию тестов. Предпочитайте внедрение зависимости с контролируемым жизненным циклом. Подробнее здесь — Зависимости, паттерны.
Вопрос. DTO и "настоящая" доменная сущность — в одном классе, логика размазана.
Ответ. Объект передачи данных не обязан нести бизнес-правила. Разделяйте модель экрана/API и объект предметной области, иначе ORM и UI тянут класс в разные стороны. Подробнее здесь — Абстракция, ORM.
Вопрос. Не понимаю, зачем сначала статья про сложность (7), если хочется сразу синтаксис классов.
Ответ. ООП решает проблему роста сложности; без границ модулей классы превращаются в мелкие процедуры с this. Сначала — зачем объекты, потом — как писать. Подробнее здесь — Сложность и декомпозиция.
Вопрос. JavaScript: объект литерал — это уже "класс"?
Ответ. Литерал — экземпляр с полями; класс/es6 class задаёт шаблон и прототипную цепочку. Для переиспользования и полиморфизма нужен общий прототип или class. Подробнее здесь — Объекты в JavaScript.
Вопрос. После курса ООП боюсь процедурного кода в утилитах — это откат?
Ответ. Нет. Зрелый код смешает стили по задаче: скрипт импорта — процедура, ядро заказов — объекты. Подробнее здесь — Парадигмы.
Частые поисковые запросы
Вопрос. Что такое ООП простыми словами?
Ответ. Моделирование программы через объекты с данными и поведением, объединёнными в типы (классы). Подробнее здесь — Введение в ООП.
Вопрос. Чем класс отличается от объекта?
Ответ. Класс — шаблон; объект — конкретный экземпляр в памяти. Подробнее здесь — Введение в ООП.
Вопрос. Четыре столпа ООП — какие это принципы?
Ответ. Абстракция, инкапсуляция, наследование, полиморфизм (в учебниках формулировки слегка различаются). Подробнее здесь — Абстракция, Инкапсуляция, Наследование, Полиморфизм.
Вопрос. Инкапсуляция, наследование, полиморфизм, абстракция — кратко.
Ответ. Сокрытие и целостность объекта; повторное использование через иерархию; один интерфейс — разные реализации; выделение существенного. Подробнее здесь — о разделе.
Вопрос. Абстрактный класс и интерфейс — в чём разница?
Ответ. Абстрактный класс хранит состояние и общий код для родственных типов (is-a). Интерфейс описывает способность (can-do), которую могут разделять несвязанные классы; таких интерфейсов у одного класса может быть несколько. Таблица и пример — Абстракция, раздел про абстрактный класс и интерфейс.
Вопрос. Множественное наследование в Java и C# — можно ли?
Ответ. Java — несколько интерфейсов, один класс. C# — то же; от классов множественное наследование запрещено. Подробнее здесь — Наследование.
Вопрос. Diamond problem (ромбовидная проблема) — что это?
Ответ. Конфликт при наследовании от двух веток с общим предком — неясно, чей метод вызывать. Решают через интерфейсы и явное разрешение. Подробнее здесь — Наследование.
Вопрос. Виды полиморфизма в программировании.
Ответ. Подтипы (динамический), перегрузка (ad hoc), обобщения (generics). Подробнее здесь — Полиморфизм.
Вопрос. Перегрузка и переопределение методов — разница.
Ответ. Перегрузка — разные параметры в одном классе; переопределение — та же сигнатура в наследнике, выбор по типу объекта. Подробнее здесь — Полиморфизм.
Вопрос. Конструктор класса — что это и зачем?
Ответ. Метод, который инициализирует объект при создании и задаёт инварианты. Подробнее здесь — Введение в ООП.
Вопрос. Геттеры и сеттеры — зачем нужны?
Ответ. Контроль доступа к полям, валидация, возможность менять внутреннее представление без ломки API. Подробнее здесь — Инкапсуляция.
Вопрос. God object — антипаттерн, что делать?
Ответ. Разбить на классы по зонам ответственности, ввести сервисы и value objects. Подробнее здесь — Сложность и декомпозиция.
Вопрос. Composition over inheritance — что значит?
Ответ. Предпочитайте включение объектов (has-a) вместо глубоких деревьев наследования (is-a), где поведение комбинируется. Подробнее здесь — Наследование, паттерны.
Вопрос. Enum в программировании — зачем не строки?
Ответ. Закрытый набор констант, проверка на этапе компиляции, удобный switch. Подробнее здесь — Перечисления.
Вопрос. List и Array — чем отличаются коллекции?
Ответ. Массив фиксированного размера; список динамический, с операциями вставки/удаления. Оба могут хранить ссылки на объекты. Подробнее здесь — Коллекции.
Вопрос. Garbage collection — как связан с объектами?
Ответ. Среда освобождает память, когда на объект нет достижимых ссылок. Ресурсы (файлы) всё равно закрывайте явно. Подробнее здесь — Введение в ООП, сборка мусора.
Вопрос. ООП в Python для начинающих — с чего начать?
Ответ. Сначала класс, init, методы, затем наследование. Подробнее здесь — ООП в Python, Введение в ООП.
Вопрос. ООП или функциональное программирование — что лучше?
Ответ. Зависит от задачи: объекты для бизнес-модели, функции для потоков данных. Языки часто совмещают оба подхода. Подробнее здесь — Парадигмы, Полиморфизм.
Вопрос. Модификаторы доступа private, public, protected.
Ответ. public — всем; private — только класс; protected — класс и наследники. Подробнее здесь — Инкапсуляция.
Вопрос. Immutable object — неизменяемый объект, зачем?
Ответ. Безопасность в многопоточности, предсказуемость, удобство как ключа. Пример — строки в Java. Подробнее здесь — Введение в ООП.
Вопрос. Factory method — паттерн, когда применять?
Ответ. Когда создание объекта сложное или нужно скрыть конкретный класс за интерфейсом. Подробнее здесь — паттерны GoF, Абстракция.
Вопрос. Когда ООП не нужно и вредно?
Ответ. Короткие скрипты, чистые функции над данными, конвейеры ETL без долгоживущей модели. Подробнее здесь — Сложность и декомпозиция.
Вопрос. Ссылочный тип и значение — в чём разница?
Ответ. Объекты в куче передаются по ссылке; примитивы в Java/C# часто копируются по значению (в C# есть struct). Подробнее здесь — Введение в ООП.
Вопрос. this и super в ООП — зачем?
Ответ. this — текущий объект; super — вызов реализации родителя (конструктор или метод). Подробнее здесь — Наследование.
Что запомнить
ООП — методология моделирования предметной области через типы (классы), их экземпляры и иерархию наследования. Программа — совокупность объектов, обменивающихся данными через методы или сообщения. Структурирование опирается на абстрагирование, инкапсуляцию, наследование и модульность; для полноценного ООП-языка нужен ещё полиморфизм подтипов (определение).
Каждый объект объединяет состояние (атрибуты) и поведение (методы). Класс — абстрактный тип данных (АДТ) и шаблон экземпляров; объект — значение этого типа в памяти; переменная обычно хранит ссылку на объект.
Класс выступает в роли шаблона или чертежа: он определяет структуру и поведение, но не содержит конкретных данных. Объект — это экземпляр класса, размещённый в памяти, обладающий собственным состоянием и способный выполнять действия. Переменная хранит ссылку на объект, а не сам объект, что позволяет гибко управлять жизненным циклом и передавать объекты между частями программы.
Четыре фундаментальных принципа ООП образуют целостную систему:
Абстракция выделяет существенное и отделяет контракт ("что") от реализации ("как"). Абстракция данных (АДТ) — тот же принцип на уровне типа: набор операций без раскрытия внутреннего представления. Инструменты — интерфейсы, абстрактные классы, публичные методы (статья).
Инкапсуляция объединяет данные и методы в классе и управляет доступом снаружи. Сокрытие (private, свойства) — частый способ инкапсуляции, но понятия различают в ряде школ ООП (статья).
Наследование обеспечивает повторное использование кода и построение иерархий типов. Подкласс наследует свойства и методы базового класса, может расширять их новой функциональностью или переопределять существующую. Абстрактные классы и интерфейсы служат основой для полиморфизма, задавая общее поведение для группы связанных классов. Множественное наследование ограничено во многих языках из-за потенциальных конфликтов, но интерфейсы позволяют достичь аналогичной гибкости без рисков.
Полиморфизм подтипов — единый интерфейс базового типа, разные реализации у наследников (динамическое связывание). Перегрузка — ad hoc-полиморфизм (одно имя, разные параметры). Обобщения (generics) — параметрический полиморфизм (полиморфизм, теория обобщений).
Enum и коллекции — вспомогательные АДТ: закрытый набор констант (Перечисления) и контейнер с единым интерфейсом к группе объектов (Коллекции). Они поддерживают типизацию, модульность и полиморфные контейнеры (List<БазовыйТип>).
Жизненный цикл объекта — от создания через конструктор до уничтожения сборщиком мусора — управляется средой выполнения, что освобождает разработчика от ручного управления памятью в большинстве современных языков. Конструкторы инициализируют состояние объекта, гарантируя его корректность с момента рождения.
Связь ООП со сложностью ПО и декомпозицией — в отдельной статье.
ООП не является единственной парадигмой, но его принципы глубоко пронизывают современную разработку. Даже в мультипарадигменных языках ООП остаётся основным способом организации бизнес-логики и моделирования предметной области. Освоение ООП — это развитие способности мыслить категориями объектов, их взаимодействия и ответственности.
Куда идти дальше
| Тема | Раздел |
|---|---|
| Частые паттерны GoF (Factory, Observer, Strategy…) | Частые паттерны GoF в реальных проектах |
| "Парадигмы и уровни абстракции — о разделе" | "Парадигмы и уровни абстракции — о разделе" |
| "Зависимости — о разделе" | "Зависимости — о разделе" |
| "Архитектура выполнения — о разделе" | "Архитектура выполнения — о разделе" |
| "ORM и работа с данными — о разделе" | "ORM и работа с данными — о разделе" |
Проверьте себя: Чек-лист самопроверки.