Co definiuje solidny kod?

42

Mój profesor ciągle powołuje się na ten przykład Java, kiedy mówi o „solidnym” kodzie:

if (var == true) {
    ...
} else if (var == false) {
    ...
} else {
    ...
}

Twierdzi, że „solidny kod” oznacza, że ​​twój program bierze pod uwagę wszystkie możliwości i że nie ma czegoś takiego jak błąd - wszystkie sytuacje są obsługiwane przez kod i skutkują prawidłowym stanem, stąd „inne”.

Wątpię jednak. Jeśli zmienna jest wartością logiczną, jaki jest sens sprawdzania stanu trzeciego, gdy stan trzeci jest logicznie niemożliwy?

„Brak czegoś takiego jak błąd” również wydaje się śmieszny; nawet aplikacje Google wyświetlają błędy bezpośrednio użytkownikowi, zamiast połykać je po cichu lub w jakiś sposób uważać je za prawidłowy stan. I to dobrze - lubię wiedzieć, kiedy coś pójdzie nie tak. Wydaje się, że twierdzenie, iż aplikacja nigdy nie zawierałaby żadnych błędów, wydaje się słuszne.

Jaka jest zatem rzeczywista definicja „solidnego kodu”?

Lotus Notes
źródło
4
Odbywałoby się to tylko w języku, który nie jest typowo pisany. W silnie typowanym języku zmienna typu boolean (nie jakaś liczba całkowita udająca boolean) może być tylko prawdą lub fałszem, nie ma trzeciej opcji ...
Marjan Venema
23
zapytaj go, jak przetestowałbyś zasięg w przypadku 3. przypadku, ponieważ solidny kod z pewnością wymagałby przetestowania, a jeśli nie zdołasz przetestować 3. przypadku, nie będziesz w stanie znaleźć żadnych błędów, które mogą się w nim czaić.
gbjbaanb
2
@Marjan - w niezbyt typowym języku najprawdopodobniej po prostu napiszesz: if (var) {} else {}
kevin cline
2
Nie znam żadnych języków, w których zarówno x, jak i x mogłyby być prawdziwe. Zauważ, że nie zasugerowałem „if (x == true) ...”; Nie znoszę takich porównań.
kevin cline

Odpowiedzi:

33

jaki jest sens sprawdzania stanu trzeciego, gdy jest ono logicznie niemożliwe?

Co z Boolean?tym, że pozwala na NULLstan, który nie jest ani prawdziwy, ani fałszywy. Co teraz powinno zrobić oprogramowanie? Niektóre programy muszą być bardzo odporne na awarie, takie jak rozruszniki serca. Czy kiedykolwiek widziałeś, jak ktoś dodaje kolumnę do bazy danych, która była Booleani początkowo inicjuje bieżące dane NULL? Wiem, że to widziałem.

Oto kilka linków, które omawiają, co to znaczy być solidnym pod względem oprogramowania:

Jeśli uważasz, że istnieje jedna powszechnie uzgodniona definicja „solidnego” tutaj, powodzenia. Mogą istnieć synonimy, takie jak dowód na bomby lub idiotyzm. Programator taśm Duct byłby przykładem kogoś, kto zwykle pisze solidny kod, przynajmniej w moim rozumieniu terminów.

JB King
źródło
13
Jeśli byłby to boolean zerowy, zarówno Java, jak i c # by wyrzucały, więc najpierw należy sprawdzić null.
Esben Skov Pedersen
Wydaje się, że nie ma powszechnie uzgodnionej definicji tego, czym jest kot lub pies.
Tulains Córdova
11

Ze względu na moją dyskusję Bool może mieć 2 stany, Prawda lub Fałsz. Wszystko inne jest niezgodne ze specyfikacją języka programowania. Jeśli Twój łańcuch narzędzi jest niezgodny ze specyfikacją, nie ma znaczenia, co robisz. Jeśli programista stworzył typ Bool, który miał więcej niż 2 stany, jest to ostatnia rzecz, jaką zrobiłby na mojej bazie kodu.

Opcja A.

if (var == true) {
    ...
} else if (var == false) {
    ...
} else {
    ...
}

Opcja B

if (var == true) {
    ...
} else {
    ...
}

Twierdzę, że opcja B jest bardziej niezawodna .....

Każdy twit może powiedzieć ci, żebyś obsługiwał nieoczekiwane błędy. Zazwyczaj są one bardzo łatwe do wykrycia, kiedy o nich pomyślisz. Przykład podany przez twojego profesora nie jest czymś, co mogłoby się zdarzyć, więc jest to bardzo słaby przykład.

Niemożliwe jest przetestowanie bez skomplikowanych wiązek testowych. Jeśli nie możesz go utworzyć, jak zamierzasz go przetestować? Jeśli nie przetestowałeś kodu, skąd wiesz, że on działa? Jeśli nie wiesz, że to działa, to nie piszesz solidnego oprogramowania. Myślę, że wciąż nazywają to Catch22 (Świetny film, obejrzyj go kiedyś).

Opcja B jest trywialna do przetestowania.

Następny problem, zadaj profesorowi to pytanie: „Co chcesz, abym to zrobił, jeśli Boolean nie jest ani Prawda, ani Fałsz?” To powinno doprowadzić do bardzo interesującej dyskusji .....

W większości przypadków zrzut rdzenia jest odpowiedni, w najgorszym przypadku drażni użytkownika lub kosztuje dużo pieniędzy. Co jeśli, powiedzmy, moduł jest systemem obliczania ponownych prób w czasie rzeczywistym? Każda odpowiedź, bez względu na to, jak niedokładna, nie może być gorsza niż przerwanie, które zabije użytkowników. Co więc zrobić, jeśli wiesz, że odpowiedź może być zła, wybierz 50/50 lub przerwij i przejdź do 100% awarii. Gdybym był członkiem załogi, wziąłbym 50/50.

Opcja A zabija mnie Opcja B daje mi równą szansę na przeżycie.

Ale poczekaj - to symulacja ponownego wejścia promu kosmicznego - co wtedy? Przerwij, abyś wiedział o tym. Brzmi jak dobry pomysł? - NIE - ponieważ musisz przetestować za pomocą kodu, który planujesz wysłać.

Opcja A jest lepsza do uproszczenia, ale nie można jej wdrożyć. Jest to bezużyteczna opcja B to wdrożony kod, więc symulacja działa tak samo, jak w systemach na żywo.

Powiedzmy, że to był ważny problem. Lepszym rozwiązaniem byłoby odizolowanie obsługi błędów od logiki aplikacji.

if (var != true || var != false) {
    errorReport("Hell just froze over, var must be true or false")
}
......
if (var == true){
 .... 
} else {
 .... 
}

Dalsze czytanie - maszyna Therac-25 Xray, awaria rakiety Ariane 5 i inne (Link ma wiele uszkodzonych linków, ale wystarczająca ilość informacji, które pomoże Google)

mattnz
źródło
1
„… nieoczekiwane błędy. Zwykle są one łatwe do wykrycia, gdy tylko o nich pomyślisz” - ale kiedy o nich pomyślisz, nie są już nieoczekiwane.
gbjbaanb
7
Istnieje pytanie, czy zamiast tego if (var != true || var != false) {należy użyć kodu &&.
1
Mogę łatwo wymyślić bool, który nie jest ani prawdziwy, ani fałszywy, ale wciąż jest nieoczekiwany. Jeśli powiesz, że bool nie może być niczym innym, jeśli sprawdzę, czy litera jest cyfrą, a następnie przekonwertuję ją na wartość całkowitą, łatwo mogę pomyśleć, że ta liczba całkowita jest mniejsza niż 0 lub większa niż 9, ale nadal jest niespodziewany.
gnasher729
1
Null Booleans są obsługiwane w Javie i C # i mają aplikację w świecie rzeczywistym. Rozważ bazę danych zawierającą listę osób. Po chwili zdecydujesz, że potrzebujesz pola płci (isMale). Null oznacza „nigdy nie pytaj, więc nie wiem”; prawda oznacza mężczyznę, a fałsz oznacza kobietę. (OK, dla uproszczenia pominięto płeć).
kiwiron
@kiwiron: Lepszym rozwiązaniem nie byłoby użycie typu wyliczenia „Mężczyzna”, „Kobieta”, „Nie pytałem”. Wyliczenia są lepsze - można je rozszerzyć, gdy zajdzie taka potrzeba (w twoim umyśle np. Asexual, Hermaphrodite, „odmowa odpowiedzi”).
mattnz
9

W rzeczywistości twój kod nie jest bardziej niezawodny, ale mniej odporny. Ostatecznym elsejest po prostu martwy kod, którego nie można przetestować.

W krytycznym oprogramowaniu, takim jak statki kosmiczne, martwy kod i, ogólnie rzecz biorąc, nieprzetestowany kod jest zabroniony: jeśli promień kosmiczny powoduje zdenerwowanie pojedynczego zdarzenia, które z kolei powoduje aktywację martwego kodu, wszystko jest możliwe. Jeśli SEU aktywuje część solidnego kodu, (nieoczekiwane) zachowanie pozostaje pod kontrolą.

mouviciel
źródło
Nie dostaję go do statków kosmicznych, czy kod martwy jest zabroniony? tzn. nie możesz napisać ostatniej rzeczy? skoro nie możesz tego przetestować, nie możesz tego włożyć? ale co to znaczy „Jeśli SEU aktywuje część solidnego kodu, (nieoczekiwane) zachowanie pozostaje pod kontrolą”.
wytarty
5
Tak, w testach oprogramowania o znaczeniu krytycznym musi być 100%, w związku z czym nieosiągalny kod (inaczej martwy kod) jest zabroniony.
mouviciel
7

Myślę, że profesor może mylić „błąd” i „błąd”. Solidny kod z pewnością powinien zawierać kilka błędów / żadnych błędów. Solidny kod może i we wrogim środowisku musi mieć dobre zarządzanie błędami (może to być obsługa wyjątków lub rygorystyczne testy statusu zwrotu).

Zgadzam się, że przykład kodu profesora jest głupi, ale nie tak głupi jak mój.

// Assign 3 to x
var x = 3;
x = 3;   // again, just for sure
while (x < 3 or x > 3) { x = 3; }  // being robust
if (x != 3) { ... }  // this got to be an error!
David Andersson
źródło
1
Ostatni, jeśli na pewno zostanie uruchomiony, nie wymagałby tak wiele wysiłku. Każdy doświadczony programista C widział, jak wartości nagle się zmieniają. Oczywiście logicznie, w kontrolowanym środowisku jednowątkowym nigdy nie powinno się to zdarzyć. W prawdziwym życiu kod wewnątrz if ostatecznie się wydarzy. Jeśli nie ma nic przydatnego, co możesz zrobić, to nie koduj tego! (Miałem zabawne doświadczenie podczas tworzenia konkretnego oprogramowania, w którym podniosłem wyjątek przekleństwami na wypadek, gdyby stało się coś niemożliwego ... zgadnij, co się stało?).
Alex
2
Prawdziwa historia:boolean x = something(); if (x) { x = True // make sure it's really true, ... }
Andres F.,
6

Nie ma uzgodnionej definicji Robust Code , ponieważ w wielu kwestiach programistycznych jest on mniej więcej subiektywny ...

Przykład podany przez profesora zależy od języka:

  • W Haskell Booleanmoże być albo, Truealbo Falsenie ma trzeciej opcji
  • W C ++ boolmoże być true, falselub (niestety) pochodzić od wątpliwej obsady, która umieściła go w nieznanym przypadku ... To nie powinno się zdarzyć, ale może, w wyniku poprzedniego błędu.

Jednak to, co radzi twój profesor, zaciemnia kod, wprowadzając zewnętrzną logikę zdarzeń, które nie powinny się zdarzyć , w środku programu głównego, więc zamiast tego wskażę ci programowanie defensywne .

W przypadku uniwersytetu możesz go nawet rozszerzyć, przyjmując strategię Design By Contract:

  • Ustal niezmienniki dla klas (np. sizeJest liczbą pozycji na dataliście)
  • Ustal warunki wstępne i dodatkowe dla każdej funkcji (np. Tę funkcję można wywołać tylko wtedy, gdy ajest mniejsza niż 10)
  • Przetestuj każdą z nich w punktach wejścia i wyjścia każdej z funkcji

Przykład:

class List:
  def __init__(self, items):
    self.__size = len(items)
    self.__data = items

  def __invariant(self):
    assert self.__size == len(self.__data)

  def size(self):
    self.__invariant()

    return self.__size

  def at(self, index):
    """index should be in [0,size)"""
    self.__invariant()
    assert index >= 0 and index < self.__size

    return self.__data[index]

  def pushback(self, item):
    """the subsequent list is one item longer
       the item can be retrieved by self.at(self.size()-1)"""
    self.__invariant()

    self.__data.append(item)
    self.__size += 1

    self.__invariant()
    assert self.at(self.size()-1) == item
Matthieu M.
źródło
Ale profesor wyraźnie powiedział, że to Java, a konkretnie NIE powiedział, jaki jest typ var. Jeśli jest to wartość logiczna, może mieć wartość true, false lub null. Jeśli coś innego, może być nierówne zarówno dla prawdy, jak i nierówne dla fałszu. Tak, pokrywają się mocne, defensywne i paranoiczne.
Andy Canfield
2
W C, C ++ i Objective-C bool może mieć nieokreśloną wartość, jak każdy inny typ, ale każde przypisanie ustawi na true lub false i nic więcej. Na przykład: bool b = 0; b ++; b ++; ustawi b na wartość true.
gnasher729
2

Podejście twojego profesora jest całkowicie błędne.

Funkcja lub odrobina kodu powinna mieć specyfikację określającą jej działanie, która powinna obejmować wszystkie możliwe dane wejściowe. Kod należy napisać, aby jego zachowanie gwarantowało dopasowanie do specyfikacji. W tym przykładzie napisałbym specyfikację dość prostą:

Spec: If var is false then the function does "this", otherwise it does "that". 

Następnie piszesz funkcję:

if (var == false) dothis; else dothat; 

a kod spełnia specyfikację. Więc twój profesor mówi: co jeśli var == 42? Spójrz na specyfikację: mówi, że funkcja powinna zrobić to „to”. Spójrz na kod: Funkcja wykonuje to „to”. Funkcja spełnia specyfikację.

Tam, gdzie kod twojego profesora sprawia, że ​​wszystko jest całkowicie nieufne, to fakt, że przy jego podejściu, gdy var nie jest ani prawdą, ani fałszem, wykona kod, który nigdy wcześniej nie był wywoływany i który jest całkowicie nieprzetestowany, z całkowicie nieprzewidywalnymi rezultatami.

gnasher729
źródło
1

Zgadzam się ze stwierdzeniem @ gnasher729: podejście twojego profesora jest całkowicie błędne.

Solidny oznacza, że ​​jest odporny na uszkodzenia / awarie, ponieważ nie ma wielu założeń i jest oddzielony: jest samodzielny, samokreślący się i przenośny. Obejmuje to również możliwość dostosowania do zmieniających się wymagań. Jednym słowem, twój kod jest trwały .

Generalnie przekłada się to na krótkie funkcje, które pobierają dane z parametrów przekazywanych przez program wywołujący, oraz użycie publicznych interfejsów dla konsumentów - metod abstrakcyjnych, opakowań, pośredniczenia, interfejsów typu COM itp. - zamiast funkcji zawierających konkretny kod implementacyjny.

Wektor
źródło
0

Solidny kod to po prostu kod, który dobrze radzi sobie z awariami. Nie więcej nie mniej.

W przypadku awarii istnieje wiele typów: niepoprawny kod, niekompletny kod, nieoczekiwane wartości, nieoczekiwane stany, wyjątki, wyczerpanie zasobów, ... Solidny kod dobrze sobie z nimi radzi.

iskrzący
źródło
0

Rozważę kod, który podałeś jako przykład programowania obronnego (przynajmniej tak, jak używam tego terminu). Częścią programowania obronnego jest dokonywanie wyborów, które minimalizują założenia dotyczące zachowania reszty systemu. Na przykład, który z nich jest lepszy:

for (int i = 0; i != sequence.length(); ++i) {
    // do something with sequence[i]
}

Lub:

for (int i = 0; i < sequence.length(); ++i) {
    // do something with sequence[i]
}

(W przypadku problemów z zauważeniem różnicy sprawdź test pętli: pierwsze użycie !=, drugie użycie <).

Teraz, w większości przypadków, obie pętle będą zachowywać się dokładnie tak samo. Jednak pierwsze (w porównaniu z !=) przyjmuje założenie, że ibędzie zwiększane tylko raz na iterację. Jeśli pominie wartość, sequence.length()wówczas pętla może kontynuować się poza granice sekwencji i spowodować błąd.

Można zatem argumentować, że druga implementacja jest bardziej niezawodna: nie zależy ona od założeń dotyczących zmiany treści pętli i(uwaga: w rzeczywistości nadal przyjmuje założenie, które inigdy nie jest ujemne).

Aby uzasadnić, dlaczego nie chcesz przyjąć takiego założenia, wyobraź sobie, że pętla skanuje ciąg znaków i wykonuje pewne przetwarzanie tekstu. Piszecie pętlę i wszystko jest w porządku. Teraz zmieniają się twoje wymagania i decydujesz, że musisz obsługiwać znaki zmiany znaczenia w ciągu tekstowym, więc zmieniasz treść pętli tak, że jeśli wykryje znak zmiany znaczenia (powiedzmy, odwrotny ukośnik), zwiększa się, iaby pominąć znak bezpośrednio po wyjściu. Teraz w pierwszej pętli występuje błąd, ponieważ jeśli ostatnim znakiem tekstu jest ukośnik odwrotny, treść pętli będzie się zwiększać, ia pętla będzie kontynuowana poza końcem sekwencji.

John Bartłomiej
źródło
-1

Osobiście opisuję kod jako „solidny”, który ma ten jeden, ważne atrybuty:

  1. Jeśli moja mama siedzi przed nim i pracuje z nim, nie może zepsuć systemu

Teraz przez przerwę mam na myśli albo doprowadzenie systemu do niestabilnego stanu, albo spowodowanie wyjątku UNHANDLED . Wiesz, czasami dla prostej koncepcji, możesz zrobić złożoną definicję i wyjaśnienie. Ale wolałbym proste definicje. Użytkownicy są całkiem dobrzy w znajdowaniu solidnych aplikacji. Jeśli użytkownik aplikacji wyśle ​​Ci wiele żądań dotyczących błędów, utraty stanu, nieintuicyjnych przepływów pracy itp., Oznacza to, że coś jest nie tak z twoim programowaniem.

Saeed Neamati
źródło