Привет Хабр!
Когда я гуглил эту тему, я планировал выпустить только видео для плейлиста о Паттернах. Но как то мне не очень понравились уже существующие статьи, возможно потому что я пишу на JS, а там примеры были, то на python, то еще на каком языке. Поэтому я решил опубликовать свое видение темы Программируйте на уровне интерфейсов. Возможно это будет кому-то полезным (Данная статья является расшифровкой видео).
Что подразумевается под словом интерфейс?
Начнем с базовых понятий. Что в данном принципе подразумевается под словом интерфейс? Ведь в JS нет интерфейсов, нам что нужен TypeScript, чтобы пользоваться этим принципом? Ответ, однозначно нет
Интерфейс в данном случае, это скорее идея. Мне нравится как эта идея описана в википедии:
Интерфе́йс — граница между двумя функциональными объектами, требования к которой определяются стандартом совокупность средств, методов и правил взаимодействия (управления, контроля и т. д.) между элементами системы.
Интерфейсы в реальном мире
Допустим вы хотите посмотреть кино на телевизоре. В данном случае функциональные объекты это вы и телевизор и вам нужно как то взаимодействовать. У вас есть требования к телевизору: включить его, сделать громче или тише, поставить на паузу или продолжить просмотр. И в данном случае границей между двумя функциональными объектами, удовлетворяющей нашим требованиям является пульт от телевизора. Да, именно он является интерфейсом взаимодействия с телевизором.
Интерфейсы в js
И такого рода интерфейсы нас окружают везде. Точно как и в программировании. Вы например используете какую-то npm
библиотеку, допустим classnames
, которая позволяет добавлять и убирать CSS
классы в зависимости от разных условий. Если вы откроете документацию, вы увидите, что у нее четко описано, какие параметры принимает npm
пакет и какой результат возвращает. Это и есть интерфейс npm
пакета classnames
.
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
Интерфейсы существуют не только у npm
пакетов, но и даже в любом вашем внутреннем модуле, компоненте или просто функции. Ровно в тот момент когда вы придумали, какие параметры функция принимает, как функция называется и что именно она возвращает, вы придумали интерфейс данной функции. А такие инструменты как TypeScript позволяют вам лишь описать интерфейс более явно.
type User = Readonly<{
id: string
username: string
email: string
}>
type getUserById = (userId: string) => User
Определение принципа
Я думаю вы поняли, что подразумевается под словом интерфейс в данном принципе. Остается лишь ответить на вопрос: “а что значит программировать на уровне интерфейсов?”
Мне нравится больше всего следующая трактовка: “Программирование на уровне интерфейса - это спроектировать свою программу так, чтобы она не зависела от конкретных реализаций и их можно было заменить на другие в случае необходимости”.
Давайте применим эту трактовку к предыдущим примерам. Пульт от телевизора спроектирован так, чтобы его работоспособность не зависела от конкретной модели, или вообще от марки телевизора. Зачастую мы можем увеличить или уменьшить звук одним пультом в разных телевизорах в разных комнатах.
И если вы решите создать свой телевизор, вы будете думать: "Ага! Мой телевизор должен хорошо взаимодействовать с любым пультом". Т.е. интерфейс был разработан ранее и вы сейчас делаете реализацию вашего телевизора, которая будет удовлетворять условиям уже существующего интерфейса, а не наоборот. Это и есть программировать на уровне интерфейсов, а не на уровне реализаций.
Реальный пример
Давайте рассмотрим и боевой пример. Неотъемлемой частью многих современных приложений есть какой-либо чат. Вы можете переписываться с другими пользователями в мессенджерах, либо переписываться с тех поддержкой какого-либо банковского приложения.
При написании любого чата, вам нужно получать сообщения в режиме реального времени. И допустим в нашем проекте решили использовать популярную технологию web socket
. Сервер готов, технология которую будем использовать так же прозрачна и понятна. Остается только вопрос: “а зачем в такой ситуации программировать на уровне интерфейсов?" Ведь все и так понятно, уже можно писать реализацию, время только сэкономим”
Но если вы так поступите, у вас в будущем могут возникнуть например следующие сложности:
Ваш чат сильно завязан на конкретную технологию web socket
. Но в настоящее время, сильно набирает популярность технология SSE (server-sent-events)
. И если вы решите мигрировать на нее, вероятно у вас изменится структура присылаемых с сервера данных, вариант ее подключения и многое другое. Вопрос лишь в том, насколько все эти вещи у вас размазаны по проекту. Ведь чем больше они размазаны, тем больше шансов насоздавать баги и тем больше команде тестирования придется проводить регрессию приложения.
Вы зададите вопрос, так а как программирование на уровне интерфейсов решает эту проблему?
Программируем на уровне интерфейсов
Чтобы ответить на этот вопрос, давайте попробуем программировать на уровне интерфейсов. Для этого задумайтесь, есть ли вашему чату разница каким образом вы получаете сообщения: вы получили их по web socket
, по SSE
или вообще отсылаете каждые 3 секунды https
запрос на сервер, чтобы узнать, а не пришли ли новые сообщения. Чату мне кажется на это абсолютно все равно. Чат хочет знать лишь сообщение, которое пришло.
Тогда возможно стоит создать слой абстракции, который будет отделять реализацию получения сообщений, от кода самого чата, назовем его messageReceiver
.
Остается придумать только интерфейс для этой абстракции. Думаю ему будет достаточно принимать всего одну функцию onMessage
, которая будет вызываться, когда в модуль пришло новое сообщение.
interface MessageReceiver {
onMessage: (message: Message) => void
}
При таком подходе, чату стало абсолютно все равно какую из технологий вы используете. Все нюансы технологии скрыты за слоем абстракции. Мы лишь знаем как выглядит интерфейс этой абстракции. И даже более того, если сервер еще не решил какую из технологий он будет использовать для отправки сообщений, внутри этого модуля можно поставить заглушку, которая будет имитировать поведение сервера.
Думаю каждому из вас под силу быстро написать генератор рандомных сообщений, например каждые от 1 до 20 секунд модуль вам будет присылать рандомный набор букв. В итоге, то что сервер медлит с выбором технологий - это не будет вас никак блокировать, вы сможете разрабатывать следующие фичи чата.
А когда серверная команда закончит выбирать технологию, вы не спеша можете запланировать на один из будущих спринтов миграцию с вашего генератора, на SSE
реализацию или какую-либо другую технологию, это уже совсем не важно. И более того, если окажется, что сервер плохо работает и вы ловите ошибки, вы сможете временно вернуть реализацию с рандомным генератором сообщений. Согласитесь, удобно ведь.
А если завтра опять захотят заменить на новую технологию, вам это не составит никакого труда, вам придется обновить код лишь в одном изолированном модуле. И такой модуль гораздо проще тестировать, чем размазанный по всему проекту код. А уже завтра вы можете разрабатывать мобильное приложение, и такой модуль можно вынести в npm
пакет и пере использовать как в веб проекте, так и React Native
.
Дополнительная мотивация
К сожалению, для многих разработчиков все это не имеет никакой ценности, и данная статья их никак не переубедит. Все потому, что нет очевидной выгоды здесь и сейчас, а есть лишь эфемерная выгода, что когда-то будет замена технологий, или появится React Native
или произойдет еще какое-то событие.
А многие могут ответить, ой да у нас нативные разработчики делают мобилки, и сервер точно не будет мигрировать с сокетов ближайшие 5 лет, а там уже никто не знает что будет, может уже на новый Фреймворк переезжать будем и смысла особого нет, писать на уровне интерфейсов.
Но это совсем не так. Если вы начнете писать такие независимые модули и планировать свой проект на уровне интерфейсов. Вы обнаружите, что ваши многие модули совсем не зависят от текущего фреймворка. Вы увидите, как новым разработчикам гораздо проще и быстрее вникнуть в проект и начать вносить изменения в тот или иной модуль, т.к. весь лишний код инкапсулирован и не отвлекает от сути задачи. А весь ваш проект напоминаниет конструктор из модулей, который можно в любой момент заменить на новый и не нужно будет переписывать пол приложения на любой чих.
Только представьте насколько facebook нуждается в такого рода модулях. У них десятки тысяч компонентов в проекте и если не придерживаться такого рода программирования, вы никогда не сможете зарефакторить такой сложный проект. И как думаете наймут они вас, если вы скажете, что кодить на уровне интерфейсов, все это фигня или вы никогда этим не занимались, потому что видите ли у вас проект по проще?
Поэтому я рекомендую, вернувшись к своему рабочему проекту, взглянуть на ваш код новыми глазами. И подумать, где уже прям очевидно стоило бы добавить такого рода модуль с интерфейсом отвязанным от реализации. И тем самым уменьшить связанность вашей основной кодовой базы с конкретной реализацией модуля
Чао =)