C++: Путь от новичка до Senior-разработчика

Этот курс проведет вас через все этапы изучения C++, начиная с базового синтаксиса и заканчивая сложными архитектурными решениями. Вы освоите управление памятью, современные стандарты языка и принципы написания высокопроизводительного кода.

1. Фундамент языка: синтаксис, типы данных и процедурное программирование

Введение в C++: Начало большого пути

Добро пожаловать на курс «C++: Путь от новичка до Senior-разработчика». Если вы читаете эти строки, значит, вы решили освоить один из самых мощных, быстрых и востребованных языков программирования в мире. C++ — это фундамент, на котором построены операционные системы (Windows, macOS), игровые движки (Unreal Engine), браузеры (Chrome) и высокочастотные торговые роботы.

В этой первой статье мы не будем просто заучивать команды. Мы разберем философию языка, поймем, как компьютер «думает», когда выполняет ваш код, и напишем свои первые работающие программы.

Как работает C++?

Компьютер не понимает человеческий язык. Он понимает только нули и единицы — машинный код. C++ — это компилируемый язык. Это означает, что между вашим текстом программы (исходным кодом) и исполнением стоит специальная программа — компилятор.

Процесс выглядит так:

  • Вы пишете код на C++.
  • Компилятор переводит его в машинные инструкции.
  • Получается исполняемый файл (например, .exe), который можно запустить.
  • Ваша первая программа

    Традиционно изучение любого языка начинается с программы «Hello, World!». Давайте разберем её анатомию.

    Разбор по строкам

  • #include <iostream>: Это команда препроцессору (подготовительному этапу компиляции). Мы просим подключить библиотеку ввода-вывода (i — input, o — output, stream — поток). Без неё мы не сможем ничего вывести на экран.
  • int main() { ... }: Это главная функция. Любая программа на C++ начинает свое выполнение именно отсюда. Фигурные скобки {} обозначают начало и конец блока кода.
  • std::cout: Это инструмент для вывода текста. Представьте его как трубу, ведущую к вашему экрану.
  • <<: Оператор вставки. Мы как бы «закидываем» текст "Hello, World!" в трубу cout.
  • std::endl: Команда перевода строки (end line). Курсор перепрыгнет на следующую строку.
  • return 0;: Функция main должна вернуть результат своей работы операционной системе. Ноль традиционно означает: «Всё прошло успешно, ошибок нет».
  • > «C++ разработан так, чтобы не платить за то, что вы не используете». — Бьёрн Страуструп, создатель языка

    Переменные и типы данных

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

    C++ — язык со строгой статической типизацией. Это значит, что если вы создали коробку для целых чисел, вы не сможете положить туда текст или дробное число. Компилятор ударит вас по рукам (выдаст ошибку) еще до запуска программы.

    !Визуализация переменных как коробок для хранения данных разных типов

    Основные примитивные типы

    * int (integer): Целые числа. Например: -5, 0, 42, 2023. * double: Числа с плавающей точкой (дробные). Например: 3.14, -0.01, 5.0. * char (character): Одиночный символ. Обязательно в одинарных кавычках. Например: 'A', 'z', '!'. * bool (boolean): Логический тип. Имеет только два значения: true (истина) или false (ложь).

    Пример создания переменных:

    Ввод данных (Общение с пользователем)

    Мы уже умеем выводить текст с помощью std::cout. Чтобы считать данные, которые вводит пользователь с клавиатуры, используется std::cin.

    Обратите внимание на направление стрелочек: * cout << (из программы наружу) * cin >> (снаружи в программу)

    Логика и ветвление

    Программа становится умной, когда она может принимать решения. Для этого используется конструкция if (если) и else (иначе).

    В основе принятия решений лежит булева алгебра. Рассмотрим простую логическую операцию сравнения:

    где — первое сравниваемое число, — второе сравниваемое число, а — оператор «больше или равно». Результатом этого выражения будет либо истина, либо ложь.

    В C++ это записывается так:

    Логические операторы

    Часто нам нужно проверить несколько условий одновременно. Для этого существуют специальные операторы:

  • И (&&): Оба условия должны быть верны.
  • ИЛИ (||): Хотя бы одно условие должно быть верно.
  • НЕ (!): Инверсия (меняет истину на ложь и наоборот).
  • Пример сложного условия:

    где — первое условие, — второе условие, — третье условие, — логическое И, — логическое ИЛИ. Выражение истинно, если верны и А, и B, либо если верно C.

    В коде:

    Циклы: Сила повторения

    Компьютеры идеальны для рутинных задач. Если вам нужно вывести фразу 100 раз, не нужно писать 100 строк кода. Используйте циклы.

    Цикл while (Пока)

    Выполняет код до тех пор, пока условие истинно.

    Цикл for (Для)

    Самый популярный цикл. В нем сразу собраны: инициализация счетчика, условие и шаг изменения.

    Здесь i++ — это сокращение от i = i + 1.

    Процедурное программирование и функции

    По мере роста программы писать весь код внутри main становится неудобно. Код превращается в «спагетти» — запутанный и нечитаемый. Чтобы этого избежать, мы разбиваем задачу на подзадачи. Каждая подзадача — это функция.

    Процедурное программирование — это стиль написания кода, где программа представляется как набор процедур (функций), которые вызывают друг друга.

    Анатомия функции

    Функция имеет:

  • Тип возвращаемого значения (что функция отдаст в результате).
  • Имя (как мы будем её вызывать).
  • Параметры (какие данные ей нужны для работы).
  • Если функция ничего не должна возвращать (просто выводит текст), используется тип void (пустота).

    Заключение

    Сегодня мы заложили первый камень в фундамент вашего мастерства. Вы узнали: * Как выглядит структура программы на C++. * Что такое переменные и почему типы данных важны. * Как управлять потоком выполнения через условия и циклы. * Как структурировать код с помощью функций.

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

    Полезные ресурсы для проверки: * CppReference — официальная документация. * Compiler Explorer — онлайн компилятор для экспериментов.

    2. Глубокое погружение: указатели, ручное управление памятью и объектно-ориентированное программирование

    Глубокое погружение: указатели, ручное управление памятью и объектно-ориентированное программирование

    Приветствую вас во второй части нашего курса «C++: Путь от новичка до Senior-разработчика». В прошлой статье мы научились писать простые программы, использовать переменные и функции. Мы строили домики из кубиков. Сегодня мы убираем кубики и начинаем заливать настоящий бетонный фундамент.

    Темы, которые мы затронем сегодня — указатели и память — считаются «фильтром», отсеивающим случайных людей из профессии. Но не бойтесь. Сложность указателей — это миф. Если вы понимаете, как найти квартиру по почтовому адресу, вы поймете и указатели.

    Оперативная память: Улица с миллиардом домов

    Чтобы понять C++, нужно думать как компьютер. Вся оперативная память (RAM) вашего устройства — это одна гигантская улица, на которой стоят дома (ячейки памяти). У каждого дома есть свой уникальный номер — адрес.

    Когда вы пишете int age = 25;, происходит следующее:

  • Компьютер находит свободный дом на этой улице.
  • Запоминает его адрес (например, 0x7FFF).
  • Кладет в этот дом число 25.
  • Вешает на дом табличку с именем age.
  • !Схематичное изображение оперативной памяти как улицы с адресами

    Указатели: Работа с адресами

    Указатель (Pointer) — это переменная, которая хранит не само значение (не число 25), а адрес, где это значение лежит.

    В C++ для работы с адресами используются два главных оператора:

  • & (Амперсанд) — «Взять адрес». Позволяет узнать, где лежит переменная.
  • * (Звездочка) — «Разыменовать». Позволяет получить доступ к значению по адресу.
  • Пример в коде

    Обратите внимание: int* читается как «указатель на int». Это значит, что переменная ptr может хранить адреса только тех ячеек, где лежат целые числа.

    Ручное управление памятью: Стек и Куча

    Память программы делится на две основные области:

  • Стек (Stack): Здесь хранятся обычные переменные (int a, double b). Память выделяется и очищается автоматически. Это быстро, но размер стека ограничен.
  • Куча (Heap): Это огромное пространство для данных, которыми вы управляете вручную. Здесь можно хранить гигантские массивы или объекты, размер которых неизвестен заранее.
  • Операторы new и delete

    Чтобы выделить место в Куче, мы используем оператор new. Он находит свободное место и возвращает его адрес.

    Важнейшее правило C++: Если вы взяли память вручную (new), вы обязаны вернуть её вручную (delete). Компьютер не сделает это за вас.

    Если вы забудете сделать delete, произойдет утечка памяти (Memory Leak). Ваша программа будет «пожирать» оперативную память, пока компьютер не зависнет. Это отличает C++ от языков вроде Python или Java, где работает «сборщик мусора».

    Расчет памяти

    Понимание того, сколько памяти занимает ваша программа, критически важно. Допустим, мы выделяем массив целых чисел. Общий объем памяти можно рассчитать по формуле:

    где — общий объем занимаемой памяти в байтах, — количество элементов в массиве, а — размер одного элемента в байтах (для int это обычно 4 байта).

    Например, для 1000 чисел:

    где — итоговый размер в байтах, — количество элементов, — размер типа int.

    Объектно-ориентированное программирование (ООП)

    До сих пор мы писали в процедурном стиле: данные отдельно, функции отдельно. Но в реальном мире объекты объединяют в себе и свойства, и поведение. Собака имеет цвет (данные) и умеет лаять (функция).

    ООП — это методология, где программа строится вокруг объектов.

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

    * Класс — это чертеж или шаблон. Сам по себе он не занимает места в памяти. * Объект — это реальный экземпляр, созданный по этому чертежу.

    !Иллюстрация различия между классом (чертежом) и объектами (реализациями)

    Создадим свой первый класс:

    Инкапсуляция: Защита данных

    Представьте, что вы купили телевизор. У него есть пульт (интерфейс), но вы не лезете внутрь паяльником, чтобы переключить канал. Внутренности телевизора скрыты ради безопасности.

    В ООП это называется инкапсуляцией. Мы скрываем данные и даем доступ к ним только через специальные методы.

    Для этого используются модификаторы доступа: * public: Доступно всем (пульт от телевизора). * private: Доступно только самому классу (микросхемы внутри).

    Правильный пример класса:

    Если бы balance был public, кто угодно мог бы написать account.balance = 1000000; и взломать систему. Инкапсуляция предотвращает это.

    Конструкторы и Деструкторы

    Когда объект рождается, его нужно настроить. Когда умирает — за ним нужно прибраться.

  • Конструктор: Специальный метод, который вызывается автоматически при создании объекта. Его имя совпадает с именем класса.
  • Деструктор: Вызывается автоматически при уничтожении объекта. Его имя начинается с тильды ~.
  • Заключение

    Сегодня вы сделали огромный шаг вперед. Вы узнали: * Что указатели — это просто адреса в памяти. * Как управлять «Кучей» с помощью new и delete. * Что такое Классы и Объекты. * Зачем прятать данные с помощью private.

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

    Полезные ресурсы: * LearnCpp.com — подробнейший учебник по C++. * ISO C++ — новости из мира стандарта языка.

    3. Инструментарий профессионала: шаблоны (Templates) и стандартная библиотека (STL)

    Инструментарий профессионала: шаблоны (Templates) и стандартная библиотека (STL)

    Приветствую вас в третьей части курса «C++: Путь от новичка до Senior-разработчика». В прошлых статьях мы прошли через огонь и воду: разобрались с базовым синтаксисом и нырнули в глубины управления памятью с указателями. Вы научились создавать классы и управлять объектами.

    Но есть одна проблема. Если вы попробуете написать программу, используя только то, что мы изучили ранее, вы заметите, что пишете очень много похожего кода. Профессиональные программисты ленивы (в хорошем смысле). Они не любят писать одно и то же дважды. Они следуют принципу DRY (Don't Repeat Yourself — Не повторяйся).

    Сегодня мы откроем ящик с инструментами, который превращает рутинную работу в искусство. Мы поговорим о Шаблонах (Templates) и Стандартной библиотеке шаблонов (STL).

    Шаблоны: Универсальный конвейер

    Представьте, что вы пишете функцию, которая находит большее из двух чисел.

    Для целых чисел (int) код будет таким:

    А если нам нужно сравнить дробные числа (double)? Придется копировать функцию и менять типы:

    А если нужно сравнить символы? Или строки? Писать одну и ту же логику 10 раз — это путь к ошибкам. Здесь на сцену выходят шаблоны.

    Что такое шаблон?

    Шаблон — это чертеж функции или класса, в котором тип данных не указан жестко, а является параметром. Это как формочка для печенья: вы можете использовать одно и то же лекало, чтобы вырезать печенье из разного теста (типов данных).

    !Визуализация работы шаблона: одна логика применяется к разным типам данных

    Давайте перепишем нашу функцию max с использованием шаблона:

    Разберем этот код:

  • template <typename T>: Мы говорим компилятору: «Сейчас будет шаблон. Везде, где ты увидишь букву T, считай это каким-то типом данных, который я уточню позже».
  • T a, T b: Аргументы функции имеют этот неизвестный пока тип T.
  • T max: Функция возвращает значение этого же типа.
  • Теперь мы можем использовать эту одну функцию для всего:

    Это называется Обобщенное программирование (Generic Programming). Вы пишете алгоритм один раз, и он работает с любыми данными.

    Стандартная библиотека шаблонов (STL)

    C++ поставляется с огромным набором готовых инструментов, написанных лучшими программистами мира. Этот набор называется STL (Standard Template Library).

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

  • Контейнеры: Где хранить данные.
  • Алгоритмы: Как обрабатывать данные.
  • Итераторы: Как перемещаться по данным.
  • Контейнеры: Умные хранилища

    Раньше мы использовали обычные массивы (int arr[10]). У них есть фатальный недостаток: их размер фиксирован. Если вам нужно сохранить 11-й элемент, программа сломается. В STL есть решение.

    #### std::vector (Вектор)

    Вектор — это динамический массив, который умеет растягиваться. Если место заканчивается, он сам выделяет новую память, переносит туда данные и удаляет старую. Все это происходит автоматически.

    Вам больше не нужно вызывать new и delete вручную для массивов. Вектор сделает это за вас.

    #### std::map (Словарь)

    Иногда нам нужно хранить пары «Ключ — Значение». Например, телефонную книгу (Имя — Номер). Обычный массив тут не поможет, ведь индексом массива может быть только целое число.

    std::map позволяет использовать в качестве индекса (ключа) что угодно.

    Итераторы: Навигаторы по памяти

    Как пройтись по всем элементам контейнера, если мы не знаем, как он устроен внутри? Для этого используются итераторы.

    Итератор — это умный указатель. Он знает, где находится текущий элемент и как перейти к следующему.

    У каждого контейнера есть методы: * .begin(): Возвращает итератор на начало. * .end(): Возвращает итератор на место после последнего элемента (конец).

    Алгоритмы: Готовые решения

    Зачем писать сортировку пузырьком, если в STL есть идеально оптимизированная сортировка? Библиотека <algorithm> содержит сотни функций.

    Пример сортировки вектора:

    #### Эффективность алгоритмов

    Когда мы говорим об алгоритмах, важно понимать их скорость. В информатике для этого используется «О-большое» нотация. Например, эффективность сортировки std::sort описывается формулой:

    Где: * — время выполнения алгоритма. * — верхняя граница сложности (худший случай). * — количество элементов, которые нужно отсортировать. * — логарифм от количества элементов.

    Это означает, что если количество данных увеличится в 2 раза, время работы увеличится чуть больше, чем в 2 раза, но значительно меньше, чем в 4 раза (как было бы при в пузырьковой сортировке). Использование стандартных алгоритмов гарантирует вам математически доказанную эффективность.

    Строки: std::string

    В первых уроках мы использовали char для одного символа. Для текста в стиле Си использовались массивы char[]. Это неудобно и опасно.

    В C++ есть класс std::string. Это тоже часть стандартной библиотеки. Он работает как вектор, но для символов.

    Практический пример: База данных студентов

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

    В этом примере мы использовали:

  • std::string для имени.
  • std::vector для хранения списка оценок (мы не знали заранее, сколько их будет).
  • Алгоритм std::accumulate из STL для быстрого подсчета суммы.
  • Заключение

    Сегодня вы получили в руки мощнейшее оружие C++ разработчика.

    * Шаблоны позволяют писать код один раз для всех типов данных. * STL избавляет от необходимости писать велосипеды (списки, сортировки, поиск). * Векторы и Строки делают работу с памятью безопасной и удобной.

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

    Полезные ресурсы: * cppreference.com — Библия STL. * CPlusPlus.com — Хорошие примеры использования контейнеров.

    4. Modern C++: стандарты 11-20, умные указатели, семантика перемещения и многопоточность

    Modern C++: стандарты 11-20, умные указатели, семантика перемещения и многопоточность

    Добро пожаловать в четвертую часть курса «C++: Путь от новичка до Senior-разработчика». Если предыдущие статьи были фундаментом и стенами, то сегодня мы проводим электричество, интернет и устанавливаем систему «Умный дом».

    Долгое время C++ развивался медленно. Между стандартом 1998 года и следующим большим обновлением прошло 13 лет. Но в 2011 году вышел C++11, и это изменило всё. Язык стал настолько удобнее, безопаснее и быстрее, что его начали называть Modern C++.

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

    Автоматический вывод типов: auto

    В старом C++ вы обязаны были явно указывать тип каждой переменной. Иногда это приводило к монструозным конструкциям, особенно при работе с итераторами STL.

    В Modern C++ компилятор стал умнее. Если он видит, что вы присваиваете переменной число 5, он понимает, что это int. Зачем писать лишнее? Для этого появилось ключевое слово auto.

    Также появился удобный цикл for-range, похожий на то, что есть в Python или Java:

    Умные указатели (Smart Pointers)

    Вспомните наш урок про память. Мы говорили: «Выделил память через new — обязан освободить через delete». Забыли? Получили утечку памяти. Освободили дважды? Программа упала.

    Modern C++ предлагает решение: Умные указатели. Это объекты, которые ведут себя как указатели, но сами удаляют объект, когда он больше не нужен. Они живут в библиотеке <memory>.

    1. std::unique_ptr (Уникальный владелец)

    Это самый быстрый и легкий умный указатель. Он гарантирует, что у объекта есть только один владелец. Копировать его нельзя, можно только передать права владения.

    2. std::shared_ptr (Совместное владение)

    Иногда одним ресурсом должны пользоваться несколько частей программы одновременно. shared_ptr использует механизм подсчета ссылок.

    !Визуализация принципа подсчета ссылок: объект живет, пока на него указывает хотя бы один владелец.

    Когда вы копируете shared_ptr, счетчик увеличивается. Когда указатель уничтожается (выходит из области видимости), счетчик уменьшается. Когда счетчик достигает нуля, память освобождается.

    Семантика перемещения (Move Semantics)

    Это одна из самых мощных оптимизаций в C++. Представьте, что вы переезжаете в новый дом. У вас есть два варианта:

  • Копирование: Купить точно такую же мебель в новый дом, а старую сжечь.
  • Перемещение: Загрузить старую мебель в грузовик и перевезти.
  • В старом C++ при передаче тяжелых объектов (например, огромного вектора) в функцию часто происходило полное копирование данных. Это медленно.

    L-values и R-values

    Чтобы понять перемещение, нужно различать два типа выражений: * L-value (Left value): У объекта есть имя и адрес в памяти. Он живет долго. (Пример: переменная x). * R-value (Right value): Временный объект, который исчезнет сразу после выполнения строки. (Пример: результат 2 + 2 или возвращаемое значение функции).

    std::move

    Функция std::move превращает L-value в R-value. Она как бы говорит компилятору: «Этот объект мне больше не нужен, можешь забрать его внутренности».

    После std::move(heavyObject) переменная heavyObject находится в валидном, но неопределенном состоянии (обычно пустая). Использовать её данные больше нельзя.

    Лямбда-выражения

    Лямбда — это анонимная функция, которую можно написать прямо внутри кода. Это очень удобно для алгоритмов STL.

    Синтаксис выглядит так: [] (параметры) { тело }.

    Квадратные скобки [] называются списком захвата. В них можно указать переменные из внешнего контекста, которые лямбда может видеть.

    Многопоточность (Multithreading)

    До C++11 работа с потоками зависела от операционной системы (Windows API, POSIX). Теперь многопоточность встроена в язык.

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

    std::thread

    Гонка данных (Data Race)

    Многопоточность — это опасно. Если два потока одновременно пытаются изменить одну и ту же переменную, результат непредсказуем. Это называется Race Condition.

    Для защиты данных используются мьютексы (std::mutex). Мьютекс — это замок. Пока один поток держит замок, другие ждут.

    Закон Амдала

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

    Где: * — итоговое ускорение программы. — доля программы, которую можно* распараллелить (от 0 до 1). * — количество процессоров (потоков).

    Если (только половину кода можно распараллелить), то даже при бесконечном числе процессоров () ускорение никогда не превысит 2. Это учит нас тому, что архитектура важнее простого добавления потоков.

    Заключение

    Modern C++ (стандарты 11, 14, 17, 20) превратил сложный и опасный инструмент в мощное средство разработки.

    Сегодня мы узнали: * auto экономит время на написании типов. * Умные указатели (unique_ptr, shared_ptr) спасают от утечек памяти. * Move-семантика ускоряет работу, избегая лишних копий. * Многопоточность позволяет использовать все ядра процессора.

    Вы прошли путь от «Hello World» до понимания концепций, которые используют Senior-разработчики в высоконагруженных системах. Впереди — практика и создание реальных проектов.

    Полезные ресурсы: * CppReference: Smart Pointers * CppCoreGuidelines — рекомендации от создателей языка.

    5. Уровень Senior: паттерны проектирования, метапрограммирование и оптимизация производительности

    Уровень Senior: паттерны проектирования, метапрограммирование и оптимизация производительности

    Приветствую вас в финальной части нашего курса «C++: Путь от новичка до Senior-разработчика». Мы прошли долгий путь: от первой программы «Hello World» до управления памятью, многопоточности и семантики перемещения. Если вы усвоили предыдущие уроки, вы уже пишете код лучше, чем 80% новичков.

    Но что отличает Senior-разработчика от просто хорошего программиста? Senior не просто пишет код, который работает. Он пишет код, который:

  • Масштабируем (легко расширять).
  • Понятен (другие разработчики говорят «спасибо»).
  • Оптимизирован (использует ресурсы эффективно).
  • Сегодня мы займемся архитектурой, черной магией компилятора и искусством делать программы быстрыми.

    Паттерны проектирования: Язык архитекторов

    Когда строители строят дом, они не изобретают каждый раз способ укладки кирпича. Они используют проверенные решения. В программировании такие решения называются паттернами проектирования (Design Patterns).

    Паттерны — это не готовый код, который можно скопировать. Это концепция решения типовой задачи. Знание паттернов позволяет разработчикам общаться на одном языке. Вместо «давай сделаем класс, который будет один на всю программу», вы говорите: «Здесь нужен Синглтон».

    1. Singleton (Одиночка)

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

    В современном C++ лучшая реализация — это Meyers Singleton (назван в честь Скотта Мейерса). Она потокобезопасна начиная со стандарта C++11.

    2. Factory Method (Фабричный метод)

    Задача: Мы хотим создавать объекты, но не хотим зависеть от конкретных классов. Мы хотим отделить процесс создания от использования.

    [VISUALIZATION: Схема паттерна Фабрика. Слева абстрактный класс