Хочу рассказать о применении шаблона Active Record для C# на практике. Такой класс реализует извлечение и запись структуры в базу данных. Бизнес логика выносится на следующие уровни абстракции, где с таким объектом можно работать уже как с обычной структурой.
Центральный случай, который я буду рассматривать для примера — это работа со справочником Country из базы данных, который часто читается, но очень редко меняется.
Использование active record объекта в коде бизнес логики выглядит вот так:
Country russia = Country.All[“Russia”];
Country не имеет публичного конструктора, и получение объектов возможно только через обращение к методу Dictionary<string, Country> All.
Теперь поподробнее о том, как устроен этот класс изнутри.
Конструктор записи
public readonly string name;
private Country(IDataRecord record)
{
name = record.GetString(0);
}
Благодаря приватности конструктора мы можем надеяться на корректное применение конструктора. И только внутри класса.
Например вот так:
private static Dictionary<string, Country> _all = null;
private static Dictionary<string, Country> LoadDictionary()
{
_all = new Dictionary<string, Country>();
IDataReader reader = DBWrapper.GetReader(sqlSelect);
try
{
while (reader.Read())
{
Country item = new Country(reader);
_all.Add(item.name, item);
}
}
finally { reader.Close(); }
return _all;
}
sqlSelect – приватная константа, для чтения всех записей с нужным для конструктора порядком полей.
DBWrapper – самописный класс, инкапсулирующий работу с базой данных. Благодаря ему на этом уровне нам приходится работать только с интерфейсами, без указания конкретной реализации.
Add – добавление новой записи в общий реестр, скрыта для лаконичности кода в статье.
Словарь All
Тут тоже ничего сложного:
public static Dictionary<string, Country> All
{ get {
return _all ?? LoadDictionary();
}}
В итоге мы имеем отложенную загрузку справочника по первому обращению.
Приватная переменная _all использует только в этом куске кода. К сожалению C# не позволяет ограничить ее применение меньше чем на весь класс. Остается опасность ее применения например в публичных методах. Что станет настоящей проблемой при работе в нескольких потоках.
Это первый вопрос, который я хочу обсудить: как сильнее ограничить видимость переменной _all?
Синхронизация многопоточности
Такой способ отложенной загрузки пока не пригоден для работы в многопоточности, поэтому я добавил класс LoadStatus.
private static readonly LoadStatus statusCountryList = new LoadStatus(“country”);
Название для статуса нужно для его идентификации в списке статусов всех справочников. Но об этом позже.
private static Dictionary<string, Country> _all = null;
public static Dictionary<string, Country> All
{ get {
if ( !statusCountryList.IsCompleted ) {
lock (statusCountryList) {
if ( !statusCountryList.IsCompleted ) {
statusCountryList.Start();
_all = new Dictionary<string, Country>();
IDataReader reader= DBWrapper.GetReader(sqlSelect);
try
{
while (reader.Read()) Add(new Country(reader))
statusCountryList.Finish(_all.Count);
}
catch Exception ex { statusCountryList.Error(ex); }
finally { reader.Close(); }
}}}
return _all;
}}
Много макарон, зато теперь у нас есть многопоточность и отчет о здоровье нашего справочника, в качестве бонуса от архитектуры.
LoadStatus прячет в себе синхронизацию и сбор данных о здоровье справочника.
Кроме того, через обнуление LoadStatus появляется возможность перегрузить справочник на лету.
Именно ради этой возможности я отказался от readonly для _all.
Class Generic
Решение получилось настолько удобным и изящным, что я использую его в десятках справочников в всех проектах. И возникает огромное желания превратить этот код в generic class.
Однако синтаксис C# не позволяет этого сделать.
Что вы думаете о этом решении?
Какие могут быть способы для превращения этого решения в generic?
Автор: BloodJohn