Доброго времени суток! В этой статье я доступно объясню, для чего же нужен этот "загадочный" solid, что подразумевают его постулаты, а также приведу пару примеров из реальных проектов.
К чему ведёт SOLID. Связность и связанность классов.
SOLID - набор принципов, сформированных Робертом Мартином, как наиболее ценных при построении гибких систем в рамках ООП. На основе их возможно разработать продукт, поддержка которого со временем не будет расти в экспоненциальной прогрессии.
Так нужны ли они на самом деле? Для ответа на этот вопрос определим несколько маркеров, которые выступят лакмусовыми бумажками "чистой и светлой" системы.
1)Связность.
Связность - мера взаимодействия элементов внутри системы, в данном случае внутри класса. Определить ее достаточно легко: посмотрите, в какой мере используются переменные и методы класса. В идеале должно быть выделено минимальное количество переменных, участвующих почти во всех функциях( без дублирования кода) данного объекта. Классы с высокой связностью выглядят как отлаженный механизм, в котором четко определены роли и нет заменяемых частей. Конечно, добиться этого сложно и не всегда нужно, но иметь ввиду данный аспект стоит.
2)Связанность.
Связанность - мера взаимодействия объектов внутри системы, между модулями и даже программами. Чем больше один класс обращается к другим для выполнения определенных действий, тем выше вероятность того, что другие классы выполняют его задачи. Лучший уровень связанности - минимальный.
В призмах двух этих величин мы и будем рассматривать принципы SOLID, так как соответствие им ведёт к построению легко обслуживаемого и расширяемого кода. Мы же вроде за этим здесь, не так ли?
Принцип единственной ответственности.
Один класс - одна забота. Долгое время я не мог понять, что конкретно обозначает этот принцип. Методы класса сами по себе выполняют разного рода задачи и тогда, судя по всему, мы должны всякий раз выделять новый класс из текущего при решении добавить новый метод. Суть же в другом. Стоит разграничивать функцию и сущность.
Функция - некоторый алгоритм, в результате которого мы получаем определенный результат. Функция назначена для выполнения одной цели. Сущность же в свою очередь выполняет несколько задач, решаемых с помощью его методов, и самое главное, клиент ждёт от сущности наличия данных методов для решения поставленных задач.
Проще говоря, описывая класс, мы уже подсознательно закладываем набор действий, которые хотим выполнять с его помощью. Чаще всего неверное именование либо трактование названия и вызывает создание лишних функций в классе, использование которых ставит под сомнение связность элементов класса. Верный класс выполняет строго ожидаемый набор функций, который и называется ответственность. Чтобы проще было ориентироваться, говорите: "Мой класс отвечает за ...".Причем предложение не должно содержать союзов и, или. Но и размывать ответственность общим термином не стоит. Пример верного использования:
Мой класс отвечает за переход между игровыми сценами.
Мой класс отвечает за создание ловушек на игровом поле.
Неверно:
Мой класс отвечает за игровые сцены.
Мой класс отвечает за игровые ловушки.
В сути ответственности должен лежать процесс, а не сущность, которая её должна обеспечивать.
Вот пример из проекта. Класс обеспечивает очередность выполнения "матчинга":
Наличие лишних ответственностей равносильно слабой связности. Один массив методов использует одни поля, другие свои. В итоге выходит несколько классов в одном. И в таком случае, если данные классы должны создаваться из текущего, стоит выбрать выделение функционала в отдельные цельные классы и с помощью композиции организовать нужное нам поведение (естественно на одном уровне абстракции).
Принцип открытости/закрытости
Ключевой принцип гибкости системы, это его способность изменять поведение в зависимости от контекста или целей. При этом желательно как можно меньше нарушать существующий код и вносить в него изменения, чтобы не изменить контрактную модель всей программы. Именно такую мысль и диктует этот принцип - классы должны быть открыты для расширения и закрыты для изменения.
Полностью закрывать существующий код мы не хотим, так как это налагает большие издержки во время рефакторинга. А что насчёт открытости изменениям: разве создание кода не есть само определение модификаций? Тогда что от нас требуют?
Нас не то что подталкивают, нам прямо указывают на возможность создания удачных для модификации поведения решений. И для этого нам нужно не так уж много - использовать интерфейсы и абстрактные классы. Используя интерфейсы в классе мы обеспечиваем выполнение нужного поведения согласно определенным методам. А механизм наследования позволяет создавать как и полиморфные поведение, так и изменения на лету вроде фабрик и стратегий.
Очередной пример из проекта. Определяя в нужном месте поле DefiniedGameRule, мы можем изменять поведение объекта в классах наследниках.
Принцип подстановки Лисков.
Я не буду следовать стандартному объяснению, так как оно для меня кажется слишком сложным. Вот мое определение: классы наследники должны выполнять как минимум столько, сколько и классы предки, не требовать больше(предусловия) и не обещать меньше после выполнения(постусловия). Если объяснять с точки зрения контрактного программирования, здесь нет ничего сложного - у наследников должен быть четкий контракт, который мы должны реализовать. Мы ожидаем, что сможем заменять потомков наследниками, потому и такие требования. Если же мы предполагаем в будущем изменения, которые могут нарушить этот порядок, стоит изначально отделить классы, дабы не рушить систему и не получить кучу исключений в конце.
Если вы можете использовать класс наследника также свободно как и предка, скорее всего вы не нарушаете данный принцип.
Принцип разделения интерфейсов
Полезно объединять некоторый функционал в единый интерфейс. Но вы должны понимать , что если принцип единственной ответственности не будет выполняться для интерфейсов, то он будет нарушаться и для их реализующих классов.
В следующем примере мы разграничили интерфейсы по сути использования, хотя они и могут быть реализованы в одном классе:
Принцип инверсии зависимостей.
В какой-то мере принцип тоже трактуется по принципу единственной ответственности.
Суть такова: классы должны зависеть от абстракций , а не от других классов или деталей. Выделяйте и используйте интерфейсы, чтобы не вводить зависимость от конкретных реализаций.
Пример:
В данном случае мы получаем преимущество в использовании интерфейса ISkillActivator. Мы не зависим от конкретной реализации, выполняя строго определенный контракт. Такая тактика позволит нам уберечь себя от ошибок при расширении поведения класса Skill добавлением модулей в список используемых.
В заключение хочу отметить неоценимую пользу интерфейсов во всех принципах. Попытайтесь усвоить, что даже стандартными средствами можно выстраивать устойчивые и гибкие системы, которыми другие будут восхищаться, а некоторые - даже копировать.
#unity #unity3d #программирование #solid #c#