Страницы

Поиск по вопросам

суббота, 30 ноября 2019 г.

Что такое принцип открытости и закрытости?

#c_sharp #архитектура #шаблоны_проектирования #solid


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

Читал книги, но мне не понятен такой момент: я как раз и не понимаю, если класс закрыт
от изменения, то как его можно расширить
    


Ответы

Ответ 1



Итак, принцип гласит, что программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения Итак, видно, что речь идет о: Классах Модулях Функциях и т.д. Все они должны быть открыты для расширения, но закрыты для модификации. Звучит отлично, но понять этот принцип по его названию довольно сложно. Начнем с "закрыты для модификации". Это означает, то единственная причина, по которой вы можете менять код класса\функции\модуля - это непосредственно изменение заложенной в него функции. Все. Больше причин менять этот код быть не должно. Именно в этой точке идет пересечение в принципом единственности ответственности. Далее, что значит открыты для расширения? Это означает, что если вам надо, чтобы ваш класс\функция\модуль могли выполнять заложенные функции в новом окружении - они должны это поддерживать без изменения их кода. Давайте, для наглядности, рассмотрим пример. Расширение класса делегированием Допустим, у нас есть класс для сортировки массива public class BubbleSorter { public void Sort(int[] data) { int n = data.Length; for (int i = 0; i < n - 1; i++) for (int j = 0; j < n - i - 1; j++) if (data[j] > data[j + 1]) { int temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; } } } Поглядим на этот класс. Попробуем понять, какие точки расширения у него есть. Точки расширения - это те требования, которые скорее всего в вашем приложении возникнут (или уже возникли) в ходе разных сценариев использования вашего кода. Например, мы видим, что этот метод сортирует только числа. Но с вероятностью 99% нам надо будет сортировать что то ещё, кроме чисел. Также, сортировка идет только по возрастанию, но, скорее всего нам понадобится и сортировка по убыванию. По сути, мы опять пересекаемся с принципом единственности ответственности - мы определяем, какие ответственности сейчас есть у класса и пробуем их разделить. Давайте используем существующие в дотнете интерфейсы и перепишем немного код: public class BubbleSorter { IComparer _comparer; public BubbleSorter(IComparer comparer) { _comparer = comparer; } public void Sort(T[] data) { int n = data.Length; for (int i = 0; i < n - 1; i++) for (int j = 0; j < n - i - 1; j++) if (_comparer.Compare(data[j], data[j + 1]) > 0) { var temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; } } } Теперь наш сортировщик стал немного более гибким. Теперь, в случае, если нам понадобится сортировать строки, вместо чисел, нам не придется вносить изменение в наш сортировщик. Если нам надо будет сортировать по убыванию, а не по возрастанию, нам не придется менять код сортировщика. Таким образом, мы можем расширить наш класс новым функционалом, по сути не меняя сам класс. То же самое справедливо и для функций. Например, var sorted = data.OrderBy(x=> /* ваше направление сортировки */ ); То же самое, справедливо для программных модулей (например, когда вы можете переиспользовать один и тот же модуль в разных сценариях). Однако, изложенное выше не означает, что вам нужно немедленно броситься к своим классам и начать внедрять в них подобные приемы. Как всегда, прежде, чем выполнять подобное, надо думать головой. Например, вы можете видеть, что код выше сортирует только массивы. причем сортирует на месте (меняет исходный массив). Я намеренно это оставил, так как хотел показать, что не надо уходить в крайности. Не надо слепо следовать принципу, надо перед тем, как что то написать, хорошенько подумать. Потому что, если бездумно следовать принципу, то можно в конце получить слишком абстрактный код, в котором никто ничего уже не поймет. В моем случае, я решил, что мне не нужно сортировать другие типы данных, кроме массивов. Я предположил, что в текущем проекте мне это не понадобится (считайте, что это часть синтетического примера). Но мы рассмотрели только один из вариантов расширения класса - передачи части его ответственности на другой объект, который можно подменить динамически. Что ещё мы можем сделать с классом? Расширение класса наследованием Предположим, у нас типичный класс писателя в файл CSV public class CsvWriter { protected void WriteBody(IEnumerable obj, TextWriter stream){ foreach(var ob in obj) stream.WriteLine(ob); } protected virtual void WriteInternal(IEnumerable obj, TextWriter stream) { WriteBody(obj, stream); } public void Write(IEnumerable obj, TextWriter stream) { WriteInternal(obj, stream); } } Уже наметанным глазом, вы можете увидеть, что в данном случае сам класс решает, как именно будут выглядеть записи объектов в файле. Это, конечно, кандидат на выделение в отдельную ответственность, но это нас сейчас не волнует. А вот что волнует - так это то, что наш класс не пишет заголовок файла. То есть колонки он может как то и запишет, но вот заголовка каждой колонки - нет. Что же делать? Из сигнатуры функций класса можно заметить, что у него есть виртуальный метод, полностью определяющий порядок записи данных в файл. Мы можем легко написать наследника класса и добавить в нем заголовок файла, при этом не изменяя базовый класс: public class CsvWriterWithHeader : CsvWriter { protected virtual void WriteHeader(TextWriter stream){ stream.WriteLine("I AM HEADER!"); } protected override void WriteInternal(IEnumerable obj, TextWriter stream) { WriteHeader(stream); WriteBody(obj, stream); } } Как итог - функционал расширен, исходный класс не тронут. Расширение класса аггрегированием Итак, например у нас есть интерфейс и класс для репозитория. Возможно, у вас даже несколько реализаций такого репозитория. public interface IRepository { void SaveStuff(); } public class Repository : IRepository { public void SaveStuff() { // save stuff } } И, конечно же есть какой-то клиент всего этого class RepoClient { public void DoSomethig(IRepository repo) { //... repo.SaveStuff(); } } И однажды вам босс говорит, что вам нужно логгировать каждый вызов каждой реализации вашего репозитория. Но менять из за этого все реализации не хочется (и вы уже знаете почему). Что же делать? Конечно накатать новую реализацию-обертку public class RepositoryLogDecorator : IRepository { public IRepository _inner; public RepositoryLogDecorator(IRepository inner) { _inner = inner; } public void SaveStuff() { // log enter to method try { _inner.SaveStuff(); } catch(Exception ex) { // log exception } // log exit to method } } Что она делает? По сути, она принимает декорируемый объект и проксирует вызовы к нему с логгированием. И если раньше ваш код выглядел так: var client = new RepoClient(); client.DoSomethig(new Repository()); То теперь он будет выглядеть как то так: var client = new RepoClient(); client.DoSomethig(new RepositoryLogDecorator(new Repository ())); Меняли ли мы наши реализации репозиториев? Нет. Добавили к ним функционал? Да. Ещё немного инфы про подобные фокусы с классами тут Как итог, хочу отметить, что мы затронули всего пару вариантов, как можно расширить функционал, не затрагивая реализации. На самом деле вариантов больше, тут важно понять принцип - вы пишете класс\функцию\модуль, и они делают только то, для чего они созданы и расширяем их функционал уже не затрагивая их исходный код. И не забываем про принцип единственности ответственности - как вы уже должны были заметить, он очень тесто связан с принципом открытости\закрытости. Также повторюсь, что необходимо иметь гибкость ваших структур данных такую, которая обеспечивает соблюдение требований проекта, и не порождать избыточной гибкости, так как излишняя гибкость усложняет ваши абстракции почем зря. Если вы таки дочитали до этого момента, то раскрою вам великую тайну - каждый из методов выше использует тот или иной паттерн. Попробуйте подумать какой паттерн где, пробегитесь по основным паттернам из самой известной книги по паттернам (от банды), подумайте, какие из них расширяют функционал класса, и тогда, надеюсь, мозаика начнет складываться.

Комментариев нет:

Отправить комментарий