(java || kotlin) && devOps
370 subscribers
6 photos
1 video
6 files
316 links
Полезное про Java и Kotlin - фреймворки, паттерны, тесты, тонкости JVM. Немного архитектуры. И DevOps, куда без него
Download Telegram
Всем привет!

Продолжая тему SOLID. Есть в этой аббревиатуре буква L - принцип Барбары Лисков, который гласит, что все потомки должны соблюдать контракт, объявленный в родителе.
Объясню на пальцах. Есть класс или даже скорее интерфейс А. Есть класс Б, как-то использующий А - например, принимающий его как параметр в методе или конструкторе. И принцип Лисков будет соблюдаться в том случае, если класс Б не "упадет" при получении любого (!) потомка класса А, т.е. класс Б не должен разбираться в том, какого именно потомка А он получил.

На первый взгляд все просто - в Java и Kotlin строгая типизация и инкапсуляция, поэтому если мы объявили интерфейс с определенными методами и типами параметров - все потомки обязаны их реализовать. Что тут может сломаться?

Если копнуть глубже, то в Java наследники все же могут изменять контракт:
1) заменить тип возвращаемого методом результата на тип-наследник
2) добавлять в сигнатуру метода исключения-наследники и увеличивать число объявленных исключений, тоже наследниками
3) повышать область видимости метода
Все это расширяет ограничения в исходном контракте. Поломать корректно написанный класс Б, который знает только про А - интерфейс предка - так не получится. Хотя расширяя таким образом контракт, нужно быть осторожным, т.к. мы подталкиваем потребителей нашего нового класса к использованию конкретного класса вместо интерфейса, а это уже может привести к нарушению другой буквы - D )))

Как же можно точно нарушить контракт? Для этого нужно ответить на вопрос - что есть контракт?
Контракт не равно Java API. Это может быть:
1) назначение метода. Например, называя метод getXXX или checkXXX мы как бы говорим потребителю, что он не меняет состояние объекта, т.е. нет побочных эффектов
2) порядок вызова методов класса. Опять же исходя из названий методов мы можем предположить некий порядок вызовов - init() вызываем в начале, build() - в случае паттерна Builder в конце.
3) назначение класса. Если опять же исходя из названия класса он предназначен только для работы с кэшом - нужно в потомках лазить в БД, это должны быть разные сервисные классы. Если предполагается, что в сервисном классе данного уровня нужно отбрасывать логи и метрики - все потомки должны это делать. И наоборот.
4) политика по исключениям. Если предполагается, что метод возвращает внутреннюю ошибку как результат вызова (Pair или свой класс), то именно так и должны поступать все потомки
5) время выполнения. Если в контракте явно предполагается, что метод что-то рассчитывает in-process и не ходит во внешние системы - контракт нужно соблюдать.
6) зависимости от других классов. Нужно стремится к тому, чтобы все зависимости были явно указаны в API и передавались в класс при создании. Т.е. класс Б не должен как-то настраивать среду для работы конкретного потомка класса А. Да, речь про Inversion of Control.
7) иммутабельность. Если предполагается иммутабельная реализация - все потомки должны быть такими.

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

И последняя очевидная, но IMHO важная мысль: чтобы наследники соответствовали контракту - для начала контракт нужно описать)

#arch #solid #interview_question
👍4
Всем привет!

Когда-то на своей первой работе я услышал от коллеги совет: "Не надо бросаться реализовывать в коде первую попавшуюся идею. Подумай, найди пару альтернативных решений и сравни их".

Так вот, спустя более 15 лет практики хочу сказать - это очень важная и полезная мысль!)
По моему собственному опыту в 25-50% случаев первая пришедшая в голову идея не является оптимальной. Иногда приходится выкинуть все сделанное, иногда - сильно ее видоизменить. Причем часто это происходит еще в процессе работы над исходной задачей, при попытке вписать свой новый код в существующий сервис.
По сути речь идет о проектировании и его качестве. Когда нужно проектирование? Почти всегда, кроме исправления тривиальных багов - опечаток, замены текстовок например. А на самом деле даже простой в исправлении баг может указать на необходимость рефакторинга, а рефакторинг в любом случае требует проектирования.
Хорошим вариантом проверить качество идеи является практика TDD - в этом случае придется сразу написать типовые примеры использования вашего нового класса в виде тестов. Соответственно, сразу будет видно как новый код интегрируется с существующим, подходит ли для всех случаев использования.
При этом важно отметить, что TDD не заменяет проектирование.
Единственная альтернатива проектированию, которую я вижу - закладывать 1,5х или 2х времени для работы над задачей, чтобы была возможность все переделать) Альтернатива так себе, по моей оценке время на проектирование будет ниже.
Еще худший вариант: видя, что новый код придется переделывать - "докостылить" его до работающего варианта и записать себе задачу с техдолгом. К таким техдолгам, увы, долго не возвращаются.

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

#arch
👍5
Всем привет!

Могу порекомендовать неплохую статью, объясняющую в чем принципиальная разница между классической архитектурой, основанной на слоях, и более современными и в целом похожими Onion Architecture (ну не привычно мне называть ее луковой :) ), и гексагональной, она же Порты и адаптеры.
Собственно статья https://habr.com/ru/articles/344164/

#arch