Создайте класс мага, который умеет произносить заклинания. Для упрощения у него будет только одно поле — имя, и один метод — произнесение заклинания.

Маг должен уметь колдовать следующие заклинания, выбирая в любой момент нужное:

Ледяное копьё Щит Лечение Полет

Для упрощения пусть каждое заклинание будет просто выводом сообщения в консоль, например:

{Name} создал и метнул ледяное копьё! 
{Name} создал защитный ледяной щит!
{Name} восстановил здоровье!
{Name} поднялся в воздух и полетел!

Давайте заодно проверим:

а нарушает ли наш код принцип единственной ответственности (SRP)?

Вспомним главный вопрос этого принципа:

«Сколько причин для изменения имеет данный класс?»

Если присмотреться, класс Mage отвечает только за однопроизнесение заклинаний. Он не хранит состояние игры, не управляет интерфейсом и не работает с файлами. Значит, с точки зрения SRP — всё в порядке.

Но теперь возникает новый вопрос:

«Что произойдёт, когда я захочу расширить функциональность класса?»

Например, появится новое заклинание. Что мы будем делать? Правильно — открывать класс мага и менять его, а точнее — вносить изменения в метод CastSpell.

И вот это уже становится проблемой. Каждое новое заклинание заставляет нас менять существующий код, а значит — нарушается принцип открытости/закрытости (Open–Closed Principle), о котором мы поговорим дальше.

Какие же это создает проблемы?

Во-первых, мы ломаем инкапсуляцию. Класс Mage знает слишком много о конкретных заклинаниях —

а ведь сам по себе он должен просто кастовать, а не знать, как именно работает каждое заклинание.

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

опечатка в if, неверное имя, не тот порядок условий — и внезапно перестанет работать что-то старое.

В-третьих, тестировщики снова страдают. Каждый раз, когда вы добавляете новое заклинание, им нужно тестировать не только его, но и весь метод CastSpell, чтобы убедиться, что вы случайно ничего не сломали.

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

«Не трогай уже тот код, который прекрасно работает».

Именно это и решает принцип Open–Closed. Он учит строить систему так, чтобы добавление нового поведения происходило без изменений в существующих классах. То, что у нас получилось, — классический пример того, как система вроде бы работает, но при малейших изменениях начинает рассыпаться.

Класс Mage делает своё дело — колдует — но каждый раз, когда в игру добавляется новое заклинание, нам приходится лезть внутрь метода и переписывать его.

На первый взгляд, это не страшно:

«Ну подумаешь, добавил ещё один if, делов-то!»

Но если вдуматься — именно так и рождается неустойчивый код, который нельзя развивать, не ломая старое.

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

И вот здесь появляется второй принцип SOLID — Open–Closed Principle, или принцип открытости/закрытости.

Он формулируется просто: код должен быть открыт для расширения, но закрыт для изменения.

Звучит красиво, но что это значит на практике? Это значит, что мы должны проектировать систему так, чтобы новое поведение можно было добавлять, не меняя существующий код.

В нашем случае — чтобы маг мог изучать новые заклинания, а класс Mage при этом оставался нетронутым.

Мы хотим, чтобы появление заклинания «Молния», «Призыв элементаля» или «Заморозка времени» не заставляло нас лезть в if-else внутри CastSpell.

Пусть маг просто знает, что у него есть набор заклинаний, и каждое заклинание само знает, как именно оно работает.

Давайте посмотрим, как можно преобразить наш код, чтобы маг мог выучивать новые заклинания, а мы при этом не изменяли ни строчки в его классе. Главная цель — сделать так, чтобы маг ничего не знал о конкретных заклинаниях. Он не должен сам решать, что делает «Fireball» или «Heal» — его задача заклинание произнести.

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

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

Абстракция заклинания

public interface ISpell

{

    string Name { get; }

    void Cast(string casterName);

}

Здесь мы описали контракт — набор требований, которым должно соответствовать любое заклинание:

1) у него есть имя (Name);

2) у него есть действие (Cast).

То, что раньше было внутри Mage.CastSpell, теперь переносится внутрь самих заклинаний — в метод Cast. Маг больше не знает, что именно происходит при колдовстве: он просто вызывает метод, а заклинание само делает нужное.

Конкретные классы заклинаний:

public class Fireball : ISpell

{

    public string Name => "Fireball";

    public void Cast(string casterName)

        => Console.WriteLine($"{casterName} запускает огненный шар! ");

}

public class IceBlast : ISpell

{

    public string Name => "IceBlast";

    public void Cast(string casterName)

        => Console.WriteLine($"{casterName} выпускает ледяное копьё! ");

}

public class Heal : ISpell

{

    public string Name => "Heal";

    public void Cast(string casterName)

        => Console.WriteLine($"{casterName} восстанавливает здоровье! ");

}

