Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Как стать программистом и избежать детских ошибок Разделяйте по смыслу, а не по форме


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

Давайте разберём эту тему. Она, кстати, будет ещё откликаться эхом в последующих статьях.

Историческая справка

«Ты помнишь, как всё начиналось...» © А. Макаревич

Я помню. Начиналось всё с опыта и выводов.

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

Мы можем иметь несколько разных программ, написанных разными людьми — и этот зоопарк будет решать задачу заказчика, если все они будут работать с общими согласованными данными. Sic!

Не верите?

Представьте обратную ситуацию. Бухгалтер работает с 1C, а его директор смотрит данные в Excel. Директор увидит только то, что принесёт ему бухгалтер, после выгрузки. То есть данные, из которых уже изъяты все следы украденного.

Нет. Интеграция должна иметь общее основание, и такое основание — данные.

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

Ещё было замечено, что разные части одной и той же программы могут иметь свою собственную логику, и единственное, что их связывает — это модель. Но это так, к слову.

Далее была обнаружена ещё одна интересная вещь. Оказалось, что в настольных приложениях отрисовка рабочей области окна — задача весьма техническая, напоминающая «вещь в себе». А отработка реакции на стандартные раздражители (вроде меню и кнопок) больше похожа на административную, то есть на прямое отражение ТЗ. Должна кнопка копировать в буфер? Вызываем что-то вроде

pEditor->copy();

В Веб-приложениях картина та же, только в другом ракурсе. Есть часть, где мы воплощаем задумки дизайнера, а есть часть где мы воплощаем задумки клиента (или отчёт бизнес-аналитика, что с рабочего места программиста выглядит тем же самым).

Так вот, если отделить одно от другого, получается удобно. Отрисовку уникального (логику дизайнера) назвали Представление. А всё, что осталось — Контроллер.

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

Только что обрисованный способ мышления называется MVC (Model—View-Controller) и имеет вариации, которые тоже полезны.

Делим хозяйство с дизайнером

В фокусе PHP и профессиональная разработка.

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

А дальше начинаются детали, а в них, как говорят уже не только французы, — дьявол.

Дело в том, что решение отделить HTML от PHP заманчиво своей очевидностью. Но это рушит саму идею разделения. Смотрим исходную задачу:

<table>
<? $q db_query('SELECT * FROM products'); ?>
<? while ($row db_fetch($q)) {?>
<tr <?=$row['is_new']?'class="new"':''?>>
  <td><?=htmlspecialchars($row['title'])?></td>
  <td><?=number_format($row['price'], 2','' ')?></td>
</tr>
<? } ?>
</table>

И смотрим очевидное разделение:

PHP

$q db_query('SELECT * FROM products');

$list = array();
while ($row db_fetch($q)) {

  // Приводим поля к формату, пригодному для шаблона
  $row['is_new'] = $row['is_new'] ? 'class="new"' '';
  $row['title'] = htmlspecialchars($row['title']);
  $row['price'] = number_format($row['price'], 2','' ');

  // И набор к формату, пригодному для шаблонизатора
  $list[] = $row;
}

// Условно, вызов простейшего шаблонизатора.
// То есть встроенного. Тут мог быть и Smarty.
require 'template/products.php';

HTML

<table>
<? foreach ($list as $row) { ?>
<tr <?=$row['is_new']?>>
  <td><?=$row['title']?></td>
  <td><?=$row['price']?></td>
</tr>
<? } ?>
</table>

Но существует поверье, что любой программный код в HTML — к несчастью. Давайте тогда ещё вариант.

PHP

$q db_query('SELECT * FROM products');

$list = array();
while ($row db_fetch($q)) {

  // Приводим поля к формату, пригодному для шаблона
  $row['is_new'] = $row['is_new'] ? 'class="new"' '';
  $row['title'] = htmlspecialchars($row['title']);
  $row['price'] = number_format($row['price'], 2','' ');

  // И набор к формату, пригодному для шаблонизатора
  $list[] = $row;
}

// Условно, вызов шаблонизатора, изгоняющего PHP из HTML.
$html->set('list'$list);
exec_template('products');

HTML

<table>
<!-- begin_list -->
<tr {is_new}>
  <td>{title}</td>
  <td>{price}</td>
</tr>
<!-- end_list -->
</table>

Примечание. Синтаксис взят из реального шаблонизатора. HTML-комментарии используются, как управляющие конструкции. Их формат: <!-- begin_<имя переменной> -->.

Получилось разделение на дизайн и не-дизайн? Не совсем. Смотрим детали.

В PHP у нас есть «class="new"». Это HTML. Более того, это та самая логика дизайнера, о которой мы говорили выше, то есть часть представления (view).

Если дизайнер решит, что теперь строки должны выделяться не классом, а картинкой, то нам придётся а) корректировать код, б) думать, куда встроить URL картинки из дизайна — не в PHP же, верно?

А если наша программа поддерживает скины, и в одном из них нужен класс, а в другом картинка, а в третьем ещё большее непотребство, то приплыли.

Поэтому тут пора признать, что разделение на HTML и PHP при всей своей видимой простоте убило изначальную идею разделения представления и контроллера.

Вывод прост: логика, отвечающая за представление, должна находиться там же, где HTML:

<table>
<? foreach ($list as $row) { ?>
<tr <?=$row['is_new']?'class="new"':''?>>
  <td><?=$row['title']?></td>
  <td><?=$row['price']?></td>
</tr>
<? } ?>
</table>

А где htmlspecialchars и number_format? А это типовые операции. Если поле title хранит простые строки, то их всегда нужно будет экранировать перед выводом. То же самое с форматированием валюты.

Здесь может возникнуть вопрос как быть, если потребовалась нестандартная операция. Например, укоротить строку title. Эту задачу можно решить раскодированием строки — операцией — повторным кодированием. Да, это дополнительные расходы процессора, но:

  1. Если бы мы сохраняли значения до кодирования, это были бы дополнительные расходы памяти, причём по всей программе.
  2. Если бы мы отказались от обработки строк в контроллере, пришлось бы прописывать её каждый раз в дизайне. Это вроде и не грех против идеи, поскольку обработка относится к представлению, но это надоедает, забывается и с некоторой вероятностью ставит под удар безопасность (см. XSS).

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

Делим хозяйство с клиентом

Теперь вернёмся к задаче, решаемой программой. К бизнес-логике.

Что хотел клиент от нашей странички? Вывести список продуктов. Всё.

Если конспектировать это в терминах будущей программы, то переходное звено могло бы выглядеть так:

SELECT FROM products // Что надо
template() // Что с этим сделать

Или так:

template(SELECT FROM products)

Собственно это, в совокупности с определением таблицы products, и есть вся бизнес-логика.

Остальное делится на:

  • Представление (плод дизайнера).
  • Доработку среды программирования под конкретный случай.
  • Ритуалы, позволяющие сократить число наших ошибок.
  • Прочие ритуалы языка программирования.

С представлением мы уже разобрались.

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

Например, если мы пользуемся стандартной функцией mysql_query, нам необходимо сохранить дескриптор запроса в отдельной переменной, проверить её на предмет ошибок, обработать ошибку, если она есть, и дальше прогнать цикл по дескриптору.

$q db_query('SELECT * FROM products');

$list = array();
while ($row db_fetch($q)) {

  // Приводим поля к формату, пригодному для шаблона
  $row['title'] = htmlspecialchars($row['title']);
  $row['price'] = number_format($row['price'], 2','' ');
  // Не обязательно: $row['is_new'] = (bool)$row['is_new'];

  // И набор к формату, пригодному для шаблонизатора
  $list[] = $row;
}

// Вызов шаблонизатора
require 'template/products.php';

Желаемый код отмечен жёлтым. Остальное — махинации над тем, чтобы оно вообще работало. Всё это вместе — реализация бизнес-логики. А жёлтый код, в каком-то смысле, она сама и есть.

Голубым отмечено то, что не относится к текущей части задачи, но следует из структуры таблицы. А структура эта тоже является частью бизнес-логики. Тем не менее, тут мы видим не саму структуру, а только реализацию её следствий.

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

Тем не менее, «в лоб» эта задача не решается, поскольку есть несколько типов выходных данных:

  • Чистые. Если мы формируем выдачу в JSON или XML при помощи специальной библиотеки.
  • Форматированные и экранированные для HTML. Обычный случай.
  • Форматированные, но не экранированные. Если форма отдаёт результат в виде чистого текста, например при подготовке тела e-mail или SMS.

Это требует интеллектуальной модели (метамодели в терминах MDA), популярных реализаций которой я не встречал — только экспериментальные и приватные. В т. ч. сам попробовал сделать и теперь пользуюсь, оказалось удобно.

Некоторые ритуалы позволяют сократить число ошибок.

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

То же самое касается и этого примера на Java:

public static final int THE_ANSWER = 42;

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

Сравним с Ruby:

THE_ANSWER = 42

Здесь тоже есть указание на то, что это константа, а не переменная (первая буква — заглавная). Всё остальное просто подразумевается.

Прочими ритуалами можно назвать то, что сложилось исторически, но пользы не несёт.

Например, в PHP нельзя вписать регулярное выражение в текст программы. Его нужно вписывать в строковую константу, с учётом экранирования символов. Это ненужно, но необходимо.

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

Вот и всё на сейчас

Напоминаю, что моя задача не столько показывать конкретные рецепты, сколько обратить Ваше внимание на неочевидные, но важные вещи. Развилки для мышления. Знание — это способность различать больше, чем до его (знания) приобретения.

Это позволяет формировать собственный стиль, вместо слепого использования неудобных, но привычных схем.

Ещё это позволяет понимать коллег, воспитавших своё мышление на других приоритетах.


Этот выпуск Вы можете прокомментировать в Живом Журнале.

Задать вопрос

Вы можете задать мне любой вопрос. Все полученные мною вопросы могут быть опубликованы в рассылке. Если Вы желаете скрыть свои личные данные из вопроса — укажите это в тексте письма, поскольку в дальнейшем, письма будут публиковаться с полными подписями (без емейлов, разумеется).


В избранное