Принципы, которые формируют успешные фреймворки

За последнее десятилетие появилось огромное количество фреймворков. Такие фреймворки как Spring и Ruby on Rails стали очень популярными и их изучение сильно повышает вероятность трудоустройства. Но на каждый успешный фремворк приходится большое количество фреймворков, которые так и не набрали популярность. На 1 янв 2008 года википедия содержала информацию о 67 веб-фреймворках. На сегодняшний день большинство из них либо перестали существовать либо не обновлялись больше трех лет. Как создатель фреймворка Yii, я потратил много времени на то, чтобы исследовать различные фреймворки и выяснить почему одни были успешными, а другие провалились. В этой статье я опишу некоторые выводы, которые по моему мнению помогут фреймворку стать успешным.

Почему фремворки?

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

Дуглас Шмидт описывает фреймворк как набор программных сущностей (таких как классы, объекты и компоненты), которые помогают строить реюзабельную архитектуру для схожих приложений. Согласно этому определению, фреймворк - это каркас приложения, содержащий реюзабельные и настраиваемые компоненты. Ожидается, что для создания приложения разработчик расширит и настроит фреймворк путем добавления своей логики.

Основополагающей характеристикой фреймворка является так-называемая инверсия управления. Обычно фреймворк играет главную роль и вызывает код самого приложения. Поток управления инвертируется здесь - фреймворк вызывает приложение, а не наоборот. На схеме ниже видно взаимодействие между фреймворком, приложением и библиотекой. Довольно часто фреймворк сам использует библиотеки.

Схема взаимодействия между фреймворком, приложением и библиотекой

Главным показателем полезности фреймворка является то, что он помогает разработчику повысить продуктивность и качество кода. Например, современные фреймворки (такие как Play, django) часто предоставляют инструменты для кодогенерации или готовые каркасы типовых приложений. Так же хорошо продуманный фреймворк задействует все меры безопасности, помогая разработчику писать более защищенные приложения.

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

Конечно, не все разработчики любят использовать фреймворки. Они жалуются на высокий порог вхождения, необходимость действовать в рамках выбранного фреймворка, низкую производительность и т.д. Но сегодня большинство из этих проблем не актуальны. Фреймворки вполне удачно справляются с ними, придерживаясь принципов, описанных в этой статье.

Принципы

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

Достаточно давно Тим Петерс сформулировал двенадцать принципов хорошего дизайна. Они известны как "Дзен Питона". Схожие идеи свойственны и многим другим языкам. И многие из этих принципов применимы в разработке фреймворков. Опираясь на свой опыт разработки фреймворков, я выделил наиболее важные принципы, которые необходимо соблюдать при разработке любого фреймворка.

  • Простота - это хорошо
  • Монолитность - это плохо
  • Будь последовательным
  • Явное лучше неявного
  • Соглашения выше конфигурации

Простота - это хорошо

Изучение нового фреймворка - это нелегкая задача. Изучая новый фреймворк, разработчик инвестирует в него свое время и силы и он ожидает, что эти инвестиции окупятся в его текущем и последующих проектах. В то время, как изучая библиотеку, разработчик может знакомиться с ее API по мере необходимости, фреймворк требует полного понимания его принципов до того, как разработчик приступит к его использованию. Поэтому очень важно спроектировать фреймворк так, чтобы он был понятным, доступным и приятным в работе.

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

Хорошим примером грамотного дизайна правил является работа с роутингом в Express.js - очень популярном серверном веб-фреймворке. Роутинг - это очень важная часть веб-приложений. Он определяет, как приложение должно реагировать на клиентские запросы. Express.js предоставляет простое правило для объявления роутов: app.METHOD(PATH, HANDLER), где METHOD - это метод HTTP запроса (GET, POST и т.п.), PATH - это URI часть запроса, а HANDLER - это функция-обработчик. Ниже видно, как выглядит объявление правил роутинга в Express.js.

var express = require('express');
var app = express();

// Запрос корневой страницы
app.get('/', function (req, res) {
    res.send('Hello World!');
});

// POST запрос к /user
app.post('/user', function (req, res) {
    res.send('Got a PUT request at /user');
});

// DELETE запрос к /user
app.delete('/user', function (req, res) {
    res.send('Got a DELETE request at /user');
});

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

Монолитность - это плохо

Здесь термин "монолитный" относится к фреймворкам, построенным в виде единой сильно связанной кодовой базы. Когда веб-фреймворки только набирали популярность, они как правило были монолитными т.к. их основной целью было обеспечить поддержку полного стека задач для разработки веб-приложений. Со временем стало ясно, что монолитные системы имеют много проблем. Например, внесение изменений в небольшую часть фреймворка требовало повторного тестирования и выпуска новой версии всего фремворка. А это в свою очередь влекло за собой обновление и пересборку всех приложений, использующих этот фреймворк. То, что код в монолитном фреймворке сильно связан делает трудным соблюдение обратной совместимости между различными версиями. Наконец, с появлением большого числа небольших узкоспециализированных фреймворков (для логирования, кеширования, работы в базой данных и т.д.) разработчики стали менее охотно пользоваться огромными монолитными фреймворками.

