Pull to refresh

Comments 97

Золотые слова — да девелоперу в уши.
Во первых, это девелопер и писал.
Во вторых, часть тезисов описана например в REST.
В третьих, все больше и больше девелоперов пользуются соглашениями.

А вы используете эту поговорку так, как будто девелоперы даже не слышали обо всем этом.
А вы используете эту поговорку так, как будто девелоперы даже не слышали обо всем этом.

Некоторые девелоперы пишут апи так, будто и не слышали обо всем этом.
Некоторые «представители професси» делают «работу» так, как будто не слышали обо всем этом.
Это не повод печатать все снипеты — стены не бесконечны, и уж тем более не обобщать всех девелоперов и стараться что-то им в уши сувать.
Повторение — мать учения.
… и прибежище для тупиц.
лучше в мозг, в уши не всегда надежно
А всем ли нужна инъекция java-api в мозг, когда, например, в rails часть этих рекомендаций решена конвенциями?
Методы не должны возвращать значения, требующие особой обработки. Пользователи будут забывать о проверках. Например, возвращай пустой массив или список, а не null.

А если результат — это единичный экземпляр класса?
То что возвращать вместо null?
А в объектно-ориентированном API?
Вы имеете ввиду, что в Java < 8 нет Optional? Подключить Гуаву
Нет, я имею в виду, что option как паттерн свойственен функциональным языкам. А в объектно-ориентированных, в которых нет явного паттерн-матчинга с проверкой присутствия всех веток, он правращается в ту же самую обработку специальных случаев, просто более кратко выраженную.
Из Optional значение без проверки не извлечь. Это защитит от «забывания» на этапе компиляции. На самом деле, даже на уровне IDE.
Можно пойти по пути наименьшего сопротивления и вызвать get(), и не проверять все остальные случаи — проверки на это на этапе компиляции нет.
Это значит, что пользователь не забыл про проверку, а сознательно ею пренебрег — ну, значит сам себе злобный Буратино.
Так и в случае получения просто объекта пользователь может «сознательно пренебречь» проверкой. Разница невелика, поддержки уровня компилятора нет.
Можно. Я знаю много людей, которые достают значения из Nullable<T> (схожий по поведению, но иной по задаче класс в C#) через .Value, без проверки, всегда.
Если API на самом деле всегда возвращает значение, но обернутое в Nullable, то это плохо. А если оно иногда возвращает null, мне очень интересно, как же работает код этих людей.
А он работает до тех пор, пока звезды не сложатся в фигу, после чего мы внезапно получаем nullref в гигантском однострочнике и тратим день на выяснение причин.

Контракты и статический анализ рулят.
Статический анализ сразу укажет на nullable.Value.call(), в Java, во всяком случае, сразу бы указал. В чем прокол сознательно говнокодить, зато потом гонять анализатор?
Это бессознательное. Человек просто не думает, что там может быть null. Мозг в сторону пограничных ситуаций не повернут.
Это очень спорный тезис. Во-первых, они демпингуют. А во-вторых, заказчик в половине случаев не видит разницы.
Значит это не наш заказчик.
Это защитит от «забывания» на этапе компиляции. На самом деле, даже на уровне IDE.

Для этого даже не обязательно использовать Optional: IDE вполне понимают аннотацию @javax.annotation.Nullable на методах.
… что возвращает нас к тому, что при правильно прикрученном статическом анализаторе и не менее правильно описанном контракте можно и null вернуть.
На самом деле, что Optional, что @Nullable работают только в том случае, если они проставлены везде и всегда. Т.е. если метод возвращает T, то я на 100% уверен, что нуля быть не может, если же Optional, то гарантированно хоть редко, но null будет. С аннотациями аналогично. И здесь мы упираемся в одну из проблем:

1. Проект не новый, и править в нем все методы не хочется (да и почти невозможно)
2. Может это и можно, но я не нашел как и IDEA настроить чтобы отсутствие аннотации было равносильно NotNull, а не @Nullable. А в таком случае, ставить аннотацию практически везде банально лень.
3. Функциональный тип Optional и работа с ним, как с монадой, для Java-программиста очень непривычна и вызывает отторжение.
2. Может это и можно, но я не нашел как и IDEA настроить чтобы отсутствие аннотации было равносильно NotNull, а не @Nullable. А в таком случае, ставить аннотацию практически везде банально лень.

Посмотрите @ParametersAreNonnullByDefault. IDEA умеет его понимать начиная с 13-й версии. Данную аннотацию можно навесить на пакет, а не только на класс.

3. Функциональный тип Optional и работа с ним, как с монадой, для Java-программиста очень непривычна и вызывает отторжение.
Согласен. Оно выглядит инородно, на самом деле пользы дает мало и с учетом @Nullable — не нужно.
Приделать классу метод isNull() и спать спокойно.
… который (а) не соответствует доменной модели и (б) все равно никто не будет проверять.
Скажем так, лично я это решение люблю за его простоту и по-возможности использую. А насчет проверки — ну тут уж сложно что-то универсальное придумать. Накосячить всегда можно, особенно в языках с динамической типизацией. Я с ужасом вспоминаю свои первые опыты с php, когда я узнал, что при вызове функции надо this проверять, что он живой, а так же все входящие параметры функции, что они вообще были переданы, и к тому же имеют нужный тип.
Скажем так, лично я это решение люблю за его простоту и по-возможности использую.

Чем оно лучше просто null?
Я, как ярый разработчик на С++, не могу вернуть null из функции, возвращающей объект какого-нибудь класса. Поэтому приходится к классу приделывать isNull(), чтобы возвращаемый объект мог быть как бы объектом нужного класса, но при этом null.
Это ограничение используемого вами языка. Это не значит, что оно применимо к прочим языкам.

Ну и более того, в этом случае у вас просто нет проблемы «операция возвращает null, который нужно обрабатывать», поэтому ваше решение вообще не от этой задачи.
А если результат — это единичный экземпляр класса?

Мое решение работает для всех языков, где есть функции, классы и тип bool. Как с ограничениями в виде статической типизации, так и без.
Более того, объект класса может нормально функционировать даже в том случае, если он isNull. А вот попытка вызова метода класса для объекта null без проверки, что это null, приведет к появлению исключения, которое придется обрабатывать.
объект класса может нормально функционировать даже в том случае, если он isNull.

Что значит «нормально»? Вот предположим, у вас объект — это запись с сотрудником, какой у нее идентификатор?

Мое решение работает для всех языков, где есть функции, классы и тип bool. Как с ограничениями в виде статической типизации, так и без.

Работать-то работает. Но в большей части случаев оно избыточно и не несет дополнительной пользы.

PS Вообще, это паттерн Null Object, со всеми его недостатками.
Вот предположим, у вас объект — это запись с сотрудником, какой у нее идентификатор?

Например, null_id. Я когда работаю с идентификаторами из БД, которые являются целыми положительными числами,
использую -1 в качестве null_id, при этом сама проверка на isNul() выглядит как
return id==null_id;
id, естественно, при этом уже знаковый тип данных.

PS Вообще, это паттерн Null Object, со всеми его недостатками.


В принципе, я с Вами согласен. Если у Вас язык с динамической типизацией, можно не париться и использовать null, но нужно быть готовым к исключениям. А вот если со статической, то Null Object неплохо работает. В библиотеке Qt, например, isNull() используется повсеместно, и получается довольно удобно. Да и у Вас в исходном вопросе было ограничение, что результат — именно объект некоего класса.
Например, null_id.

А для аналогичных значений (имя, фамилия, дата рождения)? А что будет, если система, не проверив на isNull, передаст этот идентификатор дальше?

Понимаете ли, оригинальный подход создавал потенциальный nullref. Ваш подход создает потенциально невалидные данные или отложенную ошибку класса «некорректная операция».

А вот если со статической, то Null Object неплохо работает.

Плохо он работает. Одна забытая проверка — и ошибки пошли каскадом. Хорошо он работает тогда, когда есть полностью корректный экземпляр «отсутствующих» данных.

Да и у Вас в исходном вопросе было ограничение, что результат — именно объект некоего класса.

Во многих современных языках null — это (отсутствующий) экземпляр определенного класса.
когда есть полностью корректный экземпляр «отсутствующих» данных

Именно это я и имел в виду, когда говорил про то, что объект класса, который isNull, может нормально функционировать. При этом Вы, как разработчик класса, можете это обеспечить, а те, кто будет использовать Ваше API, будут иметь возможность забыть про проверку и тем не менее получить работоспособный код.
Во многих современных языках null — это (отсутствующий) экземпляр определенного класса.

Во многих — но не во всех. И это при проектировании API нужно учитывать.
При этом Вы, как разработчик класса, можете это обеспечить

Нет, потому что не для всякого класса можно создать такой нормальный экземпляр. Например, в примере выше ваш экземпляр сотрудника — некорректен, потому что используемый им идентификатор не удовлетворяет введенному вами же правилу «идентификаторы — только целые положительные числа».

Во многих — но не во всех. И это при проектировании API нужно учитывать.

API проектируется для использования в конкретном языке (или конкретной технологии). Их возможности и надо учитывать. Учесть все существующие языки нельзя (и бессмысленно).
введенному вами же правилу

Это правило ввел не я — так работает БД. Что и позволяет мне в своем коде использовать диапазон <0 под свои корыстные нужды.
API проектируется для использования в конкретном языке (или конкретной технологии)

Хорошо, если это действительно так. А если Вы хотите обеспечить максимально возможный охват языков и платформ, базовая часть API должна быть максимально универсальной. Тогда можно будет реализовать это API в виде библиотеки на низкоуровневом языке (например, тот же С/С++), а потом по-быстрому наклепать оберток под другие языки (Java, Python, Ruby, Perl).
Это правило ввел не я — так работает БД. Что и позволяет мне в своем коде использовать диапазон <0 под свои корыстные нужды.

Что случится, если кто-то передаст в БД идентификатор -1?

А если Вы хотите обеспечить максимально возможный охват языков и платформ, базовая часть API должна быть максимально универсальной.

Не хочу. Это адски тяжелый в разработке сценарий, и он вообще не попадает под правила, описанные выше.
Что случится, если кто-то передаст в БД идентификатор -1?

Что значит «кто-то»? Сериализацию объекта в БД Вы сами и должны написать. Что бы потом пользователи этим уже не страдали.
Не хочу.

Ну не хотите — используйте null. Но будьте готовы к тому, что пользователи Вашего API и на null не проверять, и исключения не обработают. А необработанные исключения — вечный гемор на задницу. Правда, уже не на Вашу, а на пользовательскую.
Что значит «кто-то»? Сериализацию объекта в БД Вы сами и должны написать. Что бы потом пользователи этим уже не страдали.

То и значит. Это же публичный API, пользователи могут использовать возвращенные им объекты, как им угодно.

Но будьте готовы к тому, что пользователи Вашего API и на null не проверять, и исключения не обработают.

К сожалению, защитить от неразумного разработчика его же программу — весьма сложно. Можно защитить свой собственный сервис (внутри границы API).
Это же публичный API, пользователи могут использовать возвращенные им объекты, как им угодно.

Именно поэтому и надо сделать сериализацию за пользователя. Если он вдруг захочет сделать свою, ему никто не помешает накосячить там как угодно. Но по крайней мере одна работающая реализация от разработчика API будет.
защитить от неразумного разработчика его же программу — весьма сложно

См. выше http://habrahabr.ru/post/224929/#comment_7656745
Именно поэтому и надо сделать сериализацию за пользователя.

Сериализация тут не при чем. Вопрос в том, как вся остальная система обрабатывает те данные, которые вы возвращаете в качестве null object.
А какая вообще разница между nullptr и null object в этом случае? И тот и другой либо будут как-то обработаны, либо приведут к появлению ошибок или исключений. И если в рамках реализации API Вы еще можете влиять на этот процесс, то за ее пределами Вы уже ничего не сделаете.
Принципиальная.

null даст ошибку как можно раньше (т.е., на этапе вызова result.id) + null можно ловить статическим анализом.

Null object будет прикидываться адекватным объектом всю дорогу, и выдаст ошибку только тогда, когда какие-то его значения внезапно не совпадут с какими-то заложенными в систему ограничениями.
null даст ошибку как можно раньше

Null Object выдаст ошибку там, где это будет нужно. И если Вы забудете ее сгенерировать в нужной ситуации, то это будет исключительно Ваша ошибка как разработчика API, а не как не пользователя, который это API использует. А детали реализации
result.id

надо от пользователя скрывать (это называется инкапсуляция), чтобы он не имел прямой доступ к полям класса, а использовал соответствующие методы.
Null Object выдаст ошибку там, где это будет нужно.

Ошибку выдаст не он, а использующий его код.

надо от пользователя скрывать (это называется инкапсуляция), чтобы он не имел прямой доступ к полям класса, а использовал соответствующие методы.

Без разницы. Либо ваш null object при обращении к такому методу бросит ошибку (тогда это нарушение принципа подстановки Лисков), либо вернет значение, которое валидно только для null object, тогда привет целостности системы.

Можем рассмотреть все на том же примере, на примерах всегда проще.
Ошибку выдаст не он, а использующий его код.

Ну я бы сделал так, чтобы ошибку выдавал сам объект, но Вам почему-то этот вариант не нравится.

Если Вы хотите пример — поясню:
Класс Object пишется таким образом, что если объект является невалидным (Null Object), то при попытке доступа к данным этого объекта либо выбрасывается исключение, если Вы используете исключения, либо выдаются такие же null объекты, если Вы исключения не используете. При этом принцип подстановки Лисков не нарушается, хотя бы потому, что тип вообще один и не имеет подтипов.
при попытке доступа к данным этого объекта либо выбрасывается исключение

И чем это лучше обычного null?

выдаются такие же null объекты, если Вы исключения не используете

А они валидны во всех сценариях использования?

Если Вы хотите пример — поясню: Класс Object пишется таким образом

Не, это плохой пример, он не раскрывает задачу.

Вот есть задача: есть сотрудник, сотрудника можно уволить (=одна операция). Есть метод Find репозитория, который ищет сотрудника по заданным параметрам. В отличии от метода Get, метод Find не бросает эксепшн, когда не находит объект. Теперь есть простой сценарий: коду необходимо найти сотрудника по параметрам и уволить его. Программист решил использовать метод Find. Что нам делать дальше?
И чем это лучше обычного null?

Лучше тем, что можно выбрасывать более понятные исключения. Если брать пример с сотрудником: программист вызывает метод fire() у Null Object и получает в ответ не стандартное исключение «TypeError: Cannot read property 'fire' of null» (как, например, в javascript), а Ваше, где более человеческим языком написано «попытка удаления несуществующего сотрудника», ну или что-то подобное.
Вам для этих «более понятных» исключений придется предусмотреть вообще все сценарии использования. Это больно (а для публичных API — вплоть до невозможного).
Не вижу особой проблемы написать по одной строчке подобного текста на функцию. Обработку ошибок все равно делать надо, причем чем подробнее, тем лучше.
Не вижу особой проблемы написать по одной строчке подобного текста на функцию.
Он избыточен. Намного проще считать, что функция всегда оперирует валидным объектом.
Намного проще считать, что функция всегда оперирует валидным объектом.

Проще — не значит правильнее. Вы подсовываете пользователю общие исключения, я предпочитаю предоставлять ему более подробную информацию. Что лучше в использовании в конечном счете — решает сам пользователь.
Понимаете ли, вопрос стоимости разработки и поддержки тоже имеет значение. Равно как и быстродействия.
В данном контексте это обсуждать бессмысленно — вот когда будут конкретные задачи с конкретным бюджетом, тогда и надо будет думать, применять этот метод или нет. А я Вам привел вполне работоспособный пример решения поставленной Вами проблемы:
А если результат — это единичный экземпляр класса?
«лучше день потерять, потом за час долететь»

лучше один раз написать NullObject, чем везде делать двойную проверку.

например есть классы User и NullUser (наследник от User). Есть некий метод getUser(String username).
Вот что лучше:

Кусочек кода
User getUser(String username){
Чего-то тут делаем...
return user;
}

User user = getUser("username");
if (user !=null) {
  userId = user.getId();
}else{
  LOG.error("user from 'username' not found.");
}


или
Кусочек кода
class User {
    public static final NULL_USER = new NullUser();

    public boolean isNull(){ return false;}
...
    private class NullUser extend User{
        @Override
        public boolean isNull(){ return true;}
        @Override
        public Long getId(){
            throw new Exception('get id property from NullUser');
        }
    }
}

User getUser(String username){
    // Чего-то тут делаем...
    if (user = null) {
        user = User.NULL_USER;
        LOG.error("not found user from " + username);
    }
    return user;
}

User user = getUser("username");
userId = user.getId();
// или, когда надо проверку:
if (!user.isNull()) userId = user.getId();
Вот ваш код точно нарушает принцип подстановки Лисков: объект класса NullUser имеет поведение отличное от объекта класса User.
а я и не собирался для NullUser его соблюдать — цель была не расширить а именно заменить объект User на «фиктивный объект».
Вообще-то LSP надо соблюдать безотносительно того, расширяете вы объект или просто «заменяете». А то, что вы его нарушаете — признак потенциально неудачного дизайна
можете привести потенциально удачный дизайн, выполняющего те же задачи?
В функциональном программировании — использование option и pattern matching с контролем проверки всех ветвей на этапе компиляции (в F#, например, так).

В «чистом» объектно-ориентированном — использование null совместно с аннотациями/контрактами и статическим анализом.

В промежуточных вариантах — nullable- и notnullable-типы, монада Maybe и так далее.
кстати, если вместо
private class NullUser extend User{
    @Override
    public boolean isNull(){ return true;}
    @Override
    public Long getId(){
        throw new Exception('get id property from NullUser');
    }
}

написать
private class NullUser extend User{
    @Override
    public boolean isNull(){ return true;}
    @Override
    public Long getId(){
        LOG.error('get id property from NullUser');
        return null;
    }
}

то, ИМХО, нарушений LSP не должно быть
Вы просто подвинули nullref на один этап позже — теперь пользователь получит его не на фазе User.getId(), а на фазе GetHistory(User.getId()), потому что та не обучена принимать null.
Для людей прошедших грабельное поле — всё выше описанное имеет смысл, находит отклик «по опыту» и не нуждается в «в рамку и на стенку».
Для не прошедших — набор настовлений типа «одень шапку и застегни куртку, а то простудишься» и то же не заслуживает «в рамку и на стенку»…
Заголовок — часть перевода
На удивление неплохо, я было уже приготовился к очередной порции вредных советов.
Автор исходного поста не подразумевает таких ожиданий
Распечатал, повесил. На рамку сил не хватило, жарко…
Применяй самые подходящие типы. Например, принимай и передавай IP-адрес как специальный тип, а не число или строку.
Ох, как я согласен! Но вот беда: часто бывает, что значение вполне укладывается в примитивный тип (т.е., например, IPv4 адрес эффективнее всего передавать именно что числом), но очевидно, с точки зрения дизайна API — это плохо.

В теории можно было бы делать специальные типы, которые бы на этапе компиляции рассматривались бы как самостоятельные типы, а на этапе исполнения работали бы как примитивы. Вроде как в Java планируют Value Types, которые позволят получить близкий эффект. Как с этим в других языках?
В большинстве компилируемых языков есть zero-overhead типы в том или ином виде.
Под компилируемыми вы подразумеваете «компилируемые в машинный код»?
По поводу C, пожалуй, соглашусь. Но как на счет других? И что делать всем доминирующим на рынке не-компилируем языкам?
Не ставьте производительность выше качества API.
Видимо вы не поняли мой комментарий выше. Я говорю, что в теории возможен способ получать максимально возможную производительность без какого-либо ущерба API, если добавить определенную поддержку со стороны языка. И меня интересует, есть ли в каких-то языках уже такое в готовом виде.
Так я уже ответил. Из не компилируемых в натив, вроде, в Скале есть, но не уверен. Про другие не знаю.
Наверное во всех, где поддерживается перегрузка операторов?
Эээ, как «легкие типы» связаны с перегрузкой операторов?
Здорово конечно, но в реальности практика бывает очень далека от теории…
Пиши программы хорошо. Хорошие программы хорошо работают.

Плохо программы не пиши. Плохие программы работают хуже, чем хорошие.
Вы не правы. Во-первых, это с опытом какие-то вещи кажутся очевидными, для новичка они отнюдь не очевидны. Во-вторых, уверяю вас, если вы не перечитаете этот список непосредственно перед разработкой API, вы точно что-то упустите. В-третьих, некоторые пункты упущены в большинстве даже новых API, что уже значит, что они как минимум не очевидны. («Большинство API слишком заточены под нужды своих создателей»)

Вот, например, Закодируй шаблоны использования до реализации API — на это забивают все, а новичку даже в голову не придет. Только на своей шкуре убеждаешься, насколько это важно.
кстати, «шаблоны использования» тут = use case? TDD?
Use-cases. Не совсем. Этот код необязательно даже компилировать, если есть существенные косяки, они станут очевидны мгновенно. Но потом этот код можно будет допилить как тесты.
По следам статьи про хакатон с ЕМП?
Нет, потому что даже не понимаю, о чем речь.
Не смотря на то, что текст — поток общих слов, написанное можно использовать как чек-лист при работе над API. А лучше учитывать заранее и время от времени поглядывать в рамку на стене
Делать API удобным, простым и понятным — это безусловное благо.

К сожалению, призыв «простые вещи должны делаться просто» иногда приводит к разработке API из двух частей: «упрощённой» и «настоящей».

Если реально необходимый объём функциональности будет доступен только на уровне заведомо выше «базового» понимания, то разработка «простых и понятных» (упрощенных) методов, скрывающих настоящую сложность подсистемы — не кажется хорошей идеей. В плохом сценарии, эти методы потребуют много усилий по разработке и поддержке, но окажутся применимыми весьма ограничено.

Поэтому, если причина разработки API — необходимость не просто состыковать программные модули, а скрыть сложность какой-то подсистемы, к вопросу упрощения следует подходить сдержанно, а к проработке API — более серьёзно.
Sign up to leave a comment.

Articles