Текст книги "Дефрагментация мозга. Софтостроение изнутри"
Автор книги: Сергей Тарасов
Жанр: Программирование, Компьютеры
Возрастные ограничения: +12
сообщить о неприемлемом содержимом
Текущая страница: 5 (всего у книги 16 страниц) [доступный отрывок для чтения: 5 страниц]
ORM, или объектно-реляционный проектор
Сокрытие базы данных, или Как скрестить ежа с ужомУпомянув один из крупнейших столпов современного софтостроения – мир ООП, нельзя обойти вниманием и другой – мир реляционных баз данных. Я намеренно вставил прилагательное «реляционные» применительно ко всем основным СУБД[45]45
Система Управления Базами Данных.
[Закрыть], хотя ещё в 1970-х годах такое обобщение было бы неправомерным.
Тем не менее именно реляционным СУБД удалось в 1980-х годах освободить программистов от знания ненужных деталей организации физического хранения данных, отгородиться от них структурами логического уровня и стандартизованным языком SQL[46]46
Structured Query Language – язык структурированных запросов, также имеет название «сиквел», идущее от первой версии языка SEQUEL. Первый стандарт принят в 1986 г.
[Закрыть] для доступа к информации. Также оказалось, что большинство форматов данных, которыми оперируют программы, хорошо ложатся на модель двумерных таблиц и связей между ними. Эти два фактора предопределили успех реляционных СУБД, а в качестве поощрительной премии сообщество получило строгую математическую теорию в основании технологии.
В отличие от реляционного мира, ООП развивалось инженерами-практиками достаточно стихийно, исходя из потребностей программистского сообщества, и потому никакой строгой теории под собой не имело. Имевшие место попытки подвести таковую под ООП задним числом терпели неудачу. Наилучшего результата добились авторы объявленного стандартом UML[47]47
Unified Modeling Language – унифицированный язык моделирования, эклектичное объединение графических нотаций разных авторов, использовавшихся ими в софтостроительных проектах на основе ООП.
[Закрыть], который, однако, в основном до сих пор используется в качестве иллюстрирующих код картинок. Но лучше плохой стандарт, чем никакой.
И реляционная и объектная модели относятся к логическому уровню[48]48
В софтостроении, как инженерной дисциплине, различают концептуальный, логический и детальный (физический) уровни проектирования.
[Закрыть] проектирования программной системы. Они ортогональны и по сути представляют собой два взгляда на одну и ту же сущность. Это значит, что вы можете реализовать одну и ту же систему, оставаясь в рамках только одного реляционно-процедурного подхода или же следуя исключительно ООП.
На практике сложилась ситуация, когда программы пишутся в основном с использованием ООП, тогда как данные хранятся в реляционных БД. Не касаясь пока вопроса целесообразности такого скрещивания «ежа с ужом», примем ситуацию как данность, из которой следует необходимость отображения (проецирования) объектов на реляционные структуры и обратно.
Ввиду упомянутого отсутствия под ООП формальной теоретической базы эта задача, в общем случае нерешаемая, выполнима в случаях частных. Компонент программной системы, реализующий отображение, называется ORM[49]49
Object-Relational Mapping – объектно-реляционная проекция.
[Закрыть], или объектно-реляционный проектор – ОРП. Полноценный ORM может быть весьма нетривиальным компонентом, по сложности превосходящим остальную систему. Поэтому, хотя многие разработчики с успехом пользуются своими собственными частными реализациями, в отрасли за последние 10 лет появилось несколько широко используемых фреймворков, выполняющих в том числе и задачу проекции.
Обзор средств объектно-реляционной проекции выходит за рамки данной книги. Их и так достаточно в Сети, включая небольшой мой собственный [8], сделанный ещё в 2005 году, но не сильно устаревший. Поэтому последующие примеры будут в основном касаться фреймворка NHibernate.
В технологии отображения объектов на РСУБД есть очень важный момент, от понимания которого во многом зависит успех вашего проекта. Я не раз слышал мнение программистов, что для слоя домена[50]50
От англ. domain layer – слой объектов предметной области, часто называемых также бизнес-объектами.
[Закрыть] генерируемый проектором SQL код является аналогом результата трансляции языка высокого уровня в ассемблер целевого процессора. Это мнение не просто глубоко ошибочно, оно быстрыми темпами ведёт команду к созданию трудносопровождаемых систем с врождёнными и практически неисправимыми проблемами производительности.
Проще говоря, как только вы подумали о SQL как о некоем ассемблере по отношению к используемому языку ООП, вы сразу влипаете в очень нехорошую историю.
SQL – высокоуровневый декларативный специализированный язык четвёртого поколения, в отличие от того же Java или C#, по-прежнему относящихся к третьему поколению языков императивных. Единственный оператор SQL на три десятка строк, выполняющий нечто посложнее выборки по ключу, потребует для достижения того же результата в разы, если не на порядок, больше строк на C#.
Такая ситуация приводит разработчиков ORM к необходимости создавать собственный SQL-подобный язык для манипуляции объектами и уже его транслировать в сиквел[51]51
Жаргонное название SQL-запроса. – Примеч. ред.
[Закрыть] (см. HQL[52]52
Hibernate Query Language – SQL-подобный язык запросов, используемый в Hibernate/ Nhibernate.
[Закрыть]). Или использовать непосредственно SQL с динамическим преобразованием результата в коллекцию объектов.
В противном случае прикладной программист обречён на извлечение из БД и последующую обработку больших массивов данных непосредственно в своём приложении. Примерно так же обрабатывали табличные данные при отсутствии встроенного SQL разработчики на ранних версиях Clipper в конце 80-х годов. Там это называлось «навигационная обработка». Думаю, термин уместен и здесь.
В эпоху массового перехода с Clipper-подобных программ и файл-серверных технологий на клиент-серверные РСУБД многие приложения и их разработчики продолжали использовать навигационный подход. Приложения работали медленно, зачастую блокируя работу в многопользовательской среде. Потому что для эффективной работы с РСУБД нужно использовать подходы, ориентированные на обработку множеств на сервере, предполагающие наличие у разработчика умений работать с декларативными языками.
Тем не менее, получив в распоряжение ORM, программист зачастую возвращается к навигационным подходам обработки массивов данных вне РСУБД лишь с той разницей, что теперь этот массив, как хочется надеяться, является содержимым целой таблицы.
Почему? Недостаток знаний РСУБД пытаются возместить дополнительными уровнями абстракций. На деле же выходит обратное: уровни абстракции скрывают не детали слоя хранения объектов от программиста, а, наоборот, его некомпетентность в области баз данных от СУБД. До некоторого времени.
Несмотря на толстый слой абстракций, предоставляемый ORM типа Hibernate, заставить приложение эффективно работать с РСУБД без знаний соответствующих принципов ортогонального мира и языка SQL практически невозможно.
Но попытки продолжаются. Одни по-прежнему разрабатывают проекторы для своих внутренних нужд, зачастую очень лёгкие. Другие ищут упрощение и выход в noSQL[53]53
noSQL – Not Only SQL, обобщённое название СУБД, не базирующихся на реляционной модели, интерес к которым возрос в последние годы прежде всего из-за сложности горизонтального масштабирования в традиционных РСУБД.
[Закрыть]. Но в выигрыше пока остаются программисты и консультанты, обладающие «базоданными» компетенциями и зарабатывающие на тех, кто ими не обладает.
На софтостроительных презентациях часто рисуют красивые схемы по разделению слоёв представления, бизнес-логики и хранимых данных. Голубая мечта начинающего программиста – использовать только одну среду и язык для разработки всех слоёв и забыть про необходимость знаний реляционных СУБД, сведя их назначение к некоей «интеллектуальной файловой системе». Аббревиатура SQL вызывает негативные ассоциации, связанные с чем-то древним, не говоря уже про триггеры или хранимые процедуры. На горизонте появляются добрые люди, с книгами признанных гуру о домен-ориентированной разработке[54]54
Domain Driven Design – концепция разработки прикладных программ на основе домена – слоя объектов предметной области.
[Закрыть] под мышкой, заявляющие новичкам примерно следующее: «Ребята, реляционные СУБД – пережиток затянувшейся эпохи 30-летней давности. Сейчас всё строится на ООП. И есть чудесная штука – ORM. Начните использовать её и забудьте про тяжёлое наследие прошлого!»
Ребята принимают предложение. Дальше эволюция разработки системы примерно следующая:
• Вначале происходит выбор ORM-фреймворка для отображения. Уже на этом этапе выясняется, что с теорией и стандартами дело обстоит плохо. Впору насторожиться бы, но презентация, показывающая, как за 10 минут создать основу приложения типа записной книжки контактов, очаровывает. Решено!
• Начинаем реализовывать модель предметной области. Добавляем классы, свойства, связи. Генерируем структуру базы данных или подключаемся к существующей. Строим интерфейс управления объектами типа CRUD[55]55
Create-Retrieve-Update-Delete – базовый набор манипуляций бизнес-объектами: «создать», «выбрать», «модифицировать», «удалить».
[Закрыть]. Все достаточно просто и на первый взгляд кажется вполне сравнимым с манипуляциями над DataSet[56]56
DataSet – набор данных, в узком значении – двумерный динамический массив, заполняемый табличным результатом запроса к РСУБД.
[Закрыть] – тем, кто о них знает, конечно – ведь не все подозревают о существовании табличных форм жизни данных в приложении за пределами сеток отображения DBGrid.
Как только разработчики реализовали CRUD-логику, начинается основное действо. Использовать сиквел напрямую теперь затруднительно. Если не касаться стратегий отображения и проблем переносимости приложения между СУБД, по сути каждый SQL-запрос с соединениями, поднявшись в домен, сопровождается специфической проекцией табличного результата на созданный по этому случаю класс. По этой причине приходится использовать собственный язык запросов ORM. Нестандартный, без средств отладки и профилирования. Если он, язык, вообще имеется в данном ORM. Для поддерживающих соответствующую интеграцию среда. NET предоставляет возможность использовать LINQ[57]57
LINQ – Language Integrated Query, технология Microsoft на основе SQL-подобного встроенного языка для манипуляции объектами в. NET-приложениях.
[Закрыть], позволяющий отловить некоторые ошибки на стадии компиляции.
Сравните выразительность языка на простом примере, который я оставлю без комментариев:
SQL
SELECT *
FROM task_queue
WHERE
id_task IN (2, 3, 15)
AND id_task_origin = 10
NHibernate HQL
IList<TaskQueue> queues = session
CreateQuery("from TaskQueue where Task.Id in (2, 3, 15) and TaskOrigin.Id = 10")
List<TaskQueue>();
NHibernate без HQL с критериями
IList<TaskQueue> queues = session.CreateCriteria()
Add(Expression.In("Task.Id", someTasks.ToArray()))
Add(Expression.Eq("TaskOrigin.Id", 10))
List<TaskQueue>();
LINQ (NHibernate)
IList<TaskQueue> queues = session
Query<TaskQueue>()
Where(q => someTasks.Contains(q.Task.Id) &&
q. TaskOrigin.Id == 10).ToList();
Внезапно оказывается, что собственный язык запросов генерирует далеко не самый оптимальный SQL. Когда БД относительно небольшая, сотня тысяч записей в наиболее длинных таблицах, а запросы не слишком сложны, то даже неоптимальный сиквел во многих случаях не вызовет явных проблем. Пользователь немного подождёт.
Однако запросы типа «выбрать сотрудников, зарплата которых в течение последнего года не превышала среднюю за предыдущий год» уже вызывают проблемы на уровне встроенного языка. Тогда разработчики идут единственно возможным путём: выбираем коллекцию объектов и в циклах фильтруем и обсчитываем, вызывая методы связанных объектов. Или используем тот же LINQ над выбранным массивом. Количество промежуточных коротких SQL-запросов к СУБД при такой обработке коллекций может исчисляться десятками тысяч.
Триггер как идеальная концепция для NHibernateОбычно разработчикам баз данных я рекомендую избегать необоснованного использования триггеров. Потому что их сложнее программировать и отлаживать. Оставаясь скрытыми в потоке управления, они напрямую влияют на производительность и могут давать неожиданные побочные эффекты. Пользуйтесь декларативной ссылочной целостностью (DRI[58]58
От англ. Declarative Referential Integrity – декларативная ссылочная целостность, один из важнейших механизмов поддержания базы данных в непротиворечивом состоянии.
[Закрыть]) и хранимыми процедурами, пока возможно. А если ваш администратор баз данных склонен к параноидальным практикам «запрещено всё, что не разрешено», избегайте программировать на уровне СУБД, исключая критичные по производительности участки. Приходится говорить это с сожалением…
Однако стоит посмотреть на слой домена, живущего под управлением NHibernate, как становится ясно, что триггер в СУБД – это достаточно простая и хорошо документированная технология. В то время как NHibernate предлагает прикладному разработчику целый зоопарк триггероподобных решений.
Во-первых, имеется древний способ реализации классом домена интерфейса из пространства имён NHibernate.Classic. Например, IValidate. Вроде бы удобно: реализовал и делай проверки, генерируя исключения для отмены транзакции. Но вот незадача: при удалении объекта этот метод не срабатывает, нужно использовать другие подходы.
Во-вторых, после осознания авторами недостаточности IValidate и ILifeCycle была введена система прерываний (interceptors). Это уже больше, чем бесплатный хлеб на завтрак. Однако в обработчиках типа Save или FlushDirty в качестве аргументов используются массивы состояний объекта. То есть изменять сам объект в них напрямую нельзя: в общем случае это просто не срабатывает, но могут быть и побочные эффекты. Нужно, ни много ни мало, поискать индекс элемента в массиве имён свойств объекта, затем по найденному номеру изменить значение в другом массиве текущего состояния объекта. Что-то вроде такого кода:
Изменение свойств объекта в обработчике NHibernate
int index = Array.IndexOf(propertyNames, «PhoneNumber»);
if (index!= -1)
state[index] = "(123)456789";
Создавать новые, извлекать, изменять или удалять существующие объекты с последующим сохранением внутри обработчика совершенно не рекомендуется. Кроме собственно гонок (race condition), когда обработчик одного класса создаёт другой, а тот что-то делает с первым, могут быть и другие эффекты, включая бесконечную рекурсию. Шаг вправо, шаг влево – стреляют боевыми и без предупреждения. Неплохая иллюстрация к теме декларируемой безопасности языка C# или Java. Рекомендуемая практика обхода ловушек такого рода – запрограммировать собственную защищённую (thread safe) очередь, куда складывать все созданные или изменённые объекты, а в событиях BeforeCommit или AfterCommit эту очередь обрабатывать.
В-третьих, механизм прерываний также признан несовершенным, после чего был введён механизм событий (events), коим, начиная со второй версии, всем следует пользоваться.
Дело в том, что в сессии вы можете зарегистрировать только один класс, реализующий обработчики прерываний. И если у вас достаточно много разной обработки, то получается так называемый «волшебный класс», который реализует всё. Это неудобно, даже если использовать класс в качестве пустого фасада.
Теперь же вы можете зарегистрировать неограниченное количество классов-слушателей (listeners), ожидающих то или иное событие и соответствующим образом реагирующих на него. Один класс может реализовывать несколько обработчиков событий, будучи зарегистрированным для прослушивания нескольких их типов.
С точки зрения архитектуры это несомненный плюс, реализацию можно разбить на независимые классы. Однако для снижения побочных эффектов, прежде всего рекурсии и гонок, не сделано ничего. В том же SQL Server, напомню, рекурсия в триггерах отключена по умолчанию, поскольку трудно сходу придумать случай, когда она нужна. А в событиях NHibernate каждый сам себе вредитель. При этом однозначной методики и документации нет, нескольким десяткам типов событий в официальной документации отведено меньше страницы. Существует огромное количество записей в блогах, тиражирующих одни и те же конкретные примеры – аудит, прежде всего. Но однозначной выверенной практики нет, в каждом конкретном случае надо проводить тесты. Например, для манипуляции объектами рекомендуется создавать дочернюю сессию, что также не всегда избавляет от побочных эффектов.
В итоге имеем плохо документированную нестабильную систему, которая при отладке в разы труднее столь нелюбимых разработчиками триггеров баз данных. Тем не менее, если разработчик пишет код слоя домена, альтернатив практически нет. Генерация скелета кода по модели облегчает работу, но риски возникновения ловушек многопоточной обработки по-прежнему остаются на совести рядового программиста. А создать такие ситуации несложно. Поэтому надо признать, что использование обработчиков событий слоя домена должно быть рекомендовано еще в меньшей степени, чем использование триггеров слоя хранения данных.
ORM на софтостроительной площадкеНа короткое время судьба забросила меня в качестве консультанта в лоно одной софтостроительной фирмы, разработавшей и поддерживающей специализированную систему документооборота для управления жизненным циклом товаров. Система относительно небольшая по функционалу, а вот клиенты хоть и мало-
численные, но крупные, то есть способные упорно настаивать на своих требованиях, иногда противоречивых, аргументируя их соответствующим бюджетом.
В процессах взаимодействия фирм весьма отчётливо действуют физические законы всемирного тяготения. Небольшой планете-фирме, чтобы не упасть на большую, разбившись вдребезги, необходимо развить как минимум первую космическую скорость. В этом случае она будет стабильно вращаться вокруг большой в качестве спутника. Чтобы оторваться от поля тяготения большой планеты и начать самостоятельный полет требуется уже вторая космическая скорость.
В течение последних месяцев в фирме происходила попытка выйти на вторую космическую. Поскольку процесс, обеспечивающий первую космическую, был близок к тому, что называют экстремальным программированием, было принято решение продолжать в том же духе, назвав все это звонким словечком «скрам»[59]59
От англ. SCRUM – одна из популярных методик так называемой «гибкой разработки».
[Закрыть].
В принципе, основные элементы процесса имелись в наличии. Коллективное владение кодом, также известное, как личная безответственность при его написании, утренние «пионерские линейки» вместо чётких спецификаций, практически полное отсутствие документации, частые, до одного раза в 1–2 недели, релизы и связанный с этим нескончаемый аврал, работа в тесном и жарком помещении общего зала. Последнее вызвано объективными причинами: для поддержания жизнедеятельности муравейника требуется всё больше работяг.
Вы резонно спросите: «А где рефакторинг?» Системе на тот момент исполнилось уже 2 года. Рефакторинг проводился раньше, но, в связи с тем, что его стоимость, прежде всего по требуемым срокам поставки, возрастала, количество реструктурируемого кода линейно уменьшалось. Это создало положительную обратную связь: реструктуризация становилась всё дороже.
На столах у некоторых программистов лежали книжки по рефакторингу от раскрученных апостолов веры в эволюционный дизайн. Но в связи с занятостью чётко выполнялась только первая часть моего любимого правила «Настоящий исследователь должен поменьше читать, чтобы иметь возможность больше думать головой». На вторую часть, к сожалению, у разработчиков не было времени.
Приведу пример подсистемы, для которой рефакторинг уже стал дороже полной переделки. Имелся модуль экспорта документов в форматы, пригодные для импорта системами клиентов уровня их внутренних АСУП[60]60
АСУП – Автоматизированная Система Управления Предприятием, англ. ERP – Enterprise Resources Planning.
[Закрыть]. Коллективная ответственность привела к выбору написания императивного кода в размере 20 тысяч строк вместо разработки нескольких шаблонов XSLT[61]61
XSLT – англ. eXtensible Stylesheet Language Transformations – декларативный язык преобразования XML-документов. Стандартизован консорциумом W3C.
[Закрыть] из нескольких сотен строк. Почему? Во-первых, опасались потери производительности, а во-вторых, не имели достаточной компетенции в XSL. Цикломатическая сложность[62]62
Одна из метрик, обеспечивающая количественную оценку логической сложности программы.
[Закрыть] кода в отдельных методах превышала запредельное число 50 при рекомендованном пороге в 10–20. Глубина вложенности вызовов также была больше 10, при цикличности их части: this с верхнего уровня передаётся в качестве параметра, и где-то глубоко внизу дёргают этот this за какой-то метод. Объектно-ориентированная тарелка со спагетти.
О производительности следует сказать отдельно. Загрузка достаточно сложного документа перед его экспортом занимала порядка 30 секунд. Потому что было принято идеологическое решение «ни строчки SQL», несмотря на необходимость поддержи только одной РСУБД. Вся система работала через слой доступа[63]63
DAL – Database Access Layer, слой абстракций для доступа к данным.
[Закрыть] под управлением NHibernate. Это был именно DAL, а не домен, так как парни не использовали всю мощь NHibernate, ограничиваясь отображением, и накручивали сверху слои бизнес-логики. При загрузке сложного документа с проверками подсистемы безопасности было насчитано порядка 20 тысяч (!) коротких SQL-запросов.
Почему именно такое решение? Было сказано некоторое количество красивых слов о чистоте объектной концепции. Это стандартный ответ. Тогда я просто предложил использовать в одном узком месте вместо сотен строк вложенных циклов сишарп-кода относительно короткий рекурсивный запрос из 40 строк сиквел-кода. Вид этого кода вначале вызвал у парней лёгкий ступор, а после совещания через сутки было принято решение отказаться от него. Но надо отдать должное: ребята честно признались, что после моего ухода никто не сможет этот сиквел-код поддерживать и модифицировать. Вот, собственно, и главная причина первоначального выбора. Красноречивое подтверждение тезиса о том, что слой объектной абстракции доступа к реляционной СУБД в большинстве случаев скрывает не базу данных от приложения, а некомпетентность разработчиков приложения в области баз данных.
Долго и неинтересно рассказывать, каким образом система с тремя сотнями таблиц умудрилась обрасти миллионом строк C#-кода, для поддержки которого требуются всё новые разработчики. Клиенты проявляли недовольство, так как сроки срывались, а задержка медленно, но неуклонно росла. Два совещания, на которых генеральный директор, милейший мсьё, пытался реанимировать дух прежнего стартапа[64]64
От англ. startup – новообразованное инновационное малое предприятие, термин чаще всего употребляется в наукоемких областях деятельности.
[Закрыть], искренне желая, чтобы все, от стажера до его ближайших замов, смело поднимали и доносили проблемы, по всей видимости, прошли впустую. Состояние постоянного стресса передалось даже мне, формально не несущему ответственности за результат. Но ведь ещё есть риски социального капитала, репутации, которая долго зарабатывается и очень быстро может обесцениться. Пришлось даже посидеть с мыслями об оптимальных путях выхода из миссии[65]65
Миссия – временная работа консультанта на площадке заказчика.
[Закрыть] за бокалом бургундского.
История закончилась типично. С большими усилиями систему дотянули до сдачи в эксплуатацию, хронические проблемы вследствие архитектурных просчётов названы особенностями, после чего развитие её было заморожено, а команда частично распущена, частично переориентирована на другие проекты.
Внимание! Это не конец книги.
Если начало книги вам понравилось, то полную версию можно приобрести у нашего партнёра - распространителя легального контента. Поддержите автора!Правообладателям!
Данное произведение размещено по согласованию с ООО "ЛитРес" (20% исходного текста). Если размещение книги нарушает чьи-либо права, то сообщите об этом.Читателям!
Оплатили, но не знаете что делать дальше?