Современные фреймворки должны иметь слабо-связанную архитектуру. Full-stack фреймворки (например Spring) превратились в наборы слабо-связанных компонентов, которые могут быть использованы отдельно или заменены сторонними решениями. Специализированные фреймворки взаимодействующие "по контракту" делают конечное приложение менее зависимым от конкретного фремворка. Хорошим примером являются популярные сегодня фреймворки вроде Sinatra, Express.js и Martini. Эти фреймворки предоставляют базу для роутинга и обработки запросов к веб-приложению. Сами по себе эти фреймворки очень маленькие, но открытая архитектура позволяет легко расширять их до бесконечности, используя сторонние решения.

Будь последовательным

Будучи последовательным фреймворк везде использует одинаковые подходы в дизайне архитектуры, именовании, организации кода и т.д. Последовательный фреймворк снижает порог вхождения тем, что разработчик выучив аспекты одной части фремворка, может на ее примере быстро освоиться в оставшейся архитектуре. Последовательность помогает разработчику снизить количество ошибок из-за неправильного использования компонентов фремворка.

Например, когда мы проектировали конструктор запросов в Yii, в первую очередь мы стремились к последовательности. Конструктор запросов позволяет вам создавать SQL запросы программно и избегать SQL инъекций. Чтобы помочь разработчикам запомнить API, мы использовали текучий интерфейс и назвали методы соответственно их SQL аналогам. Следующий код демонстрирует, как можно создать SQL запрос, используя нам конструктор.

(new Query())
    ->select('id, email')
    ->from('user')
    ->orderBy('last_name, first_name')
    ->limit(10)
    ->all();

Этот код сгенерирует и выполнит следующий MySQL запрос:

SELECT `id`, `email`
FROM `user`
ORDER BY `last_name`, `first_name`
LIMIT 10

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

Явное лучше неявного

Этот принцип подразумевает написание самодокументирующегося кода и избегания "магии". На то есть две причины. Во-первых, явный код легче для понимания и сопровождения. Если код не требует пояснений, то разработчику, который будет его сопровождать (а это не обязательно будет его автор), не придется долго ковыряться в нем, чтобы понять, как этот код работает. Во-вторых, явный код менее подвержен ошибкам. Хотя явность требует написания большего числа строк кода, она снижает вероятность того, что разработчику придется "заглядывать под капот", чтобы понять, как этот код работает.

Для примера рассмотрим два куска кода из ORM, которые преследуют одну и ту же цель: создать связь между записями order и customer.

$order->link('customer', $customer);
// Или
$order->customer = $customer;

В первом случае идет обычный вызов метода. Второй вариант выглядит лучше т.к. сложная операция связывания может быть произведена при помощи обычного присваивания. Тем не менее, иллюзия простоты второго варианта омрачается сложностями, скрытыми в других местах. Например, перед тем, как использовать этот специальный синтаксис для связывания, разработчик должен изучить документацию к нему. Так же из-за того, что операция связывания выглядит как обычное присваивание, разработчик может забыть обработать потенциальные исключения, которые могут быть выброшены из-за чего программа будет работать со сбоями.

Во время разработки Yii мы долго обсуждали оба варианта и в итоге остановились на первом.

Соглашения выше конфигурации

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

Впервые принцип "соглашения выше конфигурации" был применен в фреймворке Ruby on Rails. RoR предоставляет библиотеку ActiveRecord, которая сопоставляет программные классы и таблицы базы данных. По соглашению название таблицы - это название класса во множественном числе. Так, класс Account будет сопоставлен с таблицей Accounts. Но если название таблицы отличается, разработчик может указать это явно в конфигурации.

Многие MVC фреймворки следуют этому принципу для роутинга запросов. Например, фреймворк Sails.js следуя соглашению, при запросе к /we/sya/hi запустит экшен hi, класса SayController, находящегося в директории controllers/we. Это соглашение позволяет разработчику не указывать правила роутинга для экшенов. Тем не менее, если разработчику нужно использовать особенные правила роутинга, он может это сделать, указав их явно.

Принцип "соглашения выше конфигурации" позволяет разработчику писать меньше кода. Но так же он приводит к увеличению правил, которым должен следовать разработчик. Кроме того, он конфликтует с принципом "явное лучше неявного", который обсуждался выше. Примечательно то, что хотя в ранних версиях Spring использовал такое же соглашение для правил роутинга, как и Sails.js, в более поздних версиях он требует от разработчика указывать роуты явно, используя анотации. Таким образом, решение о введении нового соглашения должно приниматься взвешенно.

Итоги

Разработка успешного фреймворка - это нахождение баланса между простотой и функциональностью. На протяжении всей разработки необходимо принимать решения, руководствуясь вышеперечисленными принципами.

Иногда вы можете сталкиваться с ситуациями, когда эти принципы будут конфликтовать друг с другом. Что важнее: последовательность или простота? Соглашения или ясность? В таких случаях всегда помните, что конечная цель фереймворка - упростить работу разработчику и ускорить написание кода. Поэтому делайте его простым и понятным. Соглашением можно пожертвовать в угоду ясности т.к. оно может усложнить работу. Аналогично последовательность можно слегка нарушить, если следование ей вносит дополнительные сложности.

Это вольный перевод статьи "Philosophies that Shaped Successful Frameworks", которую написал Qiang Xue.

Коментарии

Используйте Markdown

Спасибо за коментарий!
Ваше сообщение будет доступно после проверки.

#1 Haru Atari

Спасибо, исправил.

#0 Гость

Опечатка: "У каждый фремворка ест"