Прочитав статью «Усиливаем контроль типов: где в типичном C#-проекте присутствует непрошеный элемент слабой типизации?» был порядком удивлен, как ошибочностью данного подхода, так и тем, что никто не обратил на это внимание.
Автор поста приводит идею того, что метод, возвращающий ссылочный тип, объект которого создается из некоего репозитория, должен, тем или иным образом, гарантировать, что возвращаемый объект не будет null. При этом в одном из примеров он использует контракты, что противоречит их принципам. Я хочу разобрать ошибочность этого подхода.
У нас есть метод GetItem
, который достает объект из некоего репозитория и должен, по замыслу автора, гарантировать, что объект будет не null.
public Item GetItem(int itemId)
{
return dataAccessor.getItemById(itemId);
}
В случае, если в репозитории не оказалось нужного объекта, dataAccessor
вернет null, который будет отдан клиенту и тот, не проверив значение на null, получит NullReferenceException. Так как-же можно гарантировать, что этого не произойдет?
Можно втиснуть проверку на null внутрь метода и в случае, если объект не найден, бросить Exception. Но этим мы просто переименовываем NullReferenceException в, к примеру, ItemNotFoundException. Более того, теперь клиент обязан добавлять try-catch блок при каждом обращении к этому методу. Писатель клиента будет очень рад.
Еще вариант, это добавить пост-условие в метод. Вот так:
public Item GetItem(int itemId)
{
Contract.Ensures(Contract.Result<Item>() != null);
return dataAccessor.GetItemById(itemId);
}
Тут все становится совсем плохо и котят в мире становится меньше. Прочитав контракт, любой человек должен понять, что этот метод вернет объект всегда, при любом предоставленном идентификаторе. Но это невозможно. Метод не может гарантировать, что объект с этим Id есть в репозитории. Значит нужно добавить пред-условие, которое поможет бедолаге и спихнет все проблемы клиенту:
public Item GetItem(int itemId)
{
Contract.Requires(dataAccessor.GetItemById(itemId) != null)
Contract.Ensures(Contract.Result<Item>() != null);
return dataAccessor.GetItemById(itemId);
}
Весело, не правда ли? Что бы достать объект из репозитория, нужно сначала вытащить объект из репозитория, для чего нужно вытащить объект из репозитория, для чего…
Правда в том, что ложки нет никто не может гарантировать существование объекта в репозитории. Единственный способ гарантировать существование любого объекта это создать его и зажать его ссылку. Все остальное – плохой дизайн. Data access layer должен достать объект из репозитория и передать клиенту. Он не знает как обработать отсутствие объекта и вообще не в курсе плохо ли это. За это отвечает уровень логики.
Невозможность гарантировать существования объекта вне поля видимости, приводит ко второй идее приведенной в статье по ссылке вверху. Использование nullable контейнера для ссылочных типов. Я перефразирую: nullable reference. Масло масляное. Было странно узнать про популярность этого паттерна.
Ссылочный тип может ссылаться на null. Зачем засовывать один ссылочный тип в другой и добавлять в контейнер свойство HasValue, для меня решительно не понятно. Для проверки на HasValue? Что мешает обратится к содержимому объекту без этой проверки? Можно точно так-же безалаберно не проверить на null через неравенство. Более того, этот паттерн не только бесполезен, но и вреден. Взгляните на следующий код:
public Maybe<Item> SomeMethod()
{
var mbItem = GetMbItem();
if(mbItem.HasValue)
{
var item = mbItem.Value;
ModifyItem(ref item);
}
return mbItem;
}
В случае если метод ModifyItem подменил объект, то метод SomeMethod вернет контейнер со старым объектом. Иди потом, ищи этот баг.
В общем, я считаю практику, показанную в той статье, вредной. Для проверки ссылки на null, в C# есть стандартные средства, а для того, чтобы не допускать NullReferenceException, нужно быть внимательным. Усложнять систему для этого совсем необязательно. Работая с репозиториями нужно следить за целостностью данных и от этого не убежишь. Если в репозитории нет объекта, который там обязательно должен быть, это намного-намного хуже.
Автор: aikixd