Да, количество классов увеличилось, но каждое заклинание теперь отвечает только за себя. Если нам нужно добавить «Молнию» — мы просто создаём ещё один класс. При этом Mage остаётся нетронутым — а это и есть суть принципа OCP.

Теперь маг работает с абстракцией, а не с конкретными заклинаниями:

Теперь все заклинания подготовлены заранее и сохранены в отдельной коллекции, из которой маг выбирает нужное. Добавление нового заклинания происходит не внутри метода произнесения заклинаний, а снаружи — при формировании списка заклинаний, который передаётся магу.

public class Mage

{

    public string Name { get; }

    private readonly List<ISpell> _spells; // заклинания

    public Mage(string name, List<ISpell> spells) // отдали магу его заклинания

    {

        Name = name;

        _spells = spells;

    }

    public void CastSpell(string spellName)

    {

        var spell = _spells.FirstOrDefault(s => s.Name == spellName);

        if (spell != null)

            spell.Cast(Name);

        else

            Console.WriteLine($"{Name} не знает такого заклинания...");

    }

}

Пример использования:

var spells = new List<ISpell> { new Fireball(), new IceBlast(), new Heal() };

var mage = new Mage("Альрик", spells);

mage.CastSpell("Fireball");

mage.CastSpell("Heal");

mage.CastSpell("Unknown");

Теперь маг просто получает набор заклинаний при создании. Он умеет их произносить, но не знает, как они устроены внутри. Если завтра появится новое заклинание — «Щит» или «Молния» — достаточно добавить новый класс и включить его в список spells. Итак, что мы сделали.

Опять же, взяв то, чем мы уже владеем — интерфейсы, создание новых классов, передачу коллекции через конструктор

мы вылепили из этого довольно-таки чистое и гибкое архитектурное решение.

Мы не придумали ничего нового — просто применили знакомые инструменты, но по-другому взглянули на ответственность объектов. Теперь маг не решает, что именно делает заклинание. Он лишь знает, что каждое заклинание умеет колдовать.

А конкретные детали поведения спрятаны внутри самих заклинаний.

Благодаря этому:

  • код мага не изменяется, когда появляются новые заклинания;
  • каждое заклинание изолировано и отвечает только за себя;
  • новое поведение добавляется, а не «вшивается» в старый код.

Вот это и есть суть принципа Open–Closed Principle - система должна быть открыта для расширения, но закрыта для модификации.

Вы наглядно увидели, как можно достичь принципа открытости/закрытости на примере мага и заклинаний. Но, само собой, — это далеко не единственный способ.

Соблюдать этот принцип можно множеством разных путей:

через наследование, композицию, внедрение зависимостей, делегирование

и даже паттерны проектирования — о всех этих инструментах мы будем подробно говорить по ходу курса.

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

Помните, какой вопрос мы задавали для принципа единственной ответственности (SRP)?

«Сколько причин для изменения имеет данный класс?»

Этот вопрос помогает быстро определить, не взял ли на себя класс лишнего.

Так вот, у принципа открытости/закрытости (OCP) тоже есть свой вопрос. Он поможет вам понять, насколько гибко спроектирован ваш код, и сможете ли вы добавлять новое поведение без риска что-то сломать.

Чтобы добавить новую функциональность, нужно ли мне изменять уже существующий код?

У принципа открытости/закрытости (Open–Closed Principle) есть характерные «запахи кода», по которым можно заподозрить его нарушение. Один из главных признаков — это наличие множества условий, проверяющих значения переменных или типов.

Если вы видите, что: поведение программы меняется в зависимости от значения строки, числа или типа и для этого используются длинные конструкции вроде if, else if или switch, — то это сигнал, что ваш код, скорее всего, нарушает принцип OCP.

Это значит, что вы должны проектировать код так, чтобы новое поведение можно было добавить, не меняя уже существующий и проверенный код.

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

На практике нарушение OCP часто проявляется в виде цепочек if/else if или switch, где класс сам решает, что именно нужно делать.

Вместо этого лучше использовать полиморфизм — передавать ответственность за поведение объектам, которые знают, как именно они должны действовать.

Благодаря этому код становится гибким, расширяемым и изолированным, а каждая новая функция добавляется легко и безопасно — просто через создание нового класса, не изменяя старые.

Следуйте здравому смыслу: применять OCP стоит там, где код действительно планируется расширять или развивать в будущем. Главная цель — не избыточная абстракция, а устойчивость и предсказуемость системы.

Принцип открытости/закрытости (OCP) — код должен быть открыт для расширения, но закрыт для модификации.

Новое поведение добавляется через создание новых классов, а не через изменение существующих.

Длинные цепочки if/else if или switch — признак нарушения OCP.

Полиморфизм позволяет соблюдать OCP: поведение определяется не условиями, а объектами.

Обсуждение урока

0
Комментарии видны всем. Чтобы участвовать в обсуждении, войдите или зарегистрируйтесь.
Модерация сообщества

Пожаловаться на комментарий

Расскажите модераторам, что именно требует внимания.