Работающий программист

Приступаем к работе с Oak: проверка данных

Тэд Ньюард

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

Но сначала…

Комментарии

Если вы вернетесь и посмотрите на систему в том виде, в каком мы ее оставили в прошлый раз, то, попытавшись добавить комментарий, вы получите другую из тех полезных ошибок, которая теперь уведомляет о том, что «Blog.Controllers.Blog does not contain a definition for 'AddComment'» (Blog.Controllers.Blog не содержит определение для AddComment). В противоположность тому, что могло бы произойти в системе со статической типизацией (где отсутствие этого метода привело бы к ошибке компиляции перед циклом «компиляция-развертывание-выполнение»), в динамической системе эти ошибки не проявятся, пока не будет реального обращения к такому методу. Некоторые сторонники динамических языков заявляют, что это неотъемлемая часть таких языков и, конечно же, отсутствие необходимости заботиться о том, чтобы в рамках системы все было согласовано, может оказаться большим преимуществом, когда ум захвачен какой-то идеей и вам нужно просто выдать ее на гора. Но большинство знакомых мне разработчиков на Ruby-on-Rails, занимавшихся каким-либо проектом, крупнее типичного приложения — списка Todo, первыми согласятся, что в приложении, написанном на динамическом языке, крайне важны всеобъемлющие тесты, чтобы поддерживать качество проекта на высоком уровне и сохранить нормальное психическое состояние разработчика. Так что тестирование должно быть частью любой серьезной работы с Oak.

К сожалению, как только я начинаю говорить о тестировании, меня так и подмывает подробно обсудить модульные тесты, тесты поведения, интеграционные тесты, TDD (Test-Driven Development) и прочее, что запросто могло бы занять половину годовых номеров журнала. Поэтому я не хочу открывать здесь этот ящик Пандоры. Каковы бы ни были ваши предпочтения в тестировании или его методологии, достаточно сказать, что какой-то вид тестирования в Oak-приложении обязателен (как и в любом приложении, но в средах с динамической типизацией потребность в тестировании гораздо выше).

А тем временем, метода AddComment так и нет.

Продолжаем комментировать

В данном конкретном случае, когда пользователь вводит комментарий в представление, оно выдает команду POST HomeController-методу Comments, который выглядит так:

[HttpPost]
public ActionResult Comments(dynamic @params)
{
  dynamic blog = blogs.Single(@params.BlogId);
  blog.AddComment(@params);
  return RedirectToAction("Index");
}

Как видите, контроллер сначала получает идентификатор блога, передаваемый в параметре BlogId из формы, затем использует его для поиска соответствующей записи в блоге через метод Single в DynamicRepository и, наконец, вызывает blog.AddComment, чтобы сохранить комментарий. (И вновь, просто чтобы донести до вас все за и против: этот код у нас уже был к концу второй статьи в серии, и я теперь столкнулся с тем, что метода AddComment пока нет.)

Определение этого метода довольно прямолинейное; добавьте его в класс Blog:

void AddComment(dynamic comment)
{
  // Игнорируем добавление, если тело пустое
  if (string.IsNullOrEmpty(comment.Body)) return;
  // К любому динамическому свойству этого экземпляра можно
  // обращаться через свойство "_"
  var commentToSave = _.NewComment(comment);
  comments.Insert(commentToSave);
}

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

Заметьте, что динамическая природа системы позволяет крайне лаконично писать код. Параметры формы захватываются в именованном наборе в @params, поступающем в контроллер, и передаются без распаковки прямо в AddComment, который в свою очередь передает их в NewComment; тот конструирует из них динамический объект, и он просто вставляется в комментарии DynamicRepository. Откуда берутся имена свойств объекта? Из HTML-формы, которая является источником всего этого.

Классно, а? Так и чувствуешь, что плавишься от лени.

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

Проверьте меня

Однако у этой системы есть крупный недостаток (во всяком случае, согласно требованиям пользователей): две записи в блоге могут иметь совершенно одинаковые заголовки, а это не есть хорошо. (Читатели могут растеряться, не понимая, какую статью читать — первую с заголовком «LOL Kittehs» или вторую с заголовком «LOL Kittehs».) Так что нужен способ принудительного введения какого-то рода уникальности в коде, чтобы пользователи случайно не помещали в блог записи под одинаковыми заголовками.

В традиционной веб-инфраструктуре это было бы задачей из двух частей. Сначала в объекте модели (Blog) нужно было бы определить метод типа «спроси меня, правилен ли я», который я назову IsValid (поскольку ничего более оригинального мне в голову не приходит), а затем указать определение этой проверки в объекте, которое я назову Validates. И, судя по тому, как работал метод Associates, помогая определить связь между объектами Blog и Comment (подразумевается, что это предопределенное имя, которое Oak знает, как искать), метод Validates работает так же: если в объекте определен метод Validates, Oak вызовет уже определенный в объекте метод IsValid, а тот в свою очередь будет искать метод Validates и спрашивать у него обо всех условиях, при которых этот объект является правильным.

В коде это выглядит так:

IEnumerable<dynamic> Validates()
{
  // Определяем связь. Другие примеры проверок см. в вики Oak.
  yield return new Uniqueness("Name", blogs);
}

И вновь вы видите использование потока объектов, которые описывают требования проверки, возвращаемые как IEnumerable<dynamic> в поток, генерируемый через применение C#-механизма yield return. Как и в случае с классом Schema в прошлый раз, способ его расширения — простое добавление дополнительных элементов, которые возвращаются через yield return:

IEnumerable<dynamic> Validates()
{
  // Определяем связь. Другие примеры проверок см. в вики Oak.
  yield return new Uniqueness("Name", blogs);
  yield return new Length("Name") { Minimum=10, Maximum=199 };
}

В Oak Wiki определены полный список и использование объектов проверки (validation objects), но некоторые из наиболее значимых перечислены ниже.

  • Presence Поле не является не обязательным и должно присутствовать в объекте.
  • Acceptance Поле в объекте должно содержать конкретное значение, например поле TypedOutAcceptance объекта LegalDocument, в которое пользователь вводит строку «I Accept», указывая, что он принимает юридические ограничения.
  • Exclusion Поле не может включать определенные значения.
  • Inclusion Поле должно содержать одно из набора определенных значений.
  • Format Проверка по универсальному регулярному выражению (с применением класса Regex из Microsoft .NET Framework).
  • Numericality Позволяет делать с числовыми значения почти что угодно, в том числе помечать поле как только целое, больше чем, меньше чем или просто проверять на четное или нечетное значение.
  • Conditional Всеохватывающий «спасательный люк» для любой проверки: поле должно удовлетворять условию, описанному с помощью лямбда-функции.

Последний объект, Conditional, на самом деле сам по себе является не типом проверки, а функциональностью, присутствующей в большинстве других (если не всех) типов проверки, и поэтому заслуживает чуть более подробного пояснения. Вообразите объект Order для заказов в традиционной системе электронной коммерции. В таких системах номер кредитной карты нужен лишь тогда, когда пользователь хочет оплатить покупку кредитной картой. Аналогично адрес доставки требуется, только если пользователь приобрел товар, не предоставляемый для скачивания в электронном виде. Эти два требования четко выражаются с помощью двух условных проверок, как показано на рис. 1.

Рис. 1. Условная проверка

public class Order : DynamicModel
{
  public Order()
  {
  }
  public IEnumerable<dynamic> Validates()
  {
    yield return new Presence("CardNumber") {
      If = d => d.PaidWithCard()
    };
    yield return new Presence("Address") {
      Unless = d => d.IsDigitalPurchase()
    };
  }
  public bool PaidWithCard()
  {
    // Можно было бы заменить на This().PaymentType
    return _.PaymentType == "Card";
  }
  public bool IsDigitalPurchase()
  {
    // Можно было бы заменить на This().ItemType
    return _.ItemType == "Digital";
  }
}

Каждый из объектов Conditional использует свойство объекта Presence (наряду с лямбдой, которая возвращает значение true или false), чтобы указать, успешна ли проверка Presence. В первом случае Presence возвращает true (проверка пройдена), если локальный метод d.PaidWithCard возвращает true при условии, что поле PaymentType равно значению «Card». Во втором случае Presence возвращает true, если только isDigitalPurchase не возвращает true, подразумевая, что это товар в электронном виде и адрес не требуется.

Все это готово к использованию с любым объектом, производным от Oak DynamicModel, и, как отмечалось в прошлой статье (msdn.microsoft.com/magazine/dn519929) и во введении к этой, такому объекту не нужно явно определять поля, на которые ссылаются эти проверки. Кстати, если этих проверок окажется недостаточно для конкретной задачи, то все они определены в файле Validations.cs в папке Oak самого проекта. При желании не так уж трудно определить новую проверку: просто наследуйте от Oak.Validation и определяйте, как минимум, метод Validate, возвращающий true или false. Например, проверка Exclusion выглядит так:

public class Exclusion : Validation
{
  public Exclusion(string property)
    : base(property)
  {
  }
  public dynamic[] In { get; set; }
  public bool Validate(dynamic entity)
  {
    return !In.Contains(PropertyValueIn(entity) as object);
  }
}

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

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

Части Oak

Иногда части какой-то библиотеки выглядят по-настоящему изумительными, но возникают препятствия на пути к адаптации всей библиотеки, и хотелось бы выдернуть ее небольшую часть, чтобы задействовать в своем проекте. Хотя обычно лучше использовать Oak в целом, этот проект все же допускает выдергивание из него каких-то фрагментов (например, относящихся к динамической поддержке баз данных или только объектов, — Gemini, о которой я рассказывал в номера за август 2013 года, см. msdn.microsoft.com/magazine/dn342877) и автономное их применение без остальной системы. На странице Oak GitHub по этому поводу (bit.ly/1cjGuou) представлены NuGet-пакеты для каждой из автономных частей Oak, который я воспроизвожу здесь для вашего удобства (на момент написания этой статьи).

  • install-package oak Это полный набор Oak, который включает средства связывания MVC-моделей, генерацию схем, Oak DynamicModel и вспомогательные классы, модифицированную версию Massive (DynamicRepository) и Gemini — базовую динамическую конструкцию Oak.
  • install-package oak-json Это часть Oak, относящаяся к сериализации JSON (может быть задействована в REST API).
  • install-package cambium Это часть Oak, из которой исключены специфичные для MVC компоненты и генерация схем. Cambium включает Oak DynamicDb, DynamicModel, модифицированную версию Massive (DynamicRepository) и Gemini.
  • install-package seed Это генерация схем в Oak. Данный NuGet-пакет также включает модифицированную версию Massive (используемую для вставки образцов данных). В нем нет ни средств связывания MVC-моделей, ни Oak DynamicModel, ни вспомогательных классов.
  • install-package gemini Этот пакет установит только базовую динамическую конструкцию, лежащую в основе всей «динамики» Oak.

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

Преимущества, издержки и проблемы

Как можно было логически заключить из четырех статей этой серии, определенные преимущества в возможности просто «импровизировать по ходу дела» и работать с более динамически типизированной системой есть. Несомненно, в такой системе поднимут свои уродливые головы издержки и проблемы (особенно для новичков в программировании и тех, кто не привык писать тесты), но даже те, кто являются самыми ярыми фанатиками статической типизации, смогут извлечь из такой системы, как Oak, некоторые ценные идеи. Еще важнее, что Oak может оказаться невероятно ценным инструментом для создания прототипов на ранних стадиях разработки каких-либо систем, когда объектная модель еще не устоялась и часто изменяется. А самое главное в том, что благодаря нижележащей платформе Oak (т. е. .NET) вполне осуществимо создание MVC-приложения в Oak на ранних стадиях с последующей постепенной заменой ее частей на более статически типизированные (а значит, и проверяемые компилятором) по мере того, как детали приложения обретают более четкие контуры.

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

Удачи в кодировании!


Тэд Ньюард (Ted Neward) — глава компании Neward & Associates LLC. Автор и соавтор многочисленных книг, в том числе «Professional F# 2.0» (Wrox, 2010), более сотни статей, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области F#. С ним можно связаться по адресу ted@tedneward.com, если вы заинтересованы в сотрудничестве. Также читайте его блог blogs.tedneward.com и заметки в twitter.com/tedneward.

Выражаю благодарность за рецензирование статьи эксперту Амиру Раджану (Amir Rajan) (автор проекта Oak).