Как я могу сделать вызов с булевой очисткой? Булева ловушка

76

Как отмечено в комментариях @ benjamin-gruenbaum, это называется булевой ловушкой:

Скажем, у меня есть такая функция

UpdateRow(var item, bool externalCall);

и в моем контроллере это значение externalCallвсегда будет TRUE. Каков наилучший способ вызвать эту функцию? Я обычно пишу

UpdateRow(item, true);

Но я спрашиваю себя, должен ли я объявить логическое значение, просто чтобы указать, что означает это «истинное» значение? Вы можете узнать это, посмотрев объявление функции, но это, очевидно, быстрее и понятнее, если вы только что увидели что-то вроде

bool externalCall = true;
UpdateRow(item, externalCall);

PD: Не уверен, что этот вопрос действительно подходит, если нет, где я могу получить больше информации об этом?

PD2: Я не пометил ни одного языка, потому что думал, что это очень общая проблема. Во всяком случае, я работаю с C # и принятый ответ работает для C #

Марио Гарсия
источник
10
Этот паттерн также называется булевой ловушкой
Бенджамин Грюнбаум
11
Если вы используете язык с поддержкой алгебраических типов данных, я бы очень рекомендовал новый adt вместо логического. data CallType = ExternalCall | InternalCallв хаскеле например.
Филип Хаглунд
6
Я думаю, что Enums будет выполнять ту же цель; получение имен для логических значений и немного безопасности типов.
Филипп Хаглунд
2
Я не согласен с тем, что объявление логического значения для обозначения значения «очевидно яснее». При первом варианте очевидно, что вы всегда будете передавать значение true. Со вторым вы должны проверить (где определена переменная? Ее значение изменяется?). Конечно, это не проблема, если две строки вместе ... но кто-то может решить, что идеальное место для добавления их кода именно между двумя строками. Такое случается!
AJPerez
Объявление логического значения не является более чистым, более ясным, что оно просто перепрыгивает через шаг, который просматривает документацию рассматриваемого метода. Если вы знаете сигнатуру, менее понятно добавить новую константу.
1818

Ответы:

154

Не всегда есть идеальное решение, но у вас есть много вариантов на выбор:

  • Используйте именованные аргументы , если они доступны на вашем языке. Это работает очень хорошо и не имеет особых недостатков. В некоторых языках любой аргумент может быть передан как именованный аргумент, например updateRow(item, externalCall: true)( C # ) или update_row(item, external_call=True)(Python).

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

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

    Это очень читабельно, но приводит к большому количеству шаблонов для вас, которые пишут эти функции. Он также не может справиться с комбинаторным взрывом, когда существует множество логических аргументов. Существенным недостатком является то, что клиенты не могут установить это значение динамически, но должны использовать if / else для вызова правильной функции.

  • Используйте перечисление . Проблема с логическими значениями заключается в том, что они называются «истина» и «ложь». Итак, вместо этого введите тип с лучшими именами (например enum CallType { INTERNAL, EXTERNAL }). Как дополнительное преимущество, это повышает безопасность типов вашей программы (если ваш язык реализует перечисления как отдельные типы). Недостаток перечислений заключается в том, что они добавляют тип в ваш общедоступный API. Для чисто внутренних функций это не имеет значения, и перечисления не имеют существенных недостатков.

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

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

Амон
источник
7
Я только что узнал, что существуют «именованные аргументы». Я думаю, что это лучшее предложение. Если вы можете немного проработать эту опцию (чтобы другие люди не слишком гуглили об этом), я приму этот ответ. Также любые намеки на производительность, если вы можете ... Спасибо
Марио Гарсия
6
Одна вещь, которая может помочь решить проблемы с короткими строками, - это использовать константы со значениями строк. @MarioGarcia Не так много, чтобы уточнить. Помимо того, что упомянуто здесь, большинство деталей будет зависеть от конкретного языка. Было ли что-то конкретное, что вы хотели увидеть здесь?
jpmc26
8
В языках, где мы говорим о методе и перечислении, можно ограничить класс, я обычно использую перечисление. Он полностью самодокументируется (в единственной строке кода, который объявляет тип enum, поэтому он также является легковесным), полностью предотвращает вызов метода с голым логическим значением, и влияние на публичный API незначительно (так как идентификаторы относятся к классу).
Давидбак
4
Еще одним недостатком использования строк в этой роли является то, что вы не можете проверить их правильность во время компиляции, в отличие от перечислений.
Руслан
32
enum - победитель Тот факт, что параметр может иметь только одно из двух возможных состояний, не означает, что он должен быть автоматически логическим.
Пит
39

Правильное решение - сделать то, что вы предлагаете, но упаковать его в мини-фасад:

void updateRowExternally() {
  bool externalCall = true;
  UpdateRow(item, externalCall);
}

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

Килиан Фот
источник
8
Стоит ли эта инкапсуляция, когда я вызываю эту функцию только один раз в моем контроллере?
Марио Гарсия,
14
Да. Да, это. Причина, как я писал выше, заключается в том, что стоимость (вызов функции) меньше выгоды (вы экономите нетривиальное количество времени на разработку, потому что никто никогда не должен искать то, что trueделает.) Это будет целесообразно, даже если стоит столько же времени процессора, сколько экономит время разработчика, поскольку время процессора намного дешевле.
Килиан Фот
8
Также любой компетентный оптимизатор должен быть в состоянии указать это для вас, чтобы вы ничего не потеряли в конце.
firedraco
33
@KilianFoth За исключением того, что это ложное предположение. Сопровождающий действительно нужно знать , что делает второй параметр, и почему это так, и почти всегда это относится к деталям , что вы пытаетесь сделать. Сокрытие функциональности в крошечных функциях «смерть на тысячу резов» снизит удобство обслуживания, а не увеличит его. К сожалению, я сам это видел. Чрезмерное разбиение на функции на самом деле может быть хуже, чем огромная функция Бога.
Грэм
6
Я думаю, UpdateRow(item, true /*external call*/);что будет чище, в языках, которые допускают синтаксис комментариев. Раздувание вашего кода с помощью дополнительной функции просто для того, чтобы избежать написания комментария, кажется не стоит для простого случая. Возможно, если бы к этой функции было много других аргументов и / или какой-нибудь загроможденный + хитрый окружающий код, он начал бы привлекать больше. Но я думаю, что если вы отлаживаете, вы в конечном итоге проверяете функцию-обертку, чтобы увидеть, что она делает, и должны помнить, какие функции являются тонкими обертками вокруг библиотечного API, а какие на самом деле имеют некоторую логику.
Питер Кордес
25

Скажем, у меня есть такая функция, как UpdateRow (var item, bool externalCall);

Почему у вас есть такая функция?

При каких обстоятельствах вы бы назвали его с аргументом externalCall, установленным на разные значения?

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

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

Фил В.
источник
6
Я согласен. И просто для того, чтобы подчеркнуть мысль, высказанную другими читателями, можно выполнить анализ всех вызывающих абонентов, которые передадут либо true, false, либо переменную. Если ни один из последних не найден, я бы поспорил о двух отдельных методах (без логического) по сравнению с одним с логическим - это аргумент YAGNI, что логическое значение вводит неиспользованную / ненужную сложность. Даже если некоторые вызывающие абоненты передают переменные, я все же могу предоставить (булевские) версии без параметров, чтобы использовать их для вызывающих абонентов, передающих константу - это проще, что хорошо.
Эрик Эйдт,
18

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

UpdateRow(item, true /* row is an external call */);

или же:

UpdateRow(item, true); // true means call is external

или (правильно, как предложено Фраксом):

UpdateRow(item, /* externalCall */true);
епископ
источник
1
Нет причин для отрицания. Это совершенно обоснованное предложение.
Suncat2000
Цените <3 @ Suncat2000!
епископ
11
(Вероятно, предпочтительный) вариант просто помещает туда простое имя аргумента, например UpdateRow(item, /* externalCall */ true ). Комментарий в полном предложении анализировать гораздо сложнее, на самом деле это в основном шум (особенно второй вариант, который также очень слабо связан с аргументом).
Frax
1
Это, безусловно, самый подходящий ответ. Это именно то, для чего предназначены комментарии.
Страж
9
Не большой поклонник таких комментариев. Комментарии могут стать устаревшими, если их не трогать при рефакторинге или внесении других изменений. Не говоря уже о том, что если у вас есть более одного параметра bool, это быстро сбивает с толку.
Джон В.
11
  1. Вы можете «назвать» ваши bools. Ниже приведен пример для языка ОО (где он может быть выражен в классе, предоставляющем UpdateRow()), однако сама концепция может быть применена на любом языке:

    class Table
    {
    public:
        static const bool WithExternalCall = true;
        static const bool WithoutExternalCall = false;
    

    и на сайте вызова:

    UpdateRow(item, Table::WithExternalCall);
    
  2. Я считаю, что пункт № 1 лучше, но он не заставляет пользователя использовать новые переменные при использовании функции. Если безопасность типов важна для вас, вы можете создать enumтип и заставить его UpdateRow()принять вместо bool:

    UpdateRow(var item, ExternalCallAvailability ec);

  3. Вы можете изменить имя функции так, чтобы оно лучше отражало значение boolпараметра. Не очень уверен, но возможно:

    UpdateRowWithExternalCall(var item, bool externalCall)

Симург
источник
8
Не нравится # 3, как сейчас, если вы вызываете функцию с помощью externalCall=false, ее имя не имеет абсолютно никакого смысла. Остальная часть вашего ответа хороша.
Легкость гонок с Моникой
Согласовано. Я не был уверен, но это может быть жизнеспособным вариантом для какой-то другой функции. Я
симург
@LightnessRacesinOrbit Действительно. Безопаснее идти UpdateRowWithUsuallyExternalButPossiblyInternalCall.
Эрик Думинил
@EricDuminil: Пятно на 😂
Гонки
10

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

Например, IntelliJ IDEA печатает имя переменной переменных в вызываемом методе, если вы передаете литерал, такой как trueили nullили username + “@company.com. Это сделано маленьким шрифтом, чтобы он не занимал слишком много места на экране и сильно отличался от реального кода.

Я до сих пор не говорю, что это хорошая идея - добавлять булевы везде. Аргумент, говорящий о том, что вы читаете код гораздо чаще, чем пишете, часто очень силен, но в данном конкретном случае он сильно зависит от технологии, которую вы (и ваши коллеги!) Используете для поддержки своей работы. С IDE это гораздо меньше проблем, чем, например, с vim.

Модифицированный пример части моей тестовой установки: введите описание изображения здесь

Себастьян ван ден Брук
источник
5
Это было бы верно только в том случае, если бы все в вашей команде использовали только IDE для чтения вашего кода. Несмотря на преобладание не-IDE-редакторов, у вас также есть инструменты проверки кода, инструменты управления исходным кодом, вопросы переполнения стека и т. Д., И жизненно важно, чтобы код оставался читаемым даже в этих контекстах.
Даниэль Приден
@DanielPryden, конечно, отсюда мой комментарий «и ваших коллег». В компаниях принято иметь стандартную IDE. Что касается других инструментов, я бы сказал, что вполне возможно, что такие инструменты также будут поддерживать это или будут в будущем, или что эти аргументы просто не применяются (как для команды парных программистов из 2 человек)
Себастьян ван ден Брук
2
@Sebastiaan: я не думаю, что я согласен. Даже будучи индивидуальным разработчиком, я регулярно читаю свой собственный код в Git diffs. Git никогда не сможет сделать тот же вид контекстно-зависимой визуализации, как и среда IDE, и не должна этого делать. Я за использование хорошей IDE, чтобы помочь вам писать код, но вы никогда не должны использовать IDE в качестве предлога для написания кода, который вы бы не написали без него.
Даниэль Приден
1
@DanielPryden Я не защищаю написание чрезвычайно небрежного кода. Это не черно-белая ситуация. Могут быть случаи, когда в прошлом для конкретной ситуации вы были на 40% за написание функции с логическим значением, а на 60% - нет, и в итоге вы не сделали этого. Возможно, при хорошей поддержке IDE баланс составляет 55%, а 45% - нет. И да, возможно, вам все равно иногда придется читать его за пределами IDE, чтобы он не был идеальным. Но это все еще допустимый компромисс с альтернативой, такой как добавление другого метода или перечисления для пояснения кода в противном случае.
Себастьян ван ден Брук
6

2 дня и никто не упомянул полиморфизм?

target.UpdateRow(item);

Когда я клиент, желающий обновить строку с элементом, я не хочу думать о названии базы данных, URL-адресе микросервиса, протоколе, используемом для связи, или о том, является ли внешний вызов нужно будет использовать, чтобы это произошло. Прекратите выдвигать эти детали на меня. Просто возьми мой предмет и иди где-нибудь обновить строку.

Сделайте это, и это станет частью проблемы строительства. Это может быть решено многими способами. Вот один из них:

Target xyzTargetFactory(TargetBuilder targetBuilder) {
    return targetBuilder
        .connectionString("some connection string")
        .table("table_name")
        .external()
        .build()
    ; 
}

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

Следует отметить, что это больше, чем семантическая проблема. Это также проблема дизайна. Передайте логическое значение, и вы будете вынуждены использовать ветвление для его решения. Не очень объектно-ориентированный. Есть два или более логических значений для передачи? Желаете, чтобы ваш язык был многократным? Посмотрите, что коллекции могут делать, когда вы их вкладываете. Правильная структура данных может сделать вашу жизнь намного проще.

candied_orange
источник
2

Если вы можете изменить updateRowфункцию, возможно, вы можете изменить ее на две функции. Учитывая, что функция принимает логический параметр, я подозреваю, что это выглядит так:

function updateRow(var item, bool externalCall) {
  Database.update(item);

  if (externalCall) {
    Service.call();
  }
}

Это может быть немного запах кода. Функция может иметь совершенно разное поведение в зависимости от того, на что установлена externalCallпеременная, и в этом случае она имеет две разные обязанности. Слияние его на две функции, которые несут только одну ответственность, может улучшить читаемость:

function updateRowAndService(var item) {
  updateRow(item);
  Service.call();
}

function updateRow(var item) {
  Database.update(item);
}

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

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

Kevin
источник
0

Если UpdateRow находится в контролируемой кодовой базе, я бы рассмотрел шаблон стратегии:

public delegate void Request(string query);    
public void UpdateRow(Item item, Request request);

Где Request представляет некоторый вид DAO (обратный вызов в тривиальном случае).

Истинный случай:

UpdateRow(item, query =>  queryDatabase(query) ); // perform remote call

Ложный случай:

UpdateRow(item, query => readLocalCache(query) ); // skip remote call

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

UpdateRow(item, query => {
  var data = readLocalCache(query);
  if (data == null) {
    data = queryDatabase(query);
  }
  return data;
} );

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

Basilevs
источник