В BGL, помимо предоставления ценности нашим клиентам с помощью программного обеспечения, мы ценим хорошее качество и удобство обслуживания. Хорошо задокументированные принципы проектирования SOLID являются частью нашей структуры компетенций, и мы ожидаем, что инженеры-программисты всех уровней полностью поймут и будут использовать эти принципы как один из многих инструментов для достижения хороших практик программирования. Здесь Филипп Джонсон расскажет все, что вам нужно знать:
Я впервые познакомился с принципами проектирования SOLID, когда перешел из Microsoft и .NET, чтобы возглавить команду Ruby. Частью обязательного повышения квалификации было чтение книги Сэнди Мец по Solid Object-Oriented Design. Если вы раньше не сталкивались с Сэнди, она отличный автор и оратор, и она была сторонником принципов SOLID еще до того, как они стали широко популярными.
Изучая принципы в контексте Ruby, мне интересно, что здесь нет интерфейсов или строгой типизации. Хотя довольно сложно перевести принципы, в которых говорится о разделении интерфейсов и работе с абстракциями, в примеры на языке без интерфейсов или строгой типизации, понимание их как в этом контексте, так и в контексте языка, в котором они есть, например C #, на самом деле помог мне научиться писать лучший код.
Когда я упомянул коллеге, что узнал о чем-то, что называется принципами SOLID, они довольно сухо ответили: «Вы давно не были на собеседовании, не так ли?».
В некоторой степени это было правдой. Проработав несколько лет в крупной организации, я перемещался между командами внутри компании с минимальным количеством собеседований и часто занимал руководящие должности, а не просто занимался программированием. Я обнаружил, что недавно на собеседованиях с разработчиками стало нормой спрашивать о принципах SOLID ... Может ли разработчик назвать каждый принцип? Пишут ли они свой код вслед за ними? И т.п.
Независимо от того, следуют ли разработчики этим принципам или нет, они стали основным продуктом Google - определения пунктов списка перед собеседованием, поскольку почти гарантировано, что их будут спрашивать - иногда в компаниях, которые их даже не используют. Это просто стандартный вопрос на собеседовании.
Почему они так популярны?
Причина, по которой они так популярны, и почему многие успешные компании и разработчики действительно используют их при написании кода, заключается в том, что они, как правило, приводят к улучшению кода с точки зрения читабельности, ремонтопригодности, шаблонов проектирования и тестируемости.
Я слышал, как люди совершенно справедливо утверждают, что вы все равно можете писать плохой код, который следует всем принципам. Для меня, однако, этот аргумент - все равно что сказать, что вы все равно можете плохо водить, даже если вы разместите зеркала перед поездкой так, чтобы вы могли видеть вокруг и позади себя. Это не повод не поправлять зеркала. В том же смысле принципы SOLID - не серебряная пуля, это всего лишь один из многих инструментов, которые разработчикам необходимо учитывать в своем наборе инструментов при написании лучшего кода, который они могут.
Откуда берутся принципы SOLID?
Принципы SOLID взяты из эссе, написанного в 2000 году Робертом Мартином, известным как дядя Боб, где он обсуждал, что успешное приложение изменится и без хорошего дизайна может стать жестким, хрупким, неподвижным и вязким.
- Жесткий - все очень исправлено. Вы не можете перемещать или менять вещи, не затрагивая другие вещи, но ясно, что сломается, если вы сделаете изменение.
- Хрупкий. Легко перемещать и менять вещи, но не очевидно, что еще может в результате сломаться.
- Неподвижный. Код работает нормально, но вы не можете повторно использовать код, не дублируя или реплицируя его.
- Вязкость - все разваливается, когда вы вносите изменения, вы быстро собираете их обратно и заставляете их работать. То же самое происходит, когда кто-то другой приходит, чтобы внести изменения.
Принципы, о которых говорит Роберт Мартин, чтобы избежать четырех вышеуказанных антишаблонов проектирования, стали известны как принципы SOLID. Хотя он не изобретал принципы, он объединил несколько хороших практик программирования, которые уже существовали вокруг центральной темы управления зависимостями, и выдвинул хороший аргумент в пользу использования этих практик вместе.
Целью принципов SOLID является уменьшение зависимостей, позволяющее изменять код в одной области приложения, не влияя на код в других областях.
Твердые принципы
Единая ответственность
«Не должно быть более одной причины для смены класса»
По сути… не делайте разных вещей в одном классе или методе. Если вы реализовали чтение из текстового файла, запись в базу данных, вызов службы и запись в журнал одним и тем же методом, то вы нарушаете этот принцип.
Запах кода для нарушения принципа единой ответственности возникает, если в вашем наборе тестов много тестов для одного класса или метода, например, один метод записывает в базу данных и выводит текстовый файл.
В этом примере показан метод, выполняющий несколько задач. Он записывает в базу данных и выводит текстовый файл. Его необходимо изменить по нескольким причинам, таким как изменение схемы базы данных, изменение типа базы данных (например, SQL Server, Oracle и т. Д.), Изменение местоположения или формата записываемого файла и т. Д. , если вы пишете в базу данных в нескольких местах, то любые изменения необходимо будет внести во всех этих местах.
Я бы реорганизовал этот код для введения единой ответственности, чтобы абстрагировать две зависимости, которые у нас есть в этом одном классе и методе, до отдельных классов с одной зависимостью для каждого нового класса (т.е. один класс для обработки вызовов базы данных, а другой для обработки записи в файл). Это гарантирует, что классы, которые мы создаем, также будут следовать принципу единой ответственности. Для хорошей практики я бы также добавил эти зависимости в этот исходный класс в конструкторе. TDD - тема для другой статьи, но внедрение зависимостей, подобных этой, позволяет легко имитировать зависимости, позволяя нам имитировать зависимости в тестах и сосредоточиться на тестировании единственной ответственности тестируемого класса.
Еще один рефакторинг здесь для меня с точки зрения единой ответственности, и единственная причина для изменения - это посмотреть на код сейчас и измерить скорость изменения вещей в каждом классе. Могут ли вещи меняться с одинаковой скоростью? В приведенном выше коде для меня выделяются волшебные строки в обоих классах репозитория. Вероятно, что местоположение файла может измениться больше, чем, например, изменится класс StreamWriter. Итак, теперь я бы вытащил те вещи, которые должны были измениться, в класс, полный других вещей, которые могут измениться с той же скоростью. На самом деле это был бы файл конфигурации, но для единообразия примеров кода я использовал новые классы.
Этот код намного чище и абстрагирует зависимости. DatabaseRepository теперь является единственным классом, который изменится при изменении БД, FileRepository - теперь единственный класс, который изменится в зависимости от файловых зависимостей, а класс SingleResponsibility изменится только в том случае, если нам нужно сделать что-то еще или прекратить делать то, что мы уже делаем.
Хотя это не гарантированный способ обеспечить единую ответственность, хороший автоматизированный способ поощрения, который я видел, - это автоматическая проверка сборки для ограничения длины классов, методов и т.д. вещи в том же месте.
Принцип открытости / закрытости
«Модуль должен быть открыт для расширения, но закрыт для изменения»
Чтобы добавить или изменить функциональность в существующем классе, вы должны иметь возможность делать это, расширяя класс, а не изменяя его, то есть наследовать от класса в подклассе, который расширяет поведение суперкласса с помощью новой функциональности. Кроме того, я считаю, что это включает в себя отсутствие нарушения существующих интерфейсов, вам следует только добавлять к ним, а не удалять или изменять существующие элементы интерфейса.
Примером этого является возможность изменить поведение чего-либо без изменения кода. Здесь мы видим, как старая утка, которая больше не может летать, расширяет возможности полета утки, а затем как дрон можно использовать, чтобы расширить возможности полета этой старой утки, чтобы поднять ее и позволить ей летать, и все это без какого-либо кода. меняется на Duck или OldDuck.
Принцип замещения Лискова
«Подклассы должны заменять их базовые классы»
Пусть q (x) - свойство, доказуемое для объектов x типа T. Тогда q (y) должно быть доказуемо для объектов y типа S, где S - подтип T.
Самый простой пример антипаттерна, который я видел, - прямоугольник не может быть подклассом квадрата, который добавляет высоту. Это нарушает принцип подстановки, потому что у вас может быть набор квадратов, содержащих несколько прямоугольников. Поскольку код имеет дело с тем, что он считает квадратами, может быть перечисление, в котором предполагается, что площадь может быть вычислена только по ширине квадрата, но для прямоугольников вам понадобятся оба измерения.
Отличный запах кода для понимания этого принципа - это метод, который принимает параметр определенного типа или интерфейса, но затем проверяет фактический тип переданного параметра и по-разному работает с разными типами.
Когда вы рассматриваете этот плохой код выше, если прямоугольник или квадрат изменяются, то код в этом другом методе и классе также может потребоваться изменить. Это также нарушает принцип единой ответственности, показывая, как эти принципы пересекаются и хорошо работают вместе. Устранение проблем кода, относящихся к одному принципу, часто устраняет и другие проблемы кода.
Что-то еще, что нарушает принцип замещения Лискова для пуристов, - это преобладающие методы. Формула ясна: если вы делаете что-то с одним типом, результат должен соответствовать тому, что вы делаете то же самое с заменяемым типом. Например, если вы рассматриваете банковский счет, у которого есть метод, возвращающий сумму, которая может быть снята, то вызов этого метода в подклассе должен возвращать то же значение. Этот код показывает это нарушение, поскольку BankAccount вернет 50 из OverdraftLimit, а PlatinumAccount вернет 100.
Разделение интерфейсов
«Многие клиентские интерфейсы лучше, чем один интерфейс общего назначения»
Интерфейсы следует разбивать на небольшие связанные коллекции членов, которые будут реализованы всеми классами, реализующими этот интерфейс.
Самый простой пример, который я видел в этом случае, - это то, что птицы ведут себя по-разному:
· Некоторые могут ходить
· Некоторые умеют летать
· Некоторые умеют плавать
Затем у вас есть некоторые животные, например рыбы, которые тоже могут делать некоторые из этих вещей, но не все.
Вместо того, чтобы реализовывать интерфейс для животного, у которого есть три члена (например, ходить, летать и плавать), если вы реализуете три разных интерфейса (ходьба, полет и плавание), вы можете реализовать эти интерфейсы для определенных классов животных, например, рыба может реализовать плавать но утка может реализовать все три. Эти интерфейсы также можно использовать для других вещей, таких как типы животных, которые могут ходить, летать и т. Д., Или даже использовать интерфейс полета на чем-то вроде дрона или самолета.
Если вы обновляете интерфейс, вам необходимо обновить все, что реализует интерфейс. Один большой интерфейс обычно реализуется большим количеством классов, чем отдельный интерфейс, поэтому изменения большого интерфейса обычно требуют изменений большего количества классов. Кроме того, большие интерфейсы часто реализуются классами, которые разделяют некоторые общие свойства, но не все, и интерфейсы заканчиваются некоторыми специфическими для класса свойствами, которые необходимо реализовать в некоторых классах с «нереализованными» исключениями.
Если мы посмотрим, как этот пример можно плохо реализовать с одним большим интерфейсом:
Если мы добавим что-нибудь в интерфейс IAnimal, нам нужно будет обновить классы Duck и Fish. Кроме того, нам пришлось реализовать скорость ходьбы для Fish, хотя мы знаем, что рыба не может ходить.
Теперь давайте посмотрим на реализацию этого с меньшими изолированными интерфейсами:
Здесь мы можем увидеть, как нам удалось разделить SwimBehaviour как для Duck, так и для Fish, но мы реализовали скорость ходьбы только для Duck, а не для Fish. Это означает, что мы можем добавить другие атрибуты ходьбы в интерфейс IWalking, например длину шага, и нам нужно реализовать их только на Duck, тогда как с одним большим интерфейсом нам пришлось бы реализовать их и на Fish.
Инверсия зависимостей
«Положитесь на абстракции. Не полагайтесь на конкременты.
Модули высокого уровня не должны зависеть от модулей низкого уровня, а детали должны зависеть от абстракций.
Если мы рассмотрим часть DatabaseRepository в приведенном выше примере единой ответственности и напишем репозиторий для другой базы данных, например, для Oracle вместо SQL. В настоящее время нам нужно обновить наш класс, который зависит от DatabaseRepository, чтобы изменить его для использования репозитория Oracle вместо репозитория SQL. Этот запах кода виден здесь, в этом модифицированном примере:
Это легко исправить, обновив класс DependencyInversion, чтобы он принял интерфейс, который реализуют как SqlDatabaseRepository, так и OracleDatabaseRepository, например:
Этот отредактированный код теперь означает, что любой код, использующий класс DependencyInversion, может взаимозаменяемо внедряться в класс репозитория SqlDatabaseRepository или OracleDatabaseRepository без каких-либо изменений кода или перекомпиляции класса DependencyInversion.
Я часто слышал, как инженеры цитируют этот принцип как внедрение зависимостей или приводят в качестве примера внедрение зависимостей. Хотя в целом они хорошо сочетаются друг с другом, и использование внедрения зависимостей может привести к инверсии зависимостей, это не всегда так.
Внедрение зависимостей в современных языках обычно реализуется с использованием контейнера DI, в котором зависимости загружаются через конфигурацию, которая связывает интерфейс с конкретной реализацией. Чаще всего зависимости контейнеров DI используются в конструкторах контроллеров MVC, где зависимости, такие как постоянство или репозитории служб, объявляются как параметры типов интерфейса. Когда платформа создает экземпляр контроллера, конкретная реализация параметра интерфейса, как определено в конфигурации, автоматически передается и затем используется в действиях контроллера. Эта реализация внедрения зависимостей действительно является инверсией зависимостей, и вам не нужно просто использовать контейнеры DI в контроллерах, инженеры обычно могут добавлять зависимости в контейнер и извлекать их в коде, чтобы использовать их где угодно.
Однако в своей простейшей форме внедрение зависимостей просто означает передачу (или внедрение) зависимостей в метод или конструктор, которые будут использоваться в этом методе, а не созданы в методе. Вам не нужно использовать контейнер DI, и вы можете внедрять конкретные реализации зависимостей, и в этом случае вы реализуете внедрение зависимостей, нарушая принцип инверсии зависимостей.
Лучший способ справиться с инверсией зависимостей, наряду с реализацией против интерфейсов, - это посмотреть на порядок зависимости ваших объектов, расположить их на диаграмме, где объекты, расположенные дальше справа, зависят от объектов слева. Теперь подумайте, как быстро вы ожидаете изменения каждого объекта. Если объект слева будет меняться быстрее, чем объект справа, вам следует подумать об инвертировании этой зависимости. Внедрение зависимостей часто является хорошим способом сделать это.
Рассмотрим простой пример, похожий на FileRepository, который мы рассматривали в примере SingleResponsibility, но где FileRepository отвечает за создание экземпляра FileConfig, а не за его внедрение в конструктор в качестве зависимости.
Если мы разместим порядок этих классов в порядке зависимости с классами справа в зависимости от классов слева, мы получим следующий порядок, в котором MyApp зависит от FileRepository, который зависит от FileConfig:
FileConfig ‹Репозиторий файлов‹ MyApp
Теперь, если мы рассмотрим порядок, в котором мы ожидаем, что что-то изменится ... FileRepository, вероятно, не сильно изменится ... способ, которым система записывает файл, вряд ли изменится. MyApp, вероятно, изменит больше, чем FileRepository, поскольку мы добавляем новые функции и возможности, поэтому порядок между этими двумя зависимостями кажется правильным. FileConfig, однако, является зависимостью от FileRepository, но с большей вероятностью изменится, чем она. Это нарушает принцип. Если FileConfig изменится, что вполне вероятно, нам нужно будет создать FileRepository, а затем также создать MyApp. Мы должны обратить внимание на инвертирование этой зависимости и переместить FileConfig вправо от FileRepository с точки зрения порядка зависимостей.
Для этого мы можем создать экземпляр FileConfig в MyApp и внедрить его в FileRepository.
Это инвертирует зависимость между FileConfig и FileRepository, сдвигая FileConfig вправо в порядке зависимостей и вероятности изменения, если FileConfig изменится, что, вероятно, нам нужно только сейчас построить MyApp, а не FileRepository.
Резюме
Мы видели, как определить запах кода нарушения принципов SOLID и как рефакторинг некоторых простых примеров следовать принципам SOLID.
Вы могли заметить, что примеры кода, которые следуют принципам SOLID, длиннее, чем код, который этого не делает, и это правда, что предварительная подготовка кода занимает немного больше времени, чем просто застревание, написание кода, который работает и доставка Это.
Одна вещь, которую вы можете гарантировать с приложением, - это то, что, если оно будет успешным и будет использоваться, оно со временем изменится. По мере его изменения коэффициент сложности постепенно увеличивается, пока вы не достигнете критической точки, когда становится все труднее и требуется больше времени для доставки новых функций поверх плохо написанного кода, который был быстро отправлен.
Этот график, взятый из Гипотезы выносливости дизайна Мартина Фаулера, показывает, что, хотя в краткосрочной перспективе внести изменения быстрее и проще, не уделяя внимания качественному дизайну, в долгосрочной перспективе вы достигнете точки, когда становится быстрее предоставлять функции с хорошо продуманным кодом.
Если вы планируете написать успешное приложение, а не то, которое не выйдет из строя до того, как достигнет этой переломной точки, имеет смысл потратить это немного дополнительного времени и написать дополнительный код, следуя принципам SOLID. Ваше будущее будет вам благодарно в долгосрочной перспективе.
Со временем цель состоит в том, чтобы избежать написания плохого кода и начать с разработки хорошего кода заранее. Не ожидайте, что весь код будет идеальным, даже если вы потратили его на разработку… всегда смотрите на код, который вы написали, и обдумывайте, как вы могли бы начать его рефакторинг.
Я оставлю вам вопросы, которые я нашел полезным задать себе при написании кода. Этот список похож на тот, который Сэнди Мец рекомендует следовать при рассмотрении вопроса о том, является ли класс ТВЕРДОМ:
1. Является ли класс СУХИМ?
2. Вычеркнул ли я что-нибудь, что может измениться?
3. Абстрагировал ли я что-нибудь, что не используется во всех наследующих его классах?
4. Все ли в классе меняется с одинаковой скоростью?
5. Зависит ли класс от вещей, которые меняются реже, чем он сам?
Мартин Фаулер - Гипотеза выносливости дизайна: https://martinfowler.com/bliki/DesignStaminaHypothesis.html
Санди Мец - GORUCO 2009 - ТВЕРДЫЙ объектно-ориентированный дизайн: https://www.youtube.com/watch?reload=9&v=v-2yFMzxqwU
Роберт Мартин - 2000 - Принципы и шаблоны проектирования: https://web.archive.org/web/20150906155800/https://www.objectmentor.
Ru / resources / article / Principles_and_Patterns.pdf
Первоначально опубликовано на https://medium.com 23 октября 2019 г.