Влившись в коммерческую разработку на Golang, с удивлением обнаружил, что многие маститые разработчики или не умеют или сознательно не используют SOLID-принципы при написании кода на Go. Вероятно, это связано с тем, что Go - не объектно-ориентированный ЯП, а SOLID обычно упоминают как раз в контексте ООП. Опытный разработчик, КМК, склонен воспринимать “нет ООП”, как некое упрощение или недостаток. Но это не так. Go - полноценный язык, безо всяких скидок. Поэтому solid-принципы можно и нужно в нём применять. Предлагаю рассмотреть каждый из принципов на примерах. Поехали.

Содержание

Single Responsibility principle, принцип единственной ответственности

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

Так не делай Смешивать методы манипулирования данными конверта Envelope и механизмы сохранения\извлечением этих данных из БД - не стоит.

Делай так ✓ Манипуляции над данными Envelope - отдельно, работа с БД - отдельно.

Open Closed principle, принцип открытости-закрытости

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

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

Делай так

Или так

✓ Фильтрация производится в соответствии со спецификацией, соответствующей Parametr-интерфейсу или просто задаётся любой функцией-параметром. В том числе, при необходимости, можно сделать variadic-набор фильтров. Поведение GoodFiltering не меняется, а разработчик в любом месте кода может расширить функционал.

Liskov Substitution principle, принцип подстановки Барбары Лисков

Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы

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

В терминах Go, реализации должны добросовестно удовлетворять контракту(интерфейсу). А сами интерфейсы желательно делать небольшими.

В примере ниже реализации интерфейса FeeHandler для типов Currency и Stock отличаются, т.к. в случае со стоками мы заявили равенство наценок на цену Ask и Bid, отразили это в реализации. Что привело к ошибке в расчёте среднего значения стоимости и нарушению принципа LSP.

Правильным вариантом реализации для Stock будет отдельное присвоение наценки AskMarkup и BidMarkup.

Interface Segregation principle, принцип разделения интерфейсов

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

Так не делай

Делай так

Dependency Inversion principle, принцип инверсии зависимостей

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

Так не делай Высокоуровневый BadStats зависит от низкоуровневой DataBase со вложенными данными типа Information.

Делай так Высокоуровневый GoodStats отделён от низкоуровневых инструментов интерфейсом Searcher в структуре DataBase.

Это позволяет легко заменить хранилище данных, например на PossibleSQL, без модификации поискового механизма CardsInfo.