понедельник, 14 декабря 2015 г.

Time scope в Ninject

Постановка задачи

Есть некий data-provider, собирающий информацию из разных источников (локальная сеть, файловая система, ресурсы интернета) и предоставляющий программе некоторую сводную информацию. При этом, сбор и подготовка информации занимает 1-5 секунд, а обращение к провайдеру происходит несколько раз в секунду. Однако, исходная информация обновляется достаточно медленно и небольшое устаревание (в пределах нескольких часов) вполне допустимо. Очевидное решение задачи - это кэш. Кэш - это хорошо, это правильно, но хотелось бы так, что бы попроще - 1 строчка кода.. хм, ну максимум две..

Ninject решение

И тут я вспомнил, что мой провайдер лежит в биндингах Ninject-а. Эврика! Достаточно добавить к биндингу "волшебную"строчку .InSingletonScope() и будет "счастье". При запуске программы собираются данные и оседают в синглтон-объекте до конца жизни программы. В момент следующего запуска, данные снова собираются свежие. И, все бы хорошо, если бы не один нюанс - провайдер захотелось поместить в службу на сервере, которая не перезапускается по пол года. Ок. Хочется тот же синглтон, но так, что бы он все-таки пересоздавался.. иногда, ну, например, раз в два часа. Для реализации такого функционала в Ninject есть понятие custom scope - метод .InScope(...). В качестве параметра данный метод принимает функцию, которая, в свою очередь, должна вернуть произвольный объект. Именно на этот объект и будет ориентироваться система - если он изменится (будет отличаться от того, который функция вернула в прошлый раз), то наш провайдер будет пересоздан. Осталось понять, где взять такой объект, который будет пересоздаваться через нужный временной интервал. Создавать громоздкую систему со статическими hash-таблицами совершенно не хотелось (тогда уж лучше сделать кэш). И тут пришла в голову идея - что если в качестве такого объекта использовать специальное число, полученное следующим способом:
var diff = DateTime.Now.Ticks / time.Ticks;
Где time - это нужный интервал времени (TimeSpan). И, все бы хорошо, но! Ninject, как выяснилось путем простого чтения исходных кодов (и как это я сразу не догадался туда заглянуть), сравнивает объект по ссылке, т. е. с помощью метода object.ReferenceEquals(...), а не "по-человечески" с помощью object.Equals(...), как я наивно ожидал. В итоге, простые типы в качестве специального объекта не работают.

"Магия" строк

Вот тут-то я и решил использовать "магию". Итак, мне нужен объект, который будет правильно сравниваться по ReferenceEquals и, при этом, будет вычисляться "на лету", что бы не хранить его ни в какой статике и не думать о накапливаемом "мусоре". Хм. Вообще-то такое сделать нельзя - без статического хранилища эта задача не решается. Но! можно использовать строки. Дело в том, что внутри .Net в строках уже заложено статическое хранилище и оно поддерживается системой. А, значит, нам достаточно этим воспользоваться. Добавим еще одну строчку кода и обернем это в удобную функцию, которую можно будет использовать многократно:
object GetTimeScope( TimeSpan time )
{
   var diff = DateTime.Now.Ticks / time.Ticks;
   return string.Intern( diff.ToString() );
}
Главная "магия" тут в слове Intern - именно оно и даст нам правильную работу ReferenceEquals. Ну а использовать этот метод совсем просто:
kernel.Bind<TestDataObject>().ToSelf().InScope( c => GetTimeScope( TimeSpan.FromHours( 2 ) ) );
Удачного программирования.

четверг, 17 сентября 2015 г.

Исправить или переписать. Ловушки нашего подсознания

Солнышко уже скрылось, а я все "пилю" один и тот же кусок кода. А хотел-то всего лишь исправить мелкую ошибку. Однако, исправив одно, обнаружил, что отвалилось другое, а там, в свою очередь, уже и так куча "костылей-распорок", так как код этот правится не в первый раз. Вот в такие моменты и начинаешь думать "а не переписать ли весь этот блок с нуля - так, что бы без распорок, без костылей...". На что пессимист в глубине сознания начинает возражать "А сколько на это времени уйдет? Код-то не маленький. А, может, проще-таки еще одну заплаточку?". Так как же поступить? Переписывать или править дальше? Как правильно оценить время? На самом деле, оценивать объем кода по базовым факторам мы все умеем - смотрим на количество строк, прикидываем сложность, сопоставляем с тем, сколько обычно времени уходит на такой код. Все это понятно, но есть факторы, сильно искажающие нашу оценку - факторы, действующие на подсознательном уровне и приводящие к неадекватным решениям.

Свой или чужой код

Пожалуй, это один из сильнейших факторов, искажающих нашу оценку временных затрат. И, если вы в сомнениях - переписать или добавить заплатку, то подавляющем большинстве случаев вы примете правильное решение, следуя простой формуле:
Свой код переписывайте смело.
Чужой лучше не трогать - сделайте заплатку
Дело в том, что в процессе написания кода вы так или иначе натыкаетесь на множество непредвиденных моментов, сложностей, которые вы не предусмотрели заранее. И это нормально - опытный программист всегда прибавляет к изначальной оценке небольшой резерв именно на эти сложности, при чем делает он это автоматически. При этом, если данную задачу вы уже решали, то все "подводные камни" вами уже пройдены. Кроме того, вы уже продумали весь алгоритм целиком и, по сути, вам останется продумать только те структурные моменты, из которых задача переделывается. Однако, при оценки времени, в нашем подсознании возникает задача целиком и мы оцениваем ее примерно в то же время, как и новую задачу. Конечно, сознательно мы понимаем, что сложные части уже продуманы и даже пытаемся вычесть затраченное на них время из общей оценки. Но, при этом, результирующая прикидка все равно получается завышенной. Если же мы собираемся переписать чужую задачу, то, как правило, просматриваем общую структуру кода, не вникая в рутинные и, на первый взгляд достаточно понятные куски кода. В следствии чего, наше подсознание формирует несколько заниженную оценку и, как правило, мы ее даже не корректируем.

суббота, 12 сентября 2015 г.

Упрощаем код и пишем красиво

Работая в команде, мне часто приходится читать чужой код. И, периодически я вижу простые вещи написанные весьма громоздко и совершенно нечитаемо. Например, много повторяющихся блоков или, еще хуже, лишние операции. Такой код хорош, если ваша цель сделать так, что бы никто не разобрался, однако, в большинстве случаев, такая цель перед нами не стоит.
bool a = SomeCondition();
/* Как упростить следующий код? */
bool b;
if( a.ToString().Length == 5 )
{
   b = true;
} else
{
   b = false;
}
Этот небольшой отрывок я привел отчасти ради шутки, а отчасти для того, что бы показать о чем идет речь. Разумеется, я не думаю, что кто-то реально так пишет. Но, в реальности наши задачи несколько сложнее и, по мере поступления новых вводных, мы часто перестаем видеть знакомые шаблоны и, как следствие, начинаем писать в стиле приведенного выше шуточного примера.