Comments 30
Только если эта библиотека плнируется к использованию ну совсем новичками
А после входа в функцию исключительная ситуация становится ошибочной (= отказом).
А еще некоторые убежденно доказывают, что кидаться исключениями — моветон, т.к. исключительная ситуация по их мнению, это когда сервер отвалился, в остальных случаях можно просто вернуть null и написать if. Это не про Java, но тем не менее.
Вообще я пытаюсь сказать, что мне больше нравится так
function getApple() {
return null;
}
function bar(apple) {
...
}
function foo() {
var apple = getApple();
if (!apple) {
throw new Error('apple is empty!');
}
bar(apple);
...
}
чем так
function getApple() {
return null;
}
function bar(apple) {
if (!apple) {
throw new Error('apple is empty!');
}
...
}
function foo() {
var apple = getApple();
bar(apple);
...
}
Но, может, я заблуждаюсь, т. к. разрабатывать большие программы мне не доводилось.
А еще лучше, как мне кажется, вот так:
function getApple(): Apple
{
$apple = <...>;
if (!$apple) {
throw new AppleNotFoundException();
}
return $apple;
}
Имхо, такой подход гораздо лучше. Разработчик ХОРОШЕЙ библиотеки не станет заморачиваться проверкой каждого угла на наличие null'ов. Его задача — правильно составить документацию, а в идеале — даже без нее, чтобы из названий классов и методов было ОЧЕВИДНО, возможен ли тут null или нет. А задача не пускать нулл должна быть на плечах пользователя библиотеки.
Проверка входных параметров для любых публичных методов необходима. Как и для методов, которые могут быть переопределены. Я искренне не понимаю что здесь можно обсуждать.
И в этом случае параметры проверяются дважды — до вызова метода и внутри него. Ясно, что два-три лишних if'a погоды не сделают, но зачем?
Это две разные проверки, и обе проверки — важны.
Ваша проверка в функции foo
— это часть алгоритма. Ситуация, когда яблоко было не найдено, является исключительной и требует особой обработки.
Проверку же в функции bar
требует принцип защитного программирования. Ее задача — остановить распространение возможной ошибки по программе для упрощения диагностики. Одновременно, эту проверку можно считать частью контракта функции, потому что она легко видна любому кто решит посмотреть на функцию по-внимательнее.
Ошибка, брошенная в функции bar
, по-хорошему не должна нигде ловиться (принцип "Fail Fast"). От хорошего программиста ожидается, что увидев трассировку стека с этой ошибкой, он хлопнет себя по лбу и допишет случайно пропущенную проверку в функции foo
.
Исключение это хорошо, вешаем глобальный логер, и получаем полную картину мира продакшена, и как бонус фингерприн попыток взлома ...
Из моего опыта, проверка аргументов появляется либо сразу в методе публичного API, либо после того, как кто-то словил NullReferenceException.
Проверка входных данных должна быть сразу же (если, конечно, это не прототип), и о неправильном вводе пользователь должен получать вменяемое сообщение. А внутренние методы, по хорошему, не должны вызываться с пустыми (= NULL) аргументами (хотя здесь много исключений).
после того, как кто-то словил NullReferenceException
Вот это и есть — недописанный софт. Приходится писать руководству выше, они пишут разработчикам, разработчики исправляют, высылают(!) новую версию программы, ну а пользователь теряет день-два-неделю.
В принципе это Maybe, но чуть изменённая под себя.
Я определяю два интерфейса и наследую их от IEnumerable:
public interface IOptional<T> : IEnumerable<T> { }
public interface IMandatory<T> : IEnumerable<T> { }
Это может показатся странным, но даст нам возможность делать прекрасные вещи :)
Далее я деляю два класса и наследую их от этих интерфеисов:
public class Some<T> : IOptional<T>
{
private readonly IEnumerable<T> _element;
public Some(T element)
: this(new T[1] { element }){}
public Some()
: this(new T[0]) {}
private Some(T[] element)
{
_element = element;
}
public IEnumerator<T> GetEnumerator()
{
return _element.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class Just<T> : IMandatory<T>
{
private readonly T _element;
public Just(T element)
{
_element = element;
}
public IEnumerator<T> GetEnumerator()
{
yield return _element;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Несколько вспомогательных статических методов как Exstensions к этим интерфеисом:
public static class LinqExtensions
{
public static IMandatory<TOutput> Match<TInput, TOutput>(
this IEnumerable<TInput> maybe,
Func<TInput, TOutput> some, Func<TOutput> nothing)
{
if (maybe.Any())
{
return new Just<TOutput>(
some(
maybe.First()
)
);
}
else
{
return new Just<TOutput>(
nothing()
);
}
}
public static T Fold<T>(this IMandatory<T> maybe)
{
return maybe.First();
}
}
Их можно било определить в самих интерфеисах и сделать имплементацию в соответственныйх классах, но здесь показываю как пример, а так можно ещё пару интересных методов добавить.
А теперь можно творить такие вещи:
var five = new Just<int>(5);
var @null = new Some<int>();
Console.WriteLine(
five
.SelectMany(f => @null.Select(n => f * n))
.Match(
some: r => $"Result: {r}",
nothing: () => "Ups"
)
.Fold()
);
Вот так, немного фукциональной парадигмы здорого выручает, даже не нужно объястять Монады, а она здесь присутствует. (SelectMany)
Всё легко и просто :)
Чего только люди не колхозят, лишь бы контракты не использовать...
Использование проверок на Null, нужно стараться вообще исключить из кода. Не передавать ни в качестве аргумента в методы ни ожидать в параметрах конструктора. Если такое случается это повод задуматься об дизаине кода. Скорее всего проблема в нём.
Эта фраза полностью самодостаточно, все остальное после нее — лишнее.
Что касается самих исключений, то я придерживаюсь мнения, что исключения надо использовать только по необходимости, и где это возможно избегать. Это и в стандартной библиотеке встречается, например класс ReadOnlyCollection. Если вы его откроете и посмотрите на строку его объявления то увидите, что он наследует IList, у IList есть метод Add, однако ReadOnlyCollection, использует трюк который называется Explicit Interface Implementation. Получается вы защищены от вызова метода на инстанции класса, однако не защищены от вызова этого метода на интерфейсе.
IList<int> collection = new ReadOnlyCollection<int>(new int[] { 1, 2, 3 });
collection.Add(10); // throws exception
Как generic-и нас спасают от упаковки