Найти тему
FixTick blog

SOLID, который Вам нужен.

Оглавление
Доброго времени суток! В этой статье я доступно объясню, для чего же нужен этот "загадочный" solid, что подразумевают его постулаты, а также приведу пару примеров из реальных проектов.

К чему ведёт SOLID. Связность и связанность классов.

SOLID - набор принципов, сформированных Робертом Мартином, как наиболее ценных при построении гибких систем в рамках ООП. На основе их возможно разработать продукт, поддержка которого со временем не будет расти в экспоненциальной прогрессии. 

Так нужны ли они на самом деле? Для ответа на этот вопрос определим несколько маркеров, которые выступят лакмусовыми бумажками "чистой и светлой" системы.

1)Связность.

Связность - мера взаимодействия элементов внутри системы, в данном случае внутри класса. Определить ее достаточно легко: посмотрите, в какой мере используются переменные и методы класса. В идеале должно быть выделено минимальное количество переменных, участвующих почти во всех функциях( без дублирования кода) данного объекта. Классы с высокой связностью выглядят как отлаженный механизм, в котором четко определены роли и нет заменяемых частей. Конечно, добиться этого сложно и не всегда нужно, но иметь ввиду данный аспект стоит. 

2)Связанность.

Связанность - мера взаимодействия объектов внутри системы, между модулями и даже программами. Чем больше один класс обращается к другим для выполнения определенных действий, тем выше вероятность того, что другие классы выполняют его задачи. Лучший уровень связанности - минимальный.

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

Принцип единственной ответственности.

Фото автора Lara Jameson: Pexels.
Фото автора Lara Jameson: Pexels.

Один класс - одна забота. Долгое время я не мог понять, что конкретно обозначает этот принцип. Методы класса сами по себе выполняют разного рода задачи и тогда, судя по всему, мы должны всякий раз выделять новый класс из текущего при решении добавить новый метод. Суть же в другом. Стоит разграничивать функцию и сущность. 

Функция - некоторый алгоритм, в результате которого мы получаем определенный результат. Функция назначена для выполнения одной цели. Сущность же в свою очередь выполняет несколько задач, решаемых с помощью его методов, и самое главное, клиент ждёт от сущности наличия данных методов для решения поставленных задач.

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

Мой класс отвечает за переход между игровыми сценами.

Мой класс отвечает за создание ловушек на игровом поле.

Неверно:

Мой класс отвечает за игровые сцены.

Мой класс отвечает за игровые ловушки.

В сути ответственности должен лежать процесс, а не сущность, которая её должна обеспечивать. 

Вот пример из проекта. Класс обеспечивает очередность выполнения "матчинга":

В данном классе хорошая связность - минимальное количество элементов взаимодействуют по максимуму между собой.
В данном классе хорошая связность - минимальное количество элементов взаимодействуют по максимуму между собой.

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

Принцип открытости/закрытости

Фото автора Travis Saylor: Pexels.
Фото автора Travis Saylor: Pexels.

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

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

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

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

Принцип подстановки Лисков.

Фото автора Markus Spiske: Pexels
Фото автора Markus Spiske: Pexels

Я не буду следовать стандартному объяснению, так как оно для меня кажется слишком сложным. Вот мое определение: классы наследники должны выполнять как минимум столько, сколько и классы предки, не требовать больше(предусловия) и не обещать меньше после выполнения(постусловия). Если объяснять с точки зрения контрактного программирования, здесь нет ничего сложного - у наследников должен быть четкий контракт, который мы должны реализовать. Мы ожидаем, что сможем заменять потомков наследниками, потому и такие требования. Если же мы предполагаем в будущем изменения, которые могут нарушить этот порядок, стоит изначально отделить классы, дабы не рушить систему и не получить кучу исключений в конце.

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

Принцип разделения интерфейсов

Фото автора Nicole Michalou: Pexels.
Фото автора Nicole Michalou: Pexels.

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

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

Принцип инверсии зависимостей.

Фото автора Zachary DeBottis: Pexels
Фото автора Zachary DeBottis: Pexels

В какой-то мере принцип тоже трактуется по принципу единственной ответственности.

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

Пример:

-10

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

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

#unity #unity3d #программирование #solid #c#