Парадигмы разработки Объектно-ориентированное программирование: инкапсуляция, полиморфизм, наследование
0%

Объектно-ориентированное программирование: инкапсуляция, полиморфизм, наследование

Объектно-ориентированное программирование: инкапсуляция, полиморфизм, наследование

В предыдущей статье мы разбирали процедурный код: данные отдельно, процедуры отдельно, программа — это последовательность шагов над общей кучей структур. Такая модель прекрасно работает, пока структур мало. Когда их становится двести, а процедур две тысячи, возникает вопрос, на который процедурная парадигма не отвечает: кто имеет право менять поле balance и по каким правилам? Ответ «любая функция, которая до него дотянется» — это и есть определение системы, которую невозможно сопровождать. ООП — это ответ на вопрос о границах, а не «про классы» и не «про наследование», хотя учебники начинают именно с них.

Зачем это вообще: две независимые идеи, которые слиплись

Исторически в ООП сошлись две линии мысли, и путаница между ними — источник половины плохого объектного кода. Линия первая — абстракция данных. Дэвид Парнас в 1972 году в работе «On the Criteria To Be Used in Decomposing Systems into Modules» сформулировал принцип сокрытия информации: модуль должен скрывать решение, которое может измениться. Разбивать систему по шагам обработки — плохо, потому что шаги меняются вместе. Разбивать по «секретам» — хорошо, потому что секрет меняется внутри одного модуля. Отсюда растёт инкапсуляция и абстрактные типы данных (Барбара Лисков, CLU).

Линия вторая — обмен сообщениями. Алан Кэй, придумавший термин «object-oriented», в «The Early History of Smalltalk» писал, что имел в виду не классы, а биологическую метафору: множество мелких автономных клеток, которые общаются сообщениями и не лезут друг другу внутрь. Позже он уточнял: «I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is messaging».

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

Инкапсуляция — это про инварианты, а не про private

Инкапсуляция — это объединение состояния с операциями, которые сохраняют его корректность, и запрет любых других способов это состояние менять. Модификатор доступа — механизм, а не цель. Инвариант — утверждение, истинное между вызовами методов. У банковского счёта: balance == sum(transactions) и balance >= -overdraft_limit. Если поле публичное, инвариант защищать невозможно: его нарушит любая строчка кода в любом файле.

from dataclasses import dataclass
from decimal import Decimal
from typing import Iterator

class InsufficientFunds(Exception):
    """Доменная ошибка — часть контракта, а не техническая авария."""

@dataclass(frozen=True)
class Entry:
    """Неизменяемая запись в журнале. Историю нельзя переписать — только дополнить."""
    amount: Decimal
    reason: str

class Account:
    """Инвариант: _balance == sum(e.amount) и _balance >= -_overdraft."""

    def __init__(self, overdraft: Decimal = Decimal("0")):
        self._overdraft = overdraft
        self._balance = Decimal("0")
        self._entries: list[Entry] = []

    @property
    def balance(self) -> Decimal:
        # Чтение — да. Запись — нет. Асимметрия здесь не случайна.
        return self._balance

    def entries(self) -> Iterator[Entry]:
        # Возвращаем итератор, а не сам список: иначе клиент дописал бы запись мимо нас.
        return iter(self._entries)

    def post(self, amount: Decimal, reason: str) -> None:
        if amount == 0:
            raise ValueError("Нулевая проводка бессмысленна")
        new_balance = self._balance + amount
        if new_balance < -self._overdraft:
            raise InsufficientFunds(f"Не хватает {(-new_balance) - self._overdraft}")
        # Мутация состояния — одной транзакцией, после всех проверок.
        self._entries.append(Entry(amount, reason))
        self._balance = new_balance

Четыре приёма отличают настоящую инкапсуляцию от «полей с подчёркиванием»:

  1. Нет сеттера на balance. Есть операция предметной области post, которая знает правила.
  2. Не отдаём наружу изменяемую коллекцию. Возврат self._entries напрямую — это утечка представления: клиент получил право менять внутренности. Отдаём итератор, кортеж или копию.
  3. Проверка идёт до мутации. Если исключение вылетит после append, но до присваивания _balance, объект останется в невалидном состоянии. Это называется гарантией строгой безопасности исключений и нарушается в реальном коде постоянно.
  4. Утечка через вложенный объект (order.customer.address.city = "X") не менее опасна: формально всё приватно, фактически инвариант заказа не защищён. Лекарство — value-объекты, неизменяемые типы без идентичности (Money, Address, DateRange): в C# это record struct, в Java — record, в Python — @dataclass(frozen=True).

Полиморфизм — это единственная вещь, ради которой стоит терпеть ООП

Полиморфизм — способность одного участка кода работать с разными типами. Кардель и Вегнер в классической работе 1985 года выделили виды, которые люди до сих пор путают:

Вид Где встречается Разрешается
Ad-hoc (перегрузка) print(int), print(str) компиляцией, по статическим типам
Приведение (coercion) 1 + 2.0 компилятором неявно
Параметрический List<T>, дженерики, шаблоны компиляцией, один код на все типы
Подтипов (subtype) Animal a = new Dog() во время выполнения, по фактическому типу

Именно последний — «тот самый» полиморфизм ООП (место всех четырёх на общей карте — в обзоре трека). Его настоящая ценность — инверсия зависимости: высокоуровневый модуль зависит от интерфейса, а реализация подставляется снаружи. Из этого растут все паттерны GoF, вся тестируемость и вся модульность.

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

from typing import Protocol
from decimal import Decimal

class PricingPolicy(Protocol):
    """Структурный интерфейс: реализации НЕ обязаны наследоваться от него."""
    def price(self, order: "Order") -> Decimal: ...

class RetailPolicy:
    def price(self, order: "Order") -> Decimal:
        return order.subtotal

class WholesalePolicy:
    def __init__(self, threshold: int, rate: Decimal):
        self._threshold, self._rate = threshold, rate

    def price(self, order: "Order") -> Decimal:
        return order.subtotal * (self._rate if order.items >= self._threshold else Decimal(1))

class LoyaltyPolicy:
    """Декоратор: работает поверх ЛЮБОЙ другой политики, включая себя."""
    def __init__(self, inner: PricingPolicy, discount: Decimal):
        self._inner, self._discount = inner, discount

    def price(self, order: "Order") -> Decimal:
        return self._inner.price(order) * (Decimal(1) - self._discount)

class Checkout:
    def __init__(self, policy: PricingPolicy):   # зависимость инъектируется снаружи
        self._policy = policy

    def total(self, order: "Order") -> Decimal:
        return self._policy.price(order)         # ветвлений по типу здесь нет и не будет

# Сборка поведения на лету — без единого нового класса:
policy = LoyaltyPolicy(WholesalePolicy(threshold=10, rate=Decimal("0.9")), Decimal("0.05"))

Здесь Protocol из PEP 544 даёт структурную типизацию: RetailPolicy ничего не наследует, но подходит по форме. Тот же принцип — интерфейсы Go, трейты Rust, интерфейсы TypeScript. Это ООП без иерархий, и в новых языках это доминирующий стиль (см. Go FAQ: «Is Go an object-oriented language?»).

Наследование — самый переоценённый из трёх

Наследование делает сразу три вещи, и в этом проблема:

  1. ПодтипизациюDog можно использовать там, где ждут Animal (полезно);
  2. Переиспользование кода — наследник получает реализацию базы (опасно);
  3. Жёсткую связность — наследник зависит от внутренностей базы (вредно).

Джошуа Блох в «Effective Java» посвятил этому item 18: «Favor composition over inheritance». Его аргумент — наследование ломает инкапсуляцию: наследник зависит не от контракта базы, а от того, как база реализована. Канонический пример — наследование HashSet, чтобы считать добавленные элементы:

// СЛОМАННЫЙ КОД — классический пример из Effective Java
public class CountingSet<E> extends HashSet<E> {
    private int addCount = 0;

    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);   // а внутри addAll вызывается add() -> двойной счёт!
    }

    public int getAddCount() { return addCount; }
}

addAll в HashSet реализован через add. Счётчик удваивается. Ошибка не в вашем коде и не в коде HashSet — она в невидимом контракте самовызова, о котором нигде не написано. Через версию библиотеки реализация поменяется, и код сломается или починится случайно. Правильное решение — композиция плюс делегирование:

public class CountingSet<E> implements Set<E> {
    private final Set<E> delegate;       // храним, а не наследуем
    private int addCount = 0;

    public CountingSet(Set<E> delegate) { this.delegate = delegate; }

    @Override public boolean add(E e) { addCount++; return delegate.add(e); }
    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return delegate.addAll(c);       // самовызовы внутри delegate нас не касаются
    }
    // ...остальные методы Set просто проксируются в delegate
    public int getAddCount() { return addCount; }
}

Плата — бойлерплейт делегирования (в Kotlin его убирает by, в Go — встраивание структур, в C# — генераторы исходников), и она того стоит. Практическое правило: Наследуйтесь только если: (а) отношение — настоящее «является» по поведению, а не по здравому смыслу; (б) базовый класс спроектирован под наследование и это задокументировано; (в) иерархия глубиной 1–2. Во всех остальных случаях — интерфейс + композиция. В C# и Java классы стоит объявлять sealed/final по умолчанию.

Принцип подстановки Лисков: строгая формулировка

Барбара Лисков и Дженнет Уинг в «A Behavioral Notion of Subtyping» (1994) дали строгое условие:

Пусть φ(x) — свойство, доказуемое для объектов x типа T. Тогда φ(y) должно быть истинно для объектов y типа S, где S — подтип T.

По-человечески: клиент, написанный против базового типа, не должен уметь отличить подтип по поведению. Из этого механически выводятся правила вариантности.

Правила вариантности по Лисков: контравариантность входа, ковариантность выхода

  • Предусловия нельзя усиливать. Если база принимает любое положительное число, наследник не имеет права требовать «только чётные».
  • Постусловия нельзя ослаблять. Если база обещает отсортированный результат, наследник обязан вернуть отсортированный.
  • Инварианты базы сохраняются, а исключения наследника — подтипы объявленных базой: новый непроверяемый тип ошибки ломает клиента.

Классический контрпример — Square extends Rectangle. Геометрически квадрат является прямоугольником, поведенчески — нет: клиент вправе написать r.setWidth(5); r.setHeight(4); assert r.area() == 20, а квадрат даст 16. Мораль: иерархию строят по поведению, а не по таксономии предметной области. Ещё один частый нарушитель — UnsupportedOperationException в реализации интерфейса: Collections.unmodifiableList возвращает List, у которого add бросает исключение. Для стандартной библиотеки это осознанный компромисс, но в вашем коде такой метод — сигнал, что интерфейс слишком широк и его надо разделить (принцип разделения интерфейсов — тема трека принципов проектирования).

Как это работает под капотом: механика диспетчеризации

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

Раскладка объекта и vtable: указатель на таблицу, слоты методов

Сложность. Вызов виртуального метода — O(1): две загрузки из памяти плюс косвенный переход. Память — одно машинное слово на объект (в JVM заголовок объекта — 12–16 байт с mark word и class pointer). Интерфейсные вызовы дороже: у класса может быть несколько интерфейсов, поэтому используются либо itable с поиском, либо inline caches. Реальная цена, однако, — не такты, а предсказуемость перехода:

  • мономорфный сайт вызова (всегда один тип) — JIT подставляет прямой вызов и часто инлайнит его целиком;
  • биморфный (два типа) — проверка типа плюс два прямых вызова, ещё дёшево; мегаморфный (три и более) — полноценный косвенный переход, промах предсказателя ветвлений стоит 15–20 тактов, инлайнинг невозможен, оптимизации через границу вызова отваливаются.

HotSpot собирает статистику типов в профиле и выполняет девиртуализацию: если сайт мономорфен, вставляется guard-проверка и прямой инлайн, а при появлении нового типа код деоптимизируется. Подробности механики — в документации HotSpot и в оптимизационных руководствах Agner Fog. В C++ тот же эффект даёт final на классе или методе — компилятор доказывает, что переопределений нет, и вызывает напрямую. Практический вывод: полиморфизм бесплатен в холодном коде и заметен только в горячем цикле. Если профилировщик показывает мегаморфный сайт внутри цикла на миллионы итераций — там место типо-специализированной ветке или data-oriented раскладке (массив структур → структура массивов). В остальных 99% кода спор о «стоимости виртуальных вызовов» — преждевременная оптимизация.

Множественное наследование и порядок разрешения

Когда классов-родителей несколько, возникает «ромб»: от какого предка брать метод? Python решает это C3-линеаризацией — детерминированным алгоритмом, сохраняющим порядок объявления и монотонность. Разбор — в официальном описании MRO.

class Storage:
    def save(self, data): return f"storage({data})"

class Encrypted(Storage):
    def save(self, data): return super().save(f"enc[{data}]")

class Compressed(Storage):
    def save(self, data): return super().save(f"zip[{data}]")

class SecureArchive(Encrypted, Compressed):
    pass

print([c.__name__ for c in SecureArchive.__mro__])
# ['SecureArchive', 'Encrypted', 'Compressed', 'Storage', 'object']
print(SecureArchive().save("doc"))
# storage(zip[enc[doc]])

Здесь super()не «родитель», а «следующий в MRO». Именно поэтому Encrypted.save вызвал Compressed.save, хотя они не связаны наследованием. Это делает миксины работоспособными и одновременно опасными: порядок базовых классов меняет поведение. Сложность построения MRO — O(n·m) от числа классов и длин линеаризаций, считается один раз при создании класса. C++ решает ромб иначе — виртуальным наследованием (одна общая база ценой более сложной раскладки объекта), а Java и C# отказались от множественного наследования состояния вовсе, оставив множественные интерфейсы и default-методы: осознанный размен сложности на предсказуемость.

Жизненный цикл объекта как конечный автомат

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

Типичная ошибка — вынести status в публичное поле и раскидать if (order.status == PAID) по всему коду: правила переходов оказываются размазаны и никем не гарантированы. Правильно — метод ship(), который сам проверяет, что заказ оплачен, и бросает доменную ошибку иначе. Это прямой мост к DDD-агрегатам: агрегат есть граница транзакционной консистентности, реализованная средствами инкапсуляции.

Обмен сообщениями и двойная диспетчеризация

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

Финальная ремарка на диаграмме — это проблема выражения (expression problem, термин Филипа Вадлера): ООП дёшево добавляет новые типы к фиксированному набору операций, а функциональный стиль с сопоставлением с образцом — новые операции над фиксированным набором типов. Это фундаментальный размен, а не недостаток парадигмы. Подробнее о второй половине размена — в статье о функциональном программировании.

from abc import ABC, abstractmethod

class Node(ABC):
    @abstractmethod
    def accept(self, visitor: "Visitor"): ...

class Number(Node):
    def __init__(self, value: float): self.value = value
    def accept(self, visitor): return visitor.visit_number(self)   # 1-я диспетчеризация

class Add(Node):
    def __init__(self, left: Node, right: Node): self.left, self.right = left, right
    def accept(self, visitor): return visitor.visit_add(self)

class Visitor(ABC):
    @abstractmethod
    def visit_number(self, node: Number): ...
    @abstractmethod
    def visit_add(self, node: Add): ...

class Evaluator(Visitor):          # новая операция — новый класс, старые не трогаем
    def visit_number(self, node): return node.value
    def visit_add(self, node): return node.left.accept(self) + node.right.accept(self)

class Printer(Visitor):
    def visit_number(self, node): return str(node.value)
    def visit_add(self, node): return f"({node.left.accept(self)} + {node.right.accept(self)})"

tree = Add(Number(2), Add(Number(3), Number(4)))
print(tree.accept(Printer()), "=", tree.accept(Evaluator()))   # (2 + (3 + 4)) = 9

Сложность обхода — O(n) по числу узлов, память — O(h) на стек рекурсии, где h — высота дерева; для глубоких деревьев (сгенерированный код, цепочки a+b+c+...) это реальный риск переполнения стека, поэтому в продакшн-парсерах обход делают явным стеком.

Типичные ошибки, которые видно в каждом втором проекте

  1. Анемичная модель. Классы — мешки геттеров/сеттеров, вся логика в *Service. Симптом: сервис читает три поля объекта, что-то считает и записывает четвёртое. Лечение — перенести вычисление внутрь объекта (Tell, Don’t Ask). Исключение: DTO и ORM-проекции анемичны намеренно, и это нормально.
  2. God object. Класс на 3000 строк с именем Manager, Helper, Utils. Симптом: любое изменение требует его правки. Лечение — резать по причинам изменения, а не по техническим слоям.
  3. Глубокие иерархии. AbstractBaseHttpAuthenticatedJsonController. Каждый уровень — новый неявный контракт. Три уровня — практический потолок.
  4. Виртуальный вызов из конструктора. База вызывает переопределённый метод, наследник ещё не инициализировал свои поля — получаете null или нули. В Java/C# компилятор промолчит, в C++ вызовется версия базы (что удивляет иначе):
class Base {
    public Base() { Init(); }                          // ОПАСНО
    protected virtual void Init() { }
}
class Derived : Base {
    private readonly List<int> _items = new();
    protected override void Init() => _items.Add(1);   // NullReferenceException: _items ещё null
}
  1. Нарушенный контракт equals/hashCode (Equals/GetHashCode, __eq__/__hash__). Переопределили сравнение и забыли хеш — объект «теряется» в множествах и словарях. Хуже: изменяемое поле участвует в хеше, и объект, положенный в HashSet, становится ненаходимым после мутации. Правило — в хеш только неизменяемые поля.
  2. Публичные изменяемые коллекции. getItems().clear() из чужого модуля. Возвращайте неизменяемые представления.
  3. Наследование ради переиспользования. «У Order и Invoice есть общие поля — сделаем базовый AbstractDocument». Общие данные не означают общее поведение; через полгода базовый класс становится свалкой.
  4. Ветвление по instanceof/isinstance. Цепочка if (x is A) ... else if (x is B) — это виртуальный метод, который забыли написать. Исключение: паттерн-матчинг на закрытой иерархии (sealed-классы Java 17+, C# 9+) — осознанный ADT-стиль, где компилятор проверяет полноту.

Как ООП выглядит в реальном продакшене

ООП живёт на границах, а не в ядре вычислений. Устойчивая современная схема: интерфейсы на входе/выходе системы (репозитории, шлюзы к API, шины сообщений), а внутри — данные и чистые функции. Это «функциональное ядро, императивная оболочка» (Гэри Бернхардт) и оно же — гексагональная архитектура.

Почему это работает: интерфейс объявлен потребителем (ядром), а реализация живёт снаружи. Стрелка зависимости направлена внутрь, и это позволяет подменить Postgres на in-memory в тестах, не трогая доменный код. Мартин Фаулер разбирает механику подстановки в «Inversion of Control Containers and the Dependency Injection pattern». Что ещё делают на практике:

  • Интерфейсы держат узкими — 1–3 метода. Широкий интерфейс невозможно реализовать честно и приходится городить заглушки.
  • DI-контейнер (Spring, ASP.NET Core, wire в Go) — деталь инфраструктуры. Домен о нём не знает; иначе получаете фреймворк вместо модели.
  • Сущности vs value-объекты. У сущности есть идентичность и жизненный цикл (Order), у value-объекта — только значение (Money). Value-объекты делают неизменяемыми всегда — они бесплатно потокобезопасны, что критично для конкурентных парадигм.
  • ORM — самая протекающая абстракция в ООП. Object-relational impedance mismatch: наследование, ленивая загрузка, идентичность объектов плохо ложатся на реляционную модель. Практика — держать доменную модель отдельно от persistence-модели, если домен сложный, и не бороться с ORM, если домен — CRUD.
  • Тестируемость — главный практический дивиденд. Возможность подставить фейк без mock-фреймворка — признак того, что границы проведены верно.
  • Обратное движение тоже есть. В геймдеве и высокопроизводительных системах иерархии вытеснены ECS (entity-component-system): компоненты — чистые данные, системы — процедуры над массивами. Причина не идеологическая, а кешевая: Array of Structs с виртуальными вызовами проигрывает Struct of Arrays в разы на горячих циклах. ООП там остаётся на уровне менеджеров подсистем, а не каждой сущности.

ООП в разных языках

Язык Модель Наследование Особенность
Java классы + интерфейсы одиночное + default-методы sealed-иерархии с 17, records
C# классы + интерфейсы одиночное свойства, record, паттерн-матчинг
Python всё объект, утиная типизация множественное, C3 MRO Protocol, дескрипторы, метаклассы
C++ классы, статическая диспетчеризация по умолчанию множественное, виртуальное virtual явно, RAII, шаблоны
Go структуры + интерфейсы нет, только встраивание структурная типизация, неявная реализация
Rust структуры + трейты нет трейты со статической и динамической диспетчеризацией
TypeScript классы + структурные типы одиночное типы структурные, interface стирается
Elixir объектов нет протоколы дают полиморфизм без состояния

Показательно: во всех языках, спроектированных после 2005 года, наследование реализации либо убрано, либо ограничено, а полиморфизм остался — лучший индикатор того, какой из «трёх китов» реально несёт ценность. Подробности по языкам — в треках Go, C#, TypeScript и Elixir.

// Go: интерфейс объявляет ПОТРЕБИТЕЛЬ, реализация — неявная.
// PgOrderRepository нигде не пишет "implements" — достаточно совпадения сигнатур.
type OrderRepository interface {
    ByID(ctx context.Context, id string) (*Order, error)
}

type PlaceOrder struct{ repo OrderRepository } // зависимость от абстракции

func (uc PlaceOrder) Handle(ctx context.Context, id string) error {
    order, err := uc.repo.ByID(ctx, id)
    if err != nil {
        return fmt.Errorf("загрузка заказа: %w", err)
    }
    return order.Place() // инвариант проверяется внутри агрегата
}

Когда ООП — не тот инструмент

  • Преобразования данных без состояния (ETL, аналитика) — функциональный или процедурный стиль короче; см. декларативные подходы.
  • Числодробилки и горячие циклы — важна раскладка памяти, а не иерархия типов.
  • Скрипты до 200 строк — оверхед проектирования не окупается.
  • Конкурентность с общим состоянием — изменяемые объекты плюс потоки дают гонки; надёжнее акторы или неизменяемые данные.
  • Алгоритмические ядра — нужны структуры данных и инварианты, а не полиморфизм.

Зато ООП сильнее всего там, где есть много вариаций одного поведения (плагины, платёжные провайдеры, стратегии тарификации), долгоживущее состояние с правилами (домен) и необходимость подменять реализации (тесты, разные окружения).

Мини-итог

  • Объект — это контракт с защищённым состоянием, а не структура с методами. Инкапсуляция защищает инварианты; private — механизм, не цель. Не отдавайте наружу изменяемые внутренности.
  • Полиморфизм подтипов — главная ценность ООП: он даёт инверсию зависимостей, расширяемость и тестируемость. Стоимость O(1), но мегаморфные сайты в горячем коде мешают JIT.
  • Наследование смешивает подтипизацию, переиспользование и связность: берите подтипизацию через интерфейсы, переиспользование — через композицию. LSP формализует «подтип не должен удивлять клиента»: вход контравариантен, выход ковариантен, инварианты сохраняются.
  • В проде ООП живёт на границах: порты-интерфейсы снаружи, данные и правила внутри. Иерархии — плоские, интерфейсы — узкие, value-объекты — неизменяемые.

Источники

Что дальше

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

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов