- PVSM.RU - https://www.pvsm.ru -
В этой статье я попытаюсь поделиться своим опытом в проектировании пользовательской бизнес-логики. Это явно не претендует на полноценный ликбез, т.к. я всего лишь вспоминаю то, через что прошёл лично я, какие ошибки я допустил, и как мне их удалось (или не удалось) исправить в будущем. Наверняка, опытные системные архитекторы уже все проходили и знают, однако надеюсь, что некоторые советы таки будут полезны.
Мы использовали (и используем) клиентскую часть на WPF/Silverlight, WCF сервисы и СУБД Oracle, Postrges, MsSQL. Код написан по MVVM, использована Prism для модульности и навигации. Не могу точно сказать, какие из тезисов подойдут для других платформ и языков.
Так сложилось, что в какой-то момент мне, совершенно заурядному на то время программисту, выпала задача проектировать большую и сложную систему учета данных с большим количеством условий, переходов, этапов работы. Система была предназначена для ввода данных о жителях, регулярных заседаний по выдаче им пропусков и отказов, продления пропусков, прекращения их деятельности, штрафов, и многих других мелочей. Сейчас ядро системы уже большей частью переписано, говнокод исчез, использованы новые и последние технологии, платформы.
Итак, поехали.
Получилось, что в двух разных сборках (говоря об интерфейсе – в двух разных модулях системы, которые вызываются в разные моменты времени) нужно было выбирать из БД похожий список сущностей, скажем методом GetComissions()
. Программист Петя, увидев у программиста Васи уже написанный на сервисе метод GetComissions()
(а у нас тогда был один сервис с одним эндпоинтом), недолго думая, взял и его использовал. Все бы хорошо, только через пару недель заказчику понадобилось в одном из мест кроме комиссий показывать еще и их статистику, статусы, решения, и много всего другого. Как результат, второй модуль сразу же стал падать с ошибкой.
Увы, но таких случаев было явно больше одного, и в последствии, понадобилось очень много усилий по разнесению методов сервиса на разные сервисы-эндпоинты, или хотя бы на отдельные партиал классы (в случае с одним сервисом).
Вывод: два проекта могут использовать одну и ту же функцию на сервисе тогда и только тогда, когда заранее заведомо известно, что эти проекты всегда будут работать с этой функцией одинаково. В таком случае метод должен выноситься в общий класс, который и подключается к каждому проекту.
Во всех остальных случаях принцип написания кода на сервере должен следовать чему-то похожему на Single Responsibility principle [1] (один из принципов SOLID [2]).
По моему опыту, абсолютно всегда решение проще, если все датаконтракты (в случае с RiaServices – энтити классы) остаются всегда девственно чистыми, и совпадают 1к1 со схемой БД. Всё может начаться с безобидного кода конкатенации ФИО работника в одну переменную, а заканчивается это огромными вычислениями каких-то непонятных коэффициентов (нужных, как правило, только в одном месте). В итоге датаконтракты стали на 80% состоять либо из методов (да, если в геттере свойства написан какой-то код, это тоже я считаю методом), либо из полей других таблиц, которые нужны были кому-то одному в какой-то момент. Всегда лучше делать наследники или обёртки на сервисе или на клиенте (в зависимости от задачи), которые будут использоваться сугубо для целей этого модуля. Про проблемы с ними – в следующем пункте.
Увы, но идеального решения мы так и не нашли. Рассмотрим несколько случаев:
а) Модули работают с одним клиентским референсом на сервис. Классы на сервере разбиты на партиал классы от одного сервиса.
Минусы:
copy local
для референсов проекта с клиентским сервисом (при работе с MEF и необходимости иметь одну сущность какого-то класса в сервис локаторе).Плюсы:
б) Каждый модуль самостоятельно делает клиентский референс на сервис.
Минусы:
Мы выбираем чаще всего вариант а), т.к. передача объектов между модулями довольно полезная штука. Вернемся теперь к заголовку: именно в варианте а) разница между наследниками и партиалами становится очень заметной.
Допустим, у нас в БД есть таблички Person
и Document
. По канонам, в датаконтрактах у класса Person
будет List<Document>
, т.к. у Document
есть вторичный ключ на Person
. Одной из задач может быть создание на клиенте сущностей Person
и всех его Documents
сразу, и последующее сохранение этого всего добра в БД. В этом нет никаких проблем, если использовать класс Person
и его поле List<Document>
.
Однако, если в таком случае, нам потребуется помимо создания отображать и информационные поля из других таблиц (статусы, количества других сущностей, связанных вторичными ключами с Person
или Document
), и мы для этого сделаем наследника PersonEx
и DocumentEx
, то у PersonEx
уже не будет поля List<DocumentEx>
! В таком случае приходится писать кучу обёрток и переприсваиваний, что усложняет код – ведь в настоящей задаче уровней вложений может быть не два, а гораздо больше. В таких случаях нам помогут партиал-классы, однако нужно бдить, чтобы случайно эти же поля не заполнил кто-то другой в другом месте, т.к. партиалы видны везде.
Под этими колонками я имею, прежде всего, идентификаторы. Связывание с бизнес логикой означает сортировки, группировки, использования идентификаторов в каких-то функциях, отображение их в интерфейсе, или еще хуже – функционал их редактирования напрямую.
Наверняка, это холиварная тема – но на моей практике еще не было случая, когда заказчик требовал бы какую-то сортировку, и для ее осуществления не было бы подходящего поля, заполняемого отдельно (руками или триггерами в БД – уже неважно).
При наличии связи идентификаторов с бизнес-логикой любой переход на другую схему БД, или на другую СУБД в принципе, или любое масштабирование системы на несколько БД с последующей репликацией повлечёт за собой переписывание логики.
Hint: очень удобно на каждую табличку в БД создавать две колонки date_insert
и date_update
, в которые триггерами на те же инсерт-апдейт заполнять время этих операций.
Речь идет, конечно же, о создании сущностей бизнес-логики, а не о логгировании каких-либо действий. Я лично сталкивался с сопровождением системы, в которой запись о пропуске появлялась сразу же после печати его на принтере. Это была большая головная боль, т.к.:
Идеальный вариант был сделан чуть позже – появилась кнопка «сохранить пропуск в БД», а первая кнопка осталась простым предпросмотром с возможностью печати бесконечное кол-во раз.
К тому же, предварительное сохранение данных в БД не кнопкой «сохранить» чревато очень быстрым засорением базы пустыми (лишними) записями, которые кто-то потом явно забудет (или не сможет) удалить.
Рассмотрим для примера случай на рисунке.
Допустим, мы имеем дело с поставками какой-то нумерованной сущности в больших партиях в разные регионы (в данном случае — бланк). У него есть номер и серия, печатная компания печатает ParentDiapason
, а потом подрядчик делит его на ChildDiapason
-ы и распределяет по регионам.
Однако такая схема еще далека до идеала, т.к. в БД сможет храниться бланк с серией ПП и номером 245 в чайлд диапазоне с сериями от АА до КК и номерами от 300 до 400. Кроме этого, поле quantity
наверняка зависит от границ диапазонов, поэтому БД должна запрещать наличие записи, у которой quantity
будет неравна результату некой функции от (startSeries, endSeries, startNumber, endNumber)
. Таким образом, мы приходим к списку триггеров:
Только после включения этих констреинтов с такой схемой можно работать в реальной системе.
Hint: при работе с MsSQL Server 2012 можно перенести класс-проверяльщик правильности данных в отдельную сборку, и ее использовать как на веб-сервере, так и в триггерах БД.
Наверняка прозвучит по-капитански совет не иметь в коде никаких цифр, а выносить их в константы. Этот пункт немножко развивает этот совет, предлагая ипользовать удобное хранилище статусов сущностей:
public class Users
{
public class Status : StatusBase<Status>
{
/// <summary>
/// Активен (может логиниться)
/// </summary>
[StringStatus("Активный")]
public const int Active = 1;
/// <summary>
/// Неактивен (не может логиниться)
/// </summary>
[StringStatus("Неактивный")]
public const int Inactive = 0;
}
}
public class StatusBase<T> where T : class
{
private static Dictionary<int, string> statusesDictionary;
public static string GetStringStatus(int key)
{
if (statusesDictionary == null)
{
CreateStatusDictionary();
}
string result = statusesDictionary.ContainsKey(key) ? statusesDictionary[key] : "Неизвестный статус";
return result;
}
private static void CreateStatusDictionary()
{
statusesDictionary = new Dictionary<int, string>();
FieldInfo[] fields = typeof(T).GetFields();
foreach (FieldInfo field in fields)
{
string TextStatus = ((StringStatusAttribute)field.GetCustomAttributes(typeof(StringStatusAttribute), false)[0]). TextStatus;
int statusKey = (int)field.GetValue(null);
statusesDictionary.Add(statusKey, TextStatus);
}
}
}
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string result = Data.Statuses.Users.Status.GetStringStatus((int)value);
return result;
}
Таким образом, и значения статусов, и их текстовые объяснения хранятся в одном месте, и легко меняются при необходимости.
Ну вот и всё. Буду рад, если эти советы кому-то принесут пользу.
Автор: fuazi
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/35877
Ссылки в тексте:
[1] Single Responsibility principle: http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%BD%D1%86%D0%B8%D0%BF_%D0%B5%D0%B4%D0%B8%D0%BD%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D0%B9_%D0%BE%D0%B1%D1%8F%D0%B7%D0%B0%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8
[2] SOLID: http://ru.wikipedia.org/wiki/SOLID_(%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)
[3] Источник: http://habrahabr.ru/post/182132/
Нажмите здесь для печати.