[Перевод] Если вы собираетесь сидеть и ничего не делать, то хотя бы делайте это правильно

a31c5e3f0400d35a258359595c10dae4.png

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

Например, у Windows есть сложная инфраструктура печати, но этой инфраструктуры нет у Xbox. Что должно произойти, если приложение попытается выполнить печать на Xbox?

Неправильно было бы, если бы функции печати выбрасывали Not­Supported­Exception. Установленное пользователем на Xbox приложение, вероятно, в основном (если не исключительно) тестировалось на PC, где печать всегда доступна. При запуске на Xbox исключение, скорее всего, не будет обработано, и приложение вылетит. Но даже если приложение попытается перехватить исключение, то, вероятно, отобразит сообщение типа «Ой. Что-то пошло не так. Обратитесь в службу поддержки и сообщите ей вот этот код ошибки».

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

Чтобы учесть ситуацию, когда приложение говорит «О, у тебя не установлены принтеры, давай помогу в установке», функция установки принтера может немедленно выполнять возврат с кодом результата, означающим «пользователь отменил операцию».

Смысл здесь в том, чтобы все функции печати вели себя так, чтобы поддержка печати полностью поддерживалась, но принтеры постоянно загадочным образом отсутствовали.

Также, вероятно, стоит добавить функцию, проверяющую, работает ли вообще печать. Приложения могут использовать эту функцию, чтобы скрывать кнопку «Печать» из своего UI, если они выполняются в системе, не поддерживающей печать. Но при этом наивные приложения, предполагающие, что печать работает, всё равно будут вести себя разумно: как будто вы работаете с системой, не имеющей принтеров, а все попытки установить принтер безрезультатны.

Мы называем такое «ничегонеделательное» поведение «инертным».

Поверхность API по-прежнему существует, а функции работают согласно спецификации, но в то же время он ничего не делает. Важно здесь то, что он не делает ничего согласно документации и снижает до минимума вероятность создания проблем с уже имеющимся кодом.

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

HRESULT CreateWidget(_Out_ HWIDGET* widget)
{
    *widget = nullptr;
    return S_OK;
}

// По документации каждый виджет должен иметь хотя бы один псевдоним,
// так что нам нужно создать один поддельный псевдоним (пустую строку)
HRESULT GetWidgetAliases(
    _Out_writes_to_(capacity, *actual) PWSTR* aliases,
    UINT capacity,
    _Out_ UINT* actual)
{
    *actual = 0;

    RETURN_HR_IF(
        HRESULT_FROM_WIN32(ERROR_MORE_DATA),
        capacity < 1);

    aliases[0] = make_cotaskmem_string_nothrow(L"").release();
    RETURN_IF_NULL_ALLOC(aliases[0]);

    *actual = 1;
    return S_OK;
}

// Инертные виджеты нельзя включать или отключать
HRESULT EnableWidget(HWIDGET widget, BOOL value)
{
    return E_HANDLE;
}

HRESULT Close(HWIDGET widget)
{
    RETURN_HR_IF(E_INVALIDARG, widget != nullptr);
    return S_OK;
}

Я указал на то, что если Create­Widget будет выполняться успешно, но возвращать нулевой указатель, то это запутает приложения. «Вызов завершился успешно, но я не получил валидный дескриптор?» Я даже нашёл тестовый код самой команды, проверявший успешность вызова по тому, был ли дескриптор нулевым, а не по возвращаемому значению.

Также я указал на то, что если Enable­Widget будет возвращать «недопустимый дескриптор», то это тоже вызовет непонимание. Приложение вызывает Create­Widget, он завершается успешно, оно берёт этот дескриптор (предположительно валидный) и пытается использовать его для включения виджета, но в ответ получает «Этот дескриптор недействителен». Как такое может быть? «Я попросил виджет и ты мне его дал, а когда я показал его тебе, ты сказал «это не виджет». Этот API меня газлайтит!»

Я изучил документацию к API и выяснил, что когда пользователь отказывается от создания виджета, то возвращаемое значение должно быть ERROR_CANCELLED. То есть приложения и раньше могли обрабатывать возможность того, что виджеты не создаются из-за условий, находящихся вне их контроля. Значит, мы можем этим воспользоваться: когда приложение пытается создать виджет, мы просто будем говорить: «Не-а, тут пользователь отменил операцию. Всё так и было, ага».

HRESULT CreateWidget(_Out_ HWIDGET* widget)
{
    *widget = nullptr;
    return HRESULT_FROM_WIN32(ERROR_CANCELLED);
}

HRESULT GetWidgetAliases(
    _Out_writes_to_(capacity, *actual) PWSTR* aliases,
    UINT capacity,
    _Out_ UINT* actual)
{
    *actual = 0;
    return E_HANDLE;
}

HRESULT EnableWidget(HWIDGET widget, BOOL value)
{
    return E_HANDLE;
}

HRESULT Close(HWIDGET widget)
{
    return E_HANDLE;
}

Так мы создали по-настоящему инертную поверхность API.

Если вы попытаетесь создать виджет, то мы скажем вам, что это невозможно, потому что пользователь отменил действие. Так как все попытки создать виджет заканчиваются неудачно, не существует такого понятия, как валидный дескриптор виджета, и когда бы вы его ни попробовали использовать, дескриптор будет невалидным.

Кроме того, это также позволяет избавиться от необходимости создания поддельных псевдонимов для виджетов. Так как виджетов нет, приложение ни в одной из допустимых ситуаций не может спросить у виджета его псевдонимы.

Дополнение: смысл здесь в том, что API печати всегда существовал на десктопах, потому что на этой платформе печать поддерживается, и по документации функция «выведи мне список принтеров» не выбрасывает исключение. Если вы хотите портировать API печати на Xbox, то как это сделать, чтобы при этом уже готовые десктопные приложения продолжали работать на Xbox? Инертное поведение абсолютно бесхитростно: на Xbox нет принтеров. Никто не ожидает, что в ответ на вопрос «Сколько есть принтеров?» ему ответили «Да как ты смеешь спрашивать о подобном!»

Ещё один сценарий использования инертной поверхности API — это вывод из эксплуатации старого API. Как сделать поведение API согласованным с его контрактом так, чтобы он при этом не делал ничего полезного?

© Habrahabr.ru