Jak mogę nawiązać połączenie z logicznym rozwiązaniem? Boolean Trap

76

Jak zauważono w komentarzach @ benjamin-gruenbaum, nazywa się to pułapką logiczną:

Powiedz, że mam taką funkcję

UpdateRow(var item, bool externalCall);

a w moim kontrolerze ta wartość externalCallzawsze będzie PRAWDA. Jak najlepiej wywołać tę funkcję? Zwykle piszę

UpdateRow(item, true);

Ale pytam samego siebie, czy powinienem zadeklarować wartość logiczną, aby wskazać, co oznacza ta „prawdziwa” wartość? Możesz to wiedzieć, patrząc na deklarację funkcji, ale jest to oczywiście szybsze i wyraźniejsze, jeśli zobaczysz coś takiego

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

PD: Nie jestem pewien, czy to pytanie naprawdę pasuje tutaj, jeśli nie, gdzie mogę uzyskać więcej informacji na ten temat?

PD2: Nie oznaczyłem żadnego języka, ponieważ myślałem, że to bardzo ogólny problem. W każdym razie pracuję z c # i zaakceptowana odpowiedź działa dla c #

Mario Garcia
źródło
10
Ten wzorzec nazywany jest również pułapką boolowską
Benjamin Gruenbaum
11
Jeśli używasz języka z obsługą algebraicznych typów danych, bardzo polecam nową reklamę zamiast wartości logicznej. data CallType = ExternalCall | InternalCallna przykład w haskell.
Filip Haglund
6
Myślę, że Enums wypełniłby dokładnie ten sam cel; uzyskiwanie nazw dla booleanów i trochę bezpieczeństwa typu.
Filip Haglund
2
Nie zgadzam się z tym, że zadeklarowanie wartości logicznej w celu wskazania znaczenia jest „oczywiście wyraźniejsze”. W przypadku pierwszej opcji oczywiste jest, że zawsze zdasz prawdę. W drugim przypadku musisz sprawdzić (gdzie jest zdefiniowana zmienna? Czy jej wartość się zmienia?). Jasne, to nie jest problem, jeśli dwie linie są razem ... ale ktoś może zdecydować, że idealne miejsce na dodanie kodu znajduje się dokładnie między dwiema liniami. Zdarza się!
AJPerez
Zadeklarowanie wartości logicznej nie jest czystsze, jaśniejsze, po prostu przeskakuje o krok, czyli przeglądając dokumentację omawianej metody. Jeśli znasz podpis, dodanie nowej stałej jest mniej jasne.
wnętrza w

Odpowiedzi:

154

Nie zawsze jest idealne rozwiązanie, ale masz wiele alternatyw do wyboru:

  • Użyj nazwanych argumentów , jeśli są dostępne w Twoim języku. Działa to bardzo dobrze i nie ma szczególnych wad. W niektórych językach dowolny argument można przekazać jako nazwany argument, np. updateRow(item, externalCall: true)( C # ) lub update_row(item, external_call=True)(Python).

    Twoja sugestia użycia oddzielnej zmiennej jest jednym ze sposobów symulacji nazwanych argumentów, ale nie ma związanych z tym korzyści bezpieczeństwa (nie ma gwarancji, że użyłeś poprawnej nazwy zmiennej dla tego argumentu).

  • Używaj różnych funkcji dla swojego publicznego interfejsu, z lepszymi nazwami. Jest to inny sposób symulowania nazwanych parametrów poprzez umieszczenie w nazwie wartości parametrów paremeter.

    Jest to bardzo czytelne, ale prowadzi do wielu pytań dla Ciebie, który pisze te funkcje. Nie może również dobrze poradzić sobie z eksplozją kombinatoryczną, gdy istnieje wiele argumentów boolowskich. Istotną wadą jest to, że klienci nie mogą ustawić tej wartości dynamicznie, ale muszą użyć if / else, aby wywołać poprawną funkcję.

  • Użyj wyliczenia . Problem z booleanami polega na tym, że nazywane są „prawdą” i „fałszem”. Zamiast tego wprowadź typ o lepszych nazwach (np enum CallType { INTERNAL, EXTERNAL }.). Dodatkową korzyścią jest zwiększenie bezpieczeństwa typów programu (jeśli Twój język implementuje wyliczenia jako odrębne typy). Wadą wyliczeń jest to, że dodają typ do publicznie widocznego interfejsu API. W przypadku funkcji czysto wewnętrznych nie ma to znaczenia, a wyliczenia nie mają znaczących wad.

    W językach bez wyliczeń czasami używane są krótkie ciągi znaków . Działa to, a może nawet lepiej niż surowe booleany, ale jest bardzo podatne na literówki. Funkcja powinna następnie natychmiast stwierdzić, że argument pasuje do zestawu możliwych wartości.

Żadne z tych rozwiązań nie ma nadmiernego wpływu na wydajność. Nazwane parametry i wyliczenia można całkowicie rozwiązać w czasie kompilacji (dla skompilowanego języka). Korzystanie z ciągów znaków może wiązać się z porównywaniem ciągów, ale ich koszt jest znikomy w przypadku małych literałów ciągów i większości rodzajów aplikacji.

amon
źródło
7
Właśnie dowiedziałem się, że istnieją „nazwane argumenty”. Myślę, że to zdecydowanie najlepsza sugestia. Jeśli możesz trochę rozwinąć tę opcję (aby inne osoby nie potrzebowały zbyt google o tym), zaakceptuję tę odpowiedź. Także wszelkie wskazówki dotyczące wydajności, jeśli możesz ... Dzięki
Mario Garcia
6
Jedną z rzeczy, które mogą pomóc złagodzić problemy z krótkimi łańcuchami, jest użycie stałych z wartościami łańcuchów. @MarioGarcia Nie ma wiele do rozwinięcia. Oprócz tego, co tu wspomniano, większość szczegółów zależy od konkretnego języka. Czy było coś konkretnego, o czym chciałeś tutaj wspomnieć?
jpmc26
8
W językach, w których mówimy o metodzie, a wyliczenie można objąć zakresem klasy, zwykle używam wyliczenia. Jest całkowicie samo dokumentujący (w jednym wierszu kodu, który deklaruje typ wyliczenia, więc jest również lekki), całkowicie uniemożliwia wywołanie metody nagim boolean, a wpływ na publiczne API jest znikomy (ponieważ identyfikatory są ograniczone do klasy).
davidbak
4
Kolejną wadą używania ciągów w tej roli jest to, że nie można sprawdzić ich poprawności w czasie kompilacji, w przeciwieństwie do wyliczeń.
Ruslan
32
enum jest zwycięzcą. To, że parametr może mieć tylko jeden z dwóch możliwych stanów, nie oznacza, że ​​powinien on automatycznie być wartością logiczną.
Pete
39

Prawidłowym rozwiązaniem jest robienie tego, co sugerujesz, ale zapakuj je w minifasadę:

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

Czytelność przebija mikrooptymalizację. Możesz sobie pozwolić na dodatkowe wywołanie funkcji, na pewno lepiej niż na wysiłek programisty polegający na konieczności sprawdzenia semantyki flagi logicznej nawet raz.

Kilian Foth
źródło
8
Czy to enkapsulacja naprawdę się opłaca, gdy wywołuję tę funkcję tylko raz w moim kontrolerze?
Mario Garcia
14
Tak. Tak to jest. Powodem, jak napisałem powyżej, jest to, że koszt (wywołanie funkcji) jest mniejszy niż korzyść (oszczędzasz niebanalną ilość czasu programisty, ponieważ nikt nigdy nie musi sprawdzać, co truerobi). Byłoby opłacalne, nawet gdyby to kosztują tyle czasu procesora, ile oszczędza czas programisty, ponieważ czas procesora jest znacznie tańszy.
Kilian Foth
8
Również każdy kompetentny optymalizator powinien być w stanie to dla Ciebie wprowadzić, aby nie stracić niczego na końcu.
firedraco
33
@KilianFoth Z wyjątkiem tego, że jest to fałszywe założenie. Opiekun musi wiedzieć, co robi drugi parametr, i dlatego, że to prawda, i prawie zawsze odnosi się to do szczegółu, co próbujesz zrobić. Ukrywanie funkcjonalności w drobnych funkcjach śmiertelnych o tysiąc cięć zmniejszy łatwość konserwacji, a nie ją zwiększy. Z przykrością widziałem, jak to się stało. Nadmierne dzielenie na funkcje może być gorsze niż ogromna Boska Funkcja.
Graham
6
Myślę, że UpdateRow(item, true /*external call*/);byłoby czystsze, w językach, które pozwalają na tę składnię komentarzy. Nadymanie kodu za pomocą dodatkowej funkcji tylko po to, by uniknąć pisania komentarza, nie wydaje się tego warte w prostym przypadku. Może gdyby było wiele innych argumentów do tej funkcji i / lub trochę zaśmieconego + podstępnego otaczającego kodu, zacząłby apelować bardziej. Ale myślę, że jeśli debugujesz, skończysz sprawdzanie funkcji opakowania, aby zobaczyć, co ona robi, i musisz pamiętać, które funkcje są cienkimi opakowaniami wokół interfejsu API biblioteki, a które mają pewną logikę.
Peter Cordes
25

Powiedzmy, że mam taką funkcję jak UpdateRow (var item, bool externalCall);

Dlaczego masz taką funkcję?

W jakich okolicznościach wywołałbyś to z argumentem externalCall ustawionym na inne wartości?

Jeśli jedna pochodzi, powiedzmy, z zewnętrznej aplikacji klienckiej, a druga znajduje się w tym samym programie (tj. Różnych modułach kodu), to argumentowałbym, że powinieneś mieć dwie różne metody, jedną dla każdego przypadku, być może nawet zdefiniowaną odrębnie Interfejsy

Jeśli jednak wykonujesz wywołanie na podstawie pewnej bazy danych pobranej ze źródła innego niż program (powiedzmy, plik konfiguracyjny lub odczyt bazy danych), wówczas metoda przekazywania wartości logicznej byłaby bardziej sensowna.

Phill W.
źródło
6
Zgadzam się. I tylko po to, aby wyjaśnić sprawę innym czytelnikom, można przeprowadzić analizę wszystkich rozmówców, którzy przekażą wartość true, false lub zmienną. Jeśli żadna z tych ostatnich nie zostanie znaleziona, argumentowałbym za dwiema oddzielnymi metodami (bez wartości logicznej) nad jedną z wartością logiczną - argumentem YAGNI jest to, że wartość logiczna wprowadza niewykorzystaną / niepotrzebną złożoność. Nawet jeśli niektórzy wywołujący przekazują zmienne, nadal mogę udostępnić (boolowskie) wersje bez parametrów do użycia dla wywołujących przekazujących stałą - jest to prostsze, co jest dobre.
Erik Eidt
18

Chociaż zgadzam się, że idealnym rozwiązaniem jest użycie funkcji językowej w celu wymuszenia zarówno czytelności, jak i bezpieczeństwa wartości, możesz również zdecydować się na praktyczne podejście: komentarze w czasie rozmowy. Lubić:

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

lub:

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

lub (słusznie, jak sugeruje Frax):

UpdateRow(item, /* externalCall */true);
biskup
źródło
1
Nie ma powodu do głosowania. To jest całkowicie poprawna sugestia.
Suncat2000
Doceń <3 @ Suncat2000!
biskup
11
(Prawdopodobnie preferowanym) wariantem jest po prostu umieszczenie tam prostej nazwy argumentu UpdateRow(item, /* externalCall */ true ). Komentarz w pełnym zdaniu jest znacznie trudniejszy do przeanalizowania, w rzeczywistości jest to głównie szum (szczególnie drugi wariant, który również jest bardzo luźno powiązany z argumentem).
Frax
1
To zdecydowanie najbardziej odpowiednia odpowiedź. Właśnie do tego przeznaczone są komentarze.
Sentinel,
9
Nie jest wielkim fanem takich komentarzy. Komentarze mają skłonność do starzenia się, jeśli pozostaną nietknięte podczas refaktoryzacji lub wprowadzania innych zmian. Nie wspominając już o tym, jak tylko pojawi się więcej niż jeden parametr bool, szybko się to dezorientuje.
John V.
11
  1. Możesz „nazwać” swoje boole. Poniżej znajduje się przykład języka OO (gdzie można go wyrazić w klasie zapewniającej UpdateRow()), jednak samą koncepcję można zastosować w dowolnym języku:

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

    i na stronie połączenia:

    UpdateRow(item, Table::WithExternalCall);
    
  2. Uważam, że pozycja nr 1 jest lepsza, ale nie zmusza użytkownika do korzystania z nowych zmiennych podczas korzystania z funkcji. Jeśli bezpieczeństwo typu jest dla Ciebie ważne, możesz utworzyć enumtyp i UpdateRow()zaakceptować to zamiast bool:

    UpdateRow(var item, ExternalCallAvailability ec);

  3. Możesz zmodyfikować nazwę funkcji, aby lepiej odzwierciedlała znaczenie boolparametru. Nie jestem pewien, ale może:

    UpdateRowWithExternalCall(var item, bool externalCall)

simurg
źródło
8
Nie podoba mi się # 3, jak teraz, jeśli wywołasz funkcję externalCall=false, jej nazwa nie ma absolutnie żadnego sensu. Reszta twojej odpowiedzi jest dobra.
Wyścigi lekkości na orbicie
Zgoda. Nie byłem bardzo pewien, ale może to być opłacalna opcja dla innych funkcji. I
simurg
@LightnessRacesinOrbit Rzeczywiście. Bezpieczniej jest iść UpdateRowWithUsuallyExternalButPossiblyInternalCall.
Eric Duminil
@EricDuminil: Spot on 😂
Lightness Races in Orbit
10

Inną opcją, której jeszcze tu nie czytałem, jest: Użyj nowoczesnego IDE.

Na przykład IntelliJ IDEA drukuje nazwę zmiennej w metodzie, którą wywołujesz, jeśli przekazujesz literał taki jak truelub nulllub username + “@company.com. Odbywa się to małą czcionką, dzięki czemu nie zajmuje dużo miejsca na ekranie i wygląda bardzo różnie od rzeczywistego kodu.

Nadal nie twierdzę, że dobrym pomysłem jest wrzucanie boolczyków wszędzie. Argument mówiący, że czytasz kod znacznie częściej niż piszesz, jest często bardzo silny, ale w tym konkretnym przypadku w dużej mierze zależy on od technologii, której używasz (i twoi koledzy!) Do wspierania twojej pracy. Z IDE jest to o wiele mniejszy problem niż na przykład z vimem.

Zmodyfikowany przykład części mojej konfiguracji testowej: wprowadź opis zdjęcia tutaj

Sebastiaan van den Broek
źródło
5
Byłoby to prawdą tylko wtedy, gdyby wszyscy w twoim zespole korzystali z IDE tylko do odczytu twojego kodu. Niezależnie od rozpowszechnienia edytorów spoza IDE, masz również narzędzia do przeglądania kodu, narzędzia kontroli źródła, pytania dotyczące przepełnienia stosu itp., I niezwykle ważne jest, aby kod był czytelny nawet w tych kontekstach.
Daniel Pryden
@DanielPryden na pewno, stąd mój komentarz „i twoi koledzy”. W firmach często stosuje się standardowe IDE. Jeśli chodzi o inne narzędzia, argumentowałbym, że jest całkowicie możliwe, że tego rodzaju narzędzia również obsługują to lub będą w przyszłości, lub że te argumenty po prostu nie mają zastosowania (jak w przypadku zespołu programującego pary 2 osób)
Sebastiaan van den Broek
2
@Sebastiaan: Chyba się nie zgadzam. Nawet jako solo programista regularnie czytam swój własny kod w Git diffs. Git nigdy nie będzie w stanie wykonać takiej samej wizualizacji kontekstowej, jaką może wykonać IDE, ani nie powinien. Jestem zwolennikiem używania dobrego IDE, aby pomóc Ci napisać kod, ale nigdy nie powinieneś używać IDE jako wymówki do pisania kodu, którego nie napisałbyś bez niego.
Daniel Pryden
1
@DanielPryden Nie zalecam pisania wyjątkowo niechlujnego kodu. To nie jest sytuacja czarno-biała. Mogą zdarzyć się przypadki, w których w przeszłości dla konkretnej sytuacji byłoby 40% za pisaniem funkcji z wartością logiczną, a 60% nie, a ty w końcu tego nie robiłeś. Może teraz przy dobrym wsparciu IDE saldo wynosi 55%, a pisze 45%, a nie. I tak, czasami może być konieczne przeczytanie go poza IDE, więc nie jest idealny. Ale nadal jest to ważny kompromis w stosunku do alternatywy, takiej jak dodanie innej metody lub wyliczenie w celu wyjaśnienia kodu inaczej.
Sebastiaan van den Broek
6

2 dni i nikt nie wspomniał o polimorfizmie?

target.UpdateRow(item);

Kiedy jestem klientem, który chce zaktualizować wiersz o element, nie chcę myśleć o nazwie bazy danych, adresie URL mikro-usługi, protokole używanym do komunikacji ani o tym, czy połączenie zewnętrzne jest trzeba będzie użyć, aby tak się stało. Przestańcie narzucać mi te szczegóły. Po prostu weź mój przedmiot i idź już gdzieś zaktualizować wiersz.

Zrób to, a stanie się to częścią problemu konstrukcyjnego. Można to rozwiązać na wiele sposobów. Tu jest jeden:

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

Jeśli patrzysz na to i myślisz „Ale mój język wymienia argumenty, nie potrzebuję tego”. Dobrze, świetnie, idź ich użyć. Po prostu trzymaj ten nonsens z dala od dzwoniącego klienta, który nie powinien nawet wiedzieć, z czym rozmawia.

Należy zauważyć, że jest to więcej niż problem semantyczny. To także problem projektowy. Podaj wartość logiczną, a będziesz musiał użyć rozgałęzienia, aby sobie z tym poradzić. Niezbyt zorientowany obiektowo. Masz dwa lub więcej booleanów do przejścia? Czy chcesz, aby Twój język miał wiele wysyłek? Sprawdź, co mogą zrobić kolekcje po ich zagnieżdżeniu. Odpowiednia struktura danych może znacznie ułatwić Ci życie.

candied_orange
źródło
2

Jeśli możesz zmodyfikować updateRowfunkcję, być może uda ci się przekształcić ją w dwie funkcje. Biorąc pod uwagę, że funkcja przyjmuje parametr boolowski, podejrzewam, że wygląda to tak:

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

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

To może być trochę zapachowy kod. Funkcja może mieć zupełnie inne zachowanie w zależności od ustawienia externalCallzmiennej, w którym to przypadku ma dwie różne obowiązki. Przekształcenie go w dwie funkcje, które mają tylko jedną odpowiedzialność, może poprawić czytelność:

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

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

Teraz, gdziekolwiek wywołasz te funkcje, możesz od razu stwierdzić, czy wywoływana jest usługa zewnętrzna.

Oczywiście nie zawsze tak jest. Jest to sytuacja i kwestia gustu. Przeformułowanie funkcji, która przyjmuje parametr logiczny na dwie funkcje, jest zwykle warte rozważenia, ale nie zawsze jest najlepszym wyborem.

Kevin
źródło
0

Jeśli UpdateRow znajduje się w kontrolowanej bazie kodu, rozważę wzorzec strategii:

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

Gdzie Request reprezentuje pewnego rodzaju DAO (oddzwanianie w trywialnym przypadku).

Prawdziwy przypadek:

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

Fałszywy przypadek:

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

Zwykle implementacja punktu końcowego byłaby skonfigurowana na wyższym poziomie abstrakcji i po prostu użyłbym jej tutaj. Jako przydatny darmowy efekt uboczny daje mi możliwość zaimplementowania innego sposobu dostępu do danych zdalnych, bez żadnych zmian w kodzie:

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

Zasadniczo taka inwersja kontroli zapewnia niższe sprzężenie między przechowywaniem danych a modelem.

Basilevs
źródło