Czy powinienem teraz dodać zbędny kod na wypadek, gdyby był potrzebny w przyszłości?

114

Słusznie lub nie, jestem obecnie przekonany, że zawsze powinienem starać się, aby mój kod był jak najbardziej niezawodny, nawet jeśli oznacza to dodanie zbędnego kodu / czeków, o których wiem, że nie będą w tej chwili przydatne, ale one może być x ilość lat w dół linii.

Na przykład obecnie pracuję nad aplikacją mobilną, która zawiera ten fragment kodu:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
    }

    //2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
    if(rows.Count == 0)
    {
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

Patrząc konkretnie na drugą część, wiem, że jeśli pierwsza sekcja jest prawdziwa, to druga sekcja również będzie prawdziwa. Nie mogę wymyślić żadnego powodu, dla którego sekcja pierwsza byłaby fałszywa, a sekcja druga oceniała jako prawdę, co sprawia, że ​​drugie ifzdanie jest zbędne.

Jednak w przyszłości może się zdarzyć, że to drugie ifstwierdzenie jest rzeczywiście potrzebne i ze znanego powodu.

Niektórzy ludzie mogą na to spojrzeć początkowo i myślą, że programuję z myślą o przyszłości, co oczywiście jest dobrą rzeczą. Znam jednak kilka przypadków, w których ten rodzaj kodu „ukrył” przede mną błędy. Oznacza to, że jeszcze dłużej zajęło mi ustalenie, dlaczego funkcja xyzdziała, abckiedy powinna def.

Z drugiej strony istniało także wiele przypadków, w których ten rodzaj kodu znacznie, znacznie ułatwia ulepszanie kodu za pomocą nowych zachowań, ponieważ nie muszę wracać i zapewniać, że wszystkie odpowiednie kontrole są na miejscu.

Czy istnieją jakieś ogólne zasada-of-kciuk wytyczne dla tego rodzaju kodu? (Chciałbym również dowiedzieć się, czy byłoby to uważane za dobrą czy złą praktykę?)

Uwaga: Można to uznać za podobne do tego pytania, jednak w przeciwieństwie do tego pytania, chciałbym uzyskać odpowiedź, zakładając, że nie ma żadnych terminów.

TLDR: Czy powinienem posunąć się do dodawania zbędnego kodu, aby uczynić go potencjalnie bardziej niezawodnym w przyszłości?

KidCode
źródło
95
W rozwoju oprogramowania rzeczy, które nigdy nie powinny się zdarzyć, muszą mieć miejsce cały czas.
Roman Reiner
34
Jeśli wiesz, że if(rows.Count == 0)to się nigdy nie wydarzy, możesz wtedy podnieść wyjątek - i sprawdź, dlaczego twoje założenie się nie spełniło.
knut
9
Nie ma znaczenia dla pytania, ale podejrzewam, że Twój kod zawiera błąd. Gdy wiersze są puste, tworzona jest nowa lista i (zgaduję) wyrzucona. Ale gdy wiersze nie są puste, istniejąca lista jest zmieniana. Lepszym rozwiązaniem może być naleganie, aby klient przekazał listę, która może być pusta.
Theodore Norvell,
9
Dlaczego miałby rowsbyć zawsze zerowy? Nigdy nie ma dobrego powodu, przynajmniej w .NET, aby kolekcja była pusta. Puste , jasne, ale nie puste . Rzuciłbym wyjątek, jeśli rowsjest pusty, ponieważ oznacza to, że dzwoniący ma przerwę w logice.
Kyralessa

Odpowiedzi:

176

Jako ćwiczenie najpierw zweryfikujmy logikę. Chociaż, jak zobaczymy, masz większe problemy niż jakikolwiek problem logiczny.

Wywołaj pierwszy warunek A i drugi warunek B.

Najpierw mówisz:

Patrząc konkretnie na drugą część, wiem, że jeśli pierwsza sekcja jest prawdziwa, to druga sekcja również będzie prawdziwa.

To znaczy: A implikuje B, lub, w bardziej podstawowych terminach (NOT A) OR B

I wtedy:

Nie mogę wymyślić żadnego powodu, dla którego sekcja pierwsza byłaby fałszywa, a sekcja druga oceniała jako prawdę, co czyni drugą instrukcję if zbędną.

Czyli: NOT((NOT A) AND B). Zastosuj prawo Demorgan, aby uzyskać, (NOT B) OR Aco B oznacza A.

Dlatego jeśli oba stwierdzenia są prawdziwe, A oznacza B, a B oznacza A, co oznacza, że ​​muszą być równe.

Dlatego tak, kontrole są zbędne. Wygląda na to, że masz cztery ścieżki kodu w programie, ale w rzeczywistości masz tylko dwie.

Więc teraz pytanie brzmi: jak napisać kod? Prawdziwe pytanie brzmi: jaka jest podana umowa metody ? Pytanie, czy warunki są zbędne, to czerwony śledź. Prawdziwe pytanie brzmi: „Czy zaprojektowałem rozsądną umowę i czy moja metoda wyraźnie ją realizuje?”

Spójrzmy na deklarację:

public static CalendarRow AssignAppointmentToRow(
    Appointment app,    
    List<CalendarRow> rows)

Jest publiczny, więc musi być odporny na złe dane od dowolnych dzwoniących.

Zwraca wartość, więc powinna być użyteczna dla wartości zwracanej, a nie dla efektu ubocznego.

A jednak nazwa metody jest czasownikiem, co sugeruje, że jest przydatna ze względu na efekt uboczny.

Umowa parametru listy wynosi:

  • Lista zerowa jest OK
  • Lista zawierająca jeden lub więcej elementów jest OK
  • Lista bez elementów jest niepoprawna i nie powinna być możliwa.

Ta umowa jest szalona . Wyobraź sobie pisanie dokumentacji na ten temat! Wyobraź sobie pisanie przypadków testowych!

Moja rada: zacznij od nowa. Ten interfejs API ma napisany interfejs maszyny do cukierków. (Wyrażenie pochodzi ze starej opowieści o automatach do cukierków w Microsoft, gdzie zarówno ceny, jak i wybory są liczbami dwucyfrowymi, i bardzo łatwo jest wpisać „85”, czyli cenę przedmiotu 75, a otrzymasz zła rzecz. Ciekawostka: tak, właśnie to zrobiłem przez pomyłkę, gdy próbowałem wydobyć gumę z automatu w Microsoft!)

Oto jak zaprojektować rozsądną umowę:

Spraw, by twoja metoda była użyteczna ze względu na efekt uboczny lub wartość zwracaną, a nie oba.

Nie akceptuj typów zmiennych jako danych wejściowych, takich jak listy; jeśli potrzebujesz sekwencji informacji, weź IEnumerable. Czytaj tylko sekwencję; nie pisz do przekazanej kolekcji, chyba że jest bardzo jasne, że jest to umowa metody. Biorąc IEnumerable, wysyłasz wiadomość do dzwoniącego, że nie zamierzasz mutować jego kolekcji.

Nigdy nie akceptuj zer; sekwencja zerowa jest obrzydliwością. Wymagaj od dzwoniącego przekazania pustej sekwencji, jeśli ma to sens, nigdy nie ma wartości zerowej.

Awaria natychmiast, jeśli dzwoniący naruszy umowę, aby nauczyć go, że masz na myśli biznes, i aby złapali błędy podczas testowania, a nie produkcji.

Zaprojektuj umowę, aby była jak najbardziej rozsądna, a następnie jasno ją zrealizuj. To sposób na przyszłość.

Teraz mówiłem tylko o twojej konkretnej sprawie i zadałeś ogólne pytanie. Oto kilka dodatkowych ogólnych porad:

  • Jeśli istnieje fakt, że jako deweloper możesz wywnioskować, ale kompilator nie może tego zrobić, użyj asercji, aby udokumentować ten fakt. Jeśli inny programista, taki jak przyszły ty lub jeden z twoich współpracowników, naruszy to założenie, wówczas stwierdzenie to ci powie.

  • Uzyskaj narzędzie pokrycia testowego. Upewnij się, że testy obejmują każdą linię kodu. Jeśli jest odkryty kod, oznacza to, że masz brakujący test lub martwy kod. Martwy kod jest zaskakująco niebezpieczny, ponieważ zazwyczaj nie jest przeznaczony do martwego kodu! Niesamowicie okropna luka w zabezpieczeniach Apple „nie udało się” kilka lat temu przychodzi mi na myśl.

  • Uzyskaj narzędzie do analizy statycznej. Heck, zdobądź kilka; każde narzędzie ma swoją szczególną specjalność i żadne nie jest nadzbiorem innych. Zwróć uwagę, kiedy mówi ci, że jest nieosiągalny lub nadmiarowy kod. Ponownie, są to prawdopodobnie błędy.

Jeśli brzmi to tak, jak mówię: po pierwsze, dobrze zaprojektuj kod, a po drugie, sprawdź, czy jest poprawny, no cóż, tak właśnie mówię. Takie postępowanie znacznie ułatwi radzenie sobie z przyszłością; najtrudniejszą częścią przyszłości jest radzenie sobie z całym błędnym kodem maszynowym cukierków, który ludzie pisali w przeszłości. Zrób to już dziś, a koszty będą w przyszłości niższe.

Eric Lippert
źródło
24
Nigdy tak naprawdę nie myślałem o takich metodach, myśląc o tym teraz, zawsze wydaje mi się, że staram się opisywać każdą ewentualność, kiedy w rzeczywistości, jeśli osoba / metoda wywołująca moją metodę nie przekaże mi tego, czego potrzebuję, to nie powinnam próbując naprawić swoje błędy (kiedy tak naprawdę nie wiem, co zamierzali), powinienem się po prostu rozerwać. Dzięki za to wyciągnięto cenną lekcję!
KidCode
4
Fakt, że metoda zwraca wartość, nie oznacza, że ​​nie powinna być również użyteczna ze względu na efekt uboczny. W współbieżnym kodzie zdolność funkcji do zwracania obu jest często bardzo ważna (wyobraź sobie CompareExchangebez takiej możliwości!), A nawet w scenariuszach niesąsiadujących coś takiego jak „dodaj rekord, jeśli nie istnieje, i zwróć przekazane dane record lub ten, który istniał ”może być wygodniejszy niż jakiekolwiek podejście, które nie wykorzystuje zarówno efektu ubocznego, jak i wartości zwrotnej.
supercat
5
@KidCode Tak, Eric naprawdę dobrze wyjaśnia rzeczy, nawet niektóre naprawdę skomplikowane tematy. :)
Mason Wheeler
3
@supercat Pewnie, ale trudniej jest też o tym myśleć. Najpierw prawdopodobnie będziesz chciał spojrzeć na coś, co nie modyfikuje stanu globalnego, unikając w ten sposób zarówno problemów z współbieżnością, jak i korupcją stanu. Jeśli nie jest to uzasadnione, nadal będziesz chciał oddzielić je od siebie - daje to jasne miejsca, w których współbieżność stanowi problem (a zatem może być uważany za wyjątkowo niebezpieczny), oraz miejsca, w których można sobie z tym poradzić. Był to jeden z głównych pomysłów w oryginalnym dokumencie OOP i jest podstawą użyteczności aktorów. Brak zasad religijnych - po prostu wolę oddzielać je od siebie, gdy ma to sens. Zwykle tak jest.
Luaan,
5
To bardzo przydatny post dla prawie wszystkich!
Adrian Buzea
89

To, co robisz w kodzie, który pokazujesz powyżej, to nie tyle korekta na przyszłość, co kodowanie defensywne .

Oba ifstwierdzenia sprawdzają różne rzeczy. Oba są odpowiednimi testami w zależności od potrzeb.

Sekcja 1 testuje i koryguje nullobiekt. Uwaga dodatkowa: Utworzenie listy nie tworzy żadnych elementów potomnych (np CalendarRow.).

Sekcja 2 testuje i koryguje błędy użytkownika i / lub wdrożenia. To, że masz, List<CalendarRow>nie oznacza, że ​​masz na liście jakieś elementy. Użytkownicy i realizatorzy będą robić rzeczy, których nie możesz sobie wyobrazić tylko dlatego, że mogą to zrobić, niezależnie od tego, czy ma to dla ciebie sens, czy nie.

Adam Zuckerman
źródło
1
Właściwie biorę „głupie sztuczki użytkownika”, by oznaczać wkład. TAK, nigdy nie powinieneś ufać wkładowi. Nawet spoza twojej klasy. Uprawomocnić! Jeśli uważasz, że to tylko przyszłe zmartwienie, chętnie cię dzisiaj zhakuję.
candied_orange
1
@CandiedOrange, taka była intencja, ale sformułowanie nie oddawało próby humoru. Zmieniłem brzmienie.
Adam Zuckerman
3
Krótkie pytanie tutaj, czy to błąd implementacji / złe dane, czy nie powinienem po prostu zawiesić się, zamiast próbować naprawić ich błędy?
KidCode
4
@KidCode Za każdym razem, gdy próbujesz odzyskać „naprawić ich błędy”, musisz zrobić dwie rzeczy, powrócić do znanego dobrego stanu i nie tracić po cichu cennych danych wejściowych. Zgodnie z tą zasadą w tym przypadku nasuwa się pytanie: czy lista zerowych wierszy jest wartościowym wkładem?
candied_orange 28.03.16
7
Nie zgadzaj się zdecydowanie z tą odpowiedzią. Jeśli funkcja otrzyma nieprawidłowe dane wejściowe, z definicji oznacza to błąd w programie. Prawidłowe podejście polega na zgłoszeniu wyjątku w przypadku nieprawidłowych danych wejściowych, dzięki czemu odkryjesz problem i naprawisz błąd. Podejście, które opisujesz, po prostu ukrywa błędy, co czyni je bardziej podstępnymi i trudniej je wyśledzić. Kodowanie obronne oznacza, że ​​nie ufasz automatycznie wejściom, ale je weryfikujesz, ale nie oznacza to, że powinieneś losowo zgadywać, aby „naprawić” nieprawidłowe lub nieoczekiwane dane wejściowe.
JacquesB
35

Myślę, że to pytanie w zasadzie zależy od gustu. Tak, dobrym pomysłem jest napisanie solidnego kodu, ale kod w twoim przykładzie jest lekkim naruszeniem zasady KISS (jak wiele takich „przyszłych kodów” będzie).

Osobiście prawdopodobnie nie zawracałbym sobie głowy sprawieniem, by kod był kuloodporny na przyszłość. Nie znam przyszłości i dlatego taki „przyszły kuloodporny” kod jest skazany na niepowodzenie, gdy i tak nadejdzie przyszłość.

Zamiast tego wolałbym inne podejście: sformułuj założenia, które wyrażasz wyraźnie, używając assert()makra lub podobnych obiektów. W ten sposób, gdy przyszłość nadejdzie za drzwiami, powie ci dokładnie, gdzie twoje założenia już się nie utrzymują.

cmaster
źródło
4
Podoba mi się to, że nie wiesz, co przyniesie przyszłość. W tej chwili wszystko, co naprawdę robię, to zgadywanie, co może być problemem, a następnie zgadywanie ponownie dla rozwiązania.
KidCode
3
@KidCode: Dobra obserwacja. Twoje własne myślenie tutaj jest w rzeczywistości dużo mądrzejsze niż wiele odpowiedzi tutaj, w tym ta, którą zaakceptowałeś.
JacquesB
1
Podoba mi się ta odpowiedź. Zachowaj minimalny kod, aby przyszły czytelnik mógł łatwo zrozumieć, dlaczego wymagane są jakiekolwiek kontrole. Jeśli przyszły czytelnik zobaczy czeki na rzeczy, które wyglądają niepotrzebnie, mogą tracić czas na próby zrozumienia, dlaczego czek jest dostępny. Przyszły człowiek może modyfikować tę klasę, a nie inni, którzy z niej korzystają. Również nie pisać kod, który nie potrafi zdiagnozować , co będzie miało miejsce, jeśli starają się obsłużyć przypadków, które nie mogą obecnie stało. (Chyba, że ​​napiszesz testy jednostkowe, które wykonują ścieżki kodu, których nie robi program główny).
Peter Cordes
23

Inną zasadą, o której warto pomyśleć, jest pomysł szybkiego zawierania . Chodzi o to, że gdy coś pójdzie nie tak w twoim programie, chcesz natychmiast zatrzymać program, przynajmniej podczas jego opracowywania przed jego wydaniem. Zgodnie z tą zasadą chcesz napisać wiele kontroli, aby upewnić się, że twoje założenia się utrzymują, ale poważnie zastanów się nad tym, aby Twój program zatrzymał się na swoich śladach, ilekroć założenia zostaną naruszone.

Mówiąc odważnie, jeśli w twoim programie jest choćby niewielki błąd, chcesz, aby całkowicie się zawiesił podczas oglądania!

Może się to wydawać sprzeczne z intuicją, ale pozwala wykryć błędy tak szybko, jak to możliwe podczas rutynowego programowania. Jeśli piszesz fragment kodu i uważasz, że jest on gotowy, ale zawiesza się podczas testowania, nie ma wątpliwości, że jeszcze nie skończyłeś. Ponadto większość języków programowania oferuje doskonałe narzędzia do debugowania, które są najłatwiejsze w użyciu, gdy program ulega awarii całkowicie, zamiast próbować zrobić wszystko po błędzie. Najczęstszym, najczęstszym przykładem jest to, że jeśli zawiesisz program, rzucając nieobsługiwany wyjątek, komunikat wyjątku powie ci niewiarygodną ilość informacji o błędzie, w tym o tym, który wiersz kodu zawiódł i ścieżkę przez kod, którą program obrał jego droga do tego wiersza kodu (ślad stosu).

Aby uzyskać więcej przemyśleń, przeczytaj ten krótki esej: Nie przybijaj programu do pozycji pionowej


Jest to istotne dla Ciebie, ponieważ możliwe jest, że czasami piszesz kontrole, ponieważ chcesz, aby Twój program próbował nadal działać, nawet jeśli coś poszło nie tak. Rozważmy na przykład krótką implementację sekwencji Fibonacciego:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

To działa, ale co, jeśli ktoś przekaże ujemną liczbę do Twojej funkcji? To nie zadziała! Dlatego warto dodać zaznaczenie, aby upewnić się, że funkcja jest wywoływana z nieujemnym wejściem.

Może być kuszące, aby napisać taką funkcję:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        n = 1; // Replace the negative input with an input that will work
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

Jednak jeśli to zrobisz, a następnie przypadkowo wywołaj funkcję Fibonacciego z negatywnym wejściem, nigdy nie zdasz sobie z tego sprawy! Co gorsza, Twój program prawdopodobnie będzie nadal działał, ale zacznie generować bezsensowne wyniki, nie dając żadnych wskazówek co do tego, gdzie coś poszło nie tak. To najtrudniejsze rodzaje błędów do naprawienia.

Zamiast tego lepiej napisać czek w ten sposób:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        throw new ArgumentException("Can't have negative inputs to Fibonacci");
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

Teraz, jeśli kiedykolwiek przypadkowo wywołasz funkcję Fibonacciego z ujemnym wejściem, twój program natychmiast się zatrzyma i poinformuje cię, że coś jest nie tak. Ponadto, podając ślad stosu, program poinformuje cię, która część twojego programu próbowała nieprawidłowo wykonać funkcję Fibonacciego, dając doskonały punkt wyjścia do debugowania tego, co jest nie tak.

Kevin
źródło
1
Czy c # nie ma szczególnego rodzaju wyjątku wskazującego nieprawidłowe argumenty lub argumenty spoza zakresu?
JDługosz
@ JDługosz Tak! C # ma ArgumentException , a Java ma IllegalArgumentException .
Kevin
Pytanie używało c #. Oto C ++ (link do streszczenia) dla kompletności.
JDługosz
3
„Awaria szybko do najbliższego bezpiecznego stanu” jest bardziej rozsądna. Jeśli utworzysz aplikację tak, aby ulegała awarii, gdy wydarzy się coś nieoczekiwanego, ryzykujesz utratą danych użytkownika. Miejsca, w których dane użytkownika są zagrożone, są świetnymi punktami do „przedostatniej” obsługi wyjątków (zawsze są przypadki, w których można zrobić tylko wznieść ręce do góry - ostateczna awaria). Doprowadzenie do awarii podczas debugowania to po prostu otwarcie kolejnej puszki robaków - i tak musisz przetestować to, co wdrażasz u użytkownika, a teraz spędzasz połowę czasu na testowaniu wersji, której użytkownik nigdy nie zobaczy.
Luaan
2
@Luaan: jednak nie jest to funkcja przykładowa.
whatsisname
11

Czy powinieneś dodać zbędny kod? Nie.

Ale to, co opisujesz, nie jest zbędnym kodem .

To, co opisujesz, to programowanie w obronie przed wywoływaniem kodu, który narusza warunki wstępne twojej funkcji. Niezależnie od tego, czy robisz to, czy po prostu pozostawiasz użytkownikom czytanie dokumentacji i unikanie tych naruszeń, jest to całkowicie subiektywne.

Osobiście wierzę w tę metodologię, ale podobnie jak w przypadku wszystkiego, musisz być ostrożny. Weźmy na przykład C ++ std::vector::operator[]. Odkładając na chwilę implementację trybu debugowania VS, ta funkcja nie wykonuje sprawdzania granic. Jeśli zażądasz elementu, który nie istnieje, wynik jest niezdefiniowany. Użytkownik powinien podać poprawny indeks wektorowy. Jest to dość celowe: możesz „włączyć się” do sprawdzania granic, dodając je na stronie wywoławczej, ale jeśli operator[]implementacja miałaby to zrobić, nie będziesz w stanie „zrezygnować”. Ma to sens jako funkcja dość niskiego poziomu.

Ale jeśli piszesz AddEmployee(string name)funkcję dla jakiegoś interfejsu wyższego poziomu, w pełni oczekiwałbym, że ta funkcja rzuci przynajmniej wyjątek, jeśli podasz pusty name, a także warunek ten zostanie udokumentowany bezpośrednio nad deklaracją funkcji. Być może nie udostępniasz dziś niechcianej informacji o tej funkcji przez użytkownika, ale uczynienie jej „bezpieczną” w ten sposób oznacza, że ​​wszelkie naruszenia warunków wstępnych, które pojawią się w przyszłości, można łatwo zdiagnozować, zamiast potencjalnie wywołać łańcuch domina trudnych do wykryć błędy. To nie jest redundancja: to staranność.

Gdybym musiał wymyślić ogólną zasadę (chociaż jako ogólną zasadę staram się jej unikać), powiedziałbym, że funkcja spełniająca którekolwiek z poniższych kryteriów:

  • żyje w języku wysokiego poziomu (powiedzmy JavaScript zamiast C)
  • znajduje się na granicy interfejsu
  • nie jest krytyczny dla wydajności
  • bezpośrednio akceptuje dane wprowadzone przez użytkownika

… Może skorzystać z programowania obronnego. W innych przypadkach nadal możesz pisać assertjony, które strzelają podczas testowania, ale są wyłączone w kompilacjach wersji, aby dodatkowo zwiększyć swoją zdolność do znajdowania błędów.

Temat ten jest dalej badany na Wikipedii ( https://en.wikipedia.org/wiki/Defensive_programming ).

Lekkość Wyścigi na orbicie
źródło
9

Istotne są tutaj dwa z dziesięciu przykazań programistycznych:

  • Nie należy zakładać, że dane wejściowe są prawidłowe

  • Nie będziesz tworzył kodu do wykorzystania w przyszłości

Tutaj sprawdzanie wartości null nie oznacza „tworzenia kodu do wykorzystania w przyszłości”. Tworzenie kodu do wykorzystania w przyszłości przypomina dodawanie interfejsów, ponieważ uważasz, że mogą one być przydatne „pewnego dnia”. Innymi słowy, przykazaniem jest, aby nie dodawać warstw abstrakcji, chyba że są one w tej chwili potrzebne.

Sprawdzanie wartości null nie ma nic wspólnego z przyszłym użyciem. Ma to związek z przykazaniem nr 1: nie zakładaj, że dane wejściowe będą prawidłowe. Nigdy nie zakładaj, że twoja funkcja otrzyma pewien podzbiór danych wejściowych. Funkcja powinna reagować logicznie, bez względu na to, jak fałszywe i pomieszane są dane wejściowe.

Tyler Durden
źródło
5
Gdzie są te przykazania programowania? Czy masz link? Jestem ciekawy, ponieważ pracowałem nad kilkoma programami, które podpisują się pod drugim z tych przykazań, i kilkoma, które tego nie zrobiły. Niezmiennie ci, którzy przyjęli przykazanie, Thou shall not make code for future usenatrafili na problemy z utrzymaniem wcześniej , pomimo intuicyjnej logiki przykazania. Odkryłem, w kodowaniu w prawdziwym życiu, że przykazanie jest skuteczne tylko w kodzie, w którym kontrolujesz listę funkcji i terminy, i upewniam się, że nie potrzebujesz kodu przyszłości, aby do nich dotrzeć ... to znaczy nigdy.
Cort Ammon
1
Trywialnie możliwe do udowodnienia: jeśli można oszacować prawdopodobieństwo przyszłego użycia oraz przewidywaną wartość kodu „przyszłego użycia”, a iloczyn tych dwóch jest większy niż koszt dodania kodu „przyszłego użycia”, statystycznie optymalne jest dodaj kod. Myślę, że przykazanie pojawia się w sytuacjach, w których programiści (lub menedżerowie) zmuszeni są przyznać, że ich umiejętności szacowania nie są tak wiarygodne, jak by chcieli, więc jako środek obronny po prostu decydują się wcale nie szacować przyszłego zadania.
Cort Ammon
2
@CortAmmon W programowaniu nie ma miejsca na przykazania religijne - „najlepsze praktyki” mają sens tylko w kontekście, a nauka „najlepszych praktyk” bez uzasadnienia uniemożliwia dostosowanie się. Uważam, że YAGNI jest bardzo przydatny, ale to nie znaczy, że nie myślę o miejscach, w których dodanie punktów rozszerzenia później będzie kosztowne - oznacza to po prostu, że nie muszę myśleć o prostych przypadkach z wyprzedzeniem. Oczywiście zmienia się to również w miarę upływu czasu, ponieważ coraz więcej założeń opiera się na kodzie, skutecznie zwiększając interfejs kodu - jest to działanie równoważące.
Luaan
2
@CortAmmon Twój „trywialnie możliwy do udowodnienia” przypadek ignoruje dwa bardzo ważne koszty - koszt samego oszacowania oraz koszty utrzymania (ewentualnie niepotrzebnego) punktu rozszerzenia. To tam ludzie otrzymują niezwykle niewiarygodne szacunki - nie doceniając ich. Aby uzyskać bardzo prostą funkcję, wystarczy zastanowić się przez kilka sekund, ale jest całkiem prawdopodobne, że znajdziesz całą puszkę robaków, która wynika z początkowo „prostej” funkcji. Komunikacja jest kluczem - w miarę jak sprawy stają się duże, musisz porozmawiać z liderem / klientem.
Luaan
1
@Luaan Próbowałem się przekonać, że nie ma miejsca na przykazania religijne w programowaniu. Tak długo, jak istnieje jeden przypadek biznesowy, w którym koszty oszacowania i utrzymania punktu rozszerzenia są wystarczająco ograniczone, istnieje przypadek, w którym wspomniane „przykazanie” jest wątpliwe. Z mojego doświadczenia z kodem pytanie, czy zostawić takie punkty rozszerzeń, czy nie, nigdy nie pasowało do jednego przykazania w jednym wierszu.
Cort Ammon
7

Definicja „nadmiarowego kodu” i „YAGNI” często zależy od tego, jak daleko patrzysz w przyszłość.

Jeśli napotkasz problem, będziesz pisał przyszły kod w taki sposób, aby uniknąć tego problemu. Inny programista, który nie doświadczył tego konkretnego problemu, może rozważyć nadmiar kodu.

Moją propozycją jest śledzenie, ile czasu spędzasz na „rzeczach, które jeszcze się nie pomyliły”, jeśli ładuje się ono, a Ty, rówieśnicy, wyrzucasz funkcje szybciej niż ty, a następnie zmniejszaj je.

Jeśli jednak jesteś podobny do mnie, spodziewam się, że po prostu napisałeś to wszystko „domyślnie” i tak naprawdę nie zajęło ci to dłużej.

Ewan
źródło
6

Dobrym pomysłem jest udokumentowanie wszelkich założeń dotyczących parametrów. I dobrze jest sprawdzić, czy kod klienta nie narusza tych założeń. Zrobiłbym to:

/** ...
*   Precondition: rows is null or nonempty
*/
public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    Assert( rows==null || rows.Count > 0 )
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

[Zakładając, że jest to C #, Assert może nie być najlepszym narzędziem do tego zadania, ponieważ nie jest skompilowane w wydanym kodzie. Ale to debata na kolejny dzień.]

Dlaczego to jest lepsze niż to, co napisałeś? Twój kod ma sens, jeśli w przyszłości, gdzie zmienił się twój klient, gdy klient przejdzie na pustą listę, właściwym rozwiązaniem będzie dodanie pierwszego wiersza i dodanie aplikacji do jego spotkań. Ale skąd wiesz, że tak będzie? Lepiej jest teraz przyjmować mniej założeń na temat przyszłości.

Theodore Norvell
źródło
5

Oszacować koszt dodając ten kod teraz . Będzie to stosunkowo tanie, ponieważ wszystko jest świeże w twoim umyśle, więc będziesz mógł to zrobić szybko. Dodanie testów jednostkowych jest konieczne - nie ma nic gorszego niż użycie jakiejś metody rok później, to nie działa, a okazuje się, że od samego początku było zepsute i nigdy nie działało.

Oszacuj koszt dodania tego kodu, gdy jest on potrzebny. Będzie drożej, ponieważ musisz wrócić do kodu, pamiętać o wszystkim, a to jest o wiele trudniejsze.

Oszacuj prawdopodobieństwo, że dodatkowy kod będzie rzeczywiście potrzebny. Następnie wykonaj matematykę.

Z drugiej strony, kod pełny z założeniami „X nigdy się nie wydarzy” jest okropny do debugowania. Jeśli coś nie działa zgodnie z przeznaczeniem, oznacza to albo głupi błąd, albo błędne założenie. Twoje „X nigdy się nie wydarzy” jest założeniem, a w przypadku błędu jest podejrzane. Co zmusza następnego programistę do marnowania na to czasu. Zwykle lepiej nie polegać na takich założeniach.

gnasher729
źródło
4
W pierwszym akapicie zapomniałeś wspomnieć o kosztach utrzymania tego kodu w czasie, gdy okazuje się, że funkcja, która jest rzeczywiście potrzebna, wyklucza się wzajemnie z funkcją, która została niepotrzebnie dodana. . .
ruakh
Musisz także oszacować koszt błędów, które mogą wkraść się do programu, ponieważ nie zawiedziesz przy nieprawidłowym wprowadzaniu danych. Nie można jednak oszacować kosztu błędów, ponieważ z definicji są one nieoczekiwane. Tak więc cała „matematyka” rozpada się.
JacquesB
3

Podstawowe pytanie brzmi: „co się stanie, jeśli zrobisz / nie zrobisz”?

Jak zauważyli inni, tego rodzaju programowanie obronne jest dobre, ale czasami niebezpieczne.

Na przykład, jeśli podasz wartość domyślną, to utrzymasz program. Ale program może teraz nie robić tego, co chcesz. Na przykład, jeśli zapisuje tę pustą tablicę do pliku, być może zmieniłeś swój błąd z „awarii, ponieważ przypadkowo podałem null”, aby „wyczyścił wiersze kalendarza, ponieważ podałem null przez przypadek”. (na przykład jeśli zaczniesz usuwać rzeczy, które nie pojawiają się na liście w tej części, która brzmi „// bla”)

Kluczem jest dla mnie Nigdy nie uszkadzaj danych . Pozwól mi to powtórzyć; NIGDY. SKORUMPOWANY. DANE. Jeśli Twój program się wyjątkuje, otrzymasz raport o błędzie, który możesz załatać; jeśli zapisuje złe dane do pliku, który później, musisz zasiać ziemię solą.

Wszystkie twoje „niepotrzebne” decyzje powinny być podejmowane z uwzględnieniem tej przesłanki.

deworde
źródło
2

To, z czym masz do czynienia, to w zasadzie interfejs. Dodając zachowanie „gdy dane wejściowe są null, zainicjuj dane wejściowe”, skutecznie rozszerzyłeś interfejs metody - teraz zamiast zawsze działać na prawidłowej liście, zmusiłeś ją do „naprawy” danych wejściowych. Bez względu na to, czy jest to oficjalna, czy nieoficjalna część interfejsu, możesz założyć się, że ktoś (najprawdopodobniej i ty) użyje tego zachowania.

Interfejsy powinny być proste i powinny być względnie stabilne - zwłaszcza w czymś takim jak public staticmetoda. Masz trochę swobody w metodach prywatnych, zwłaszcza w metodach instancji prywatnych. Poprzez niejawne rozszerzenie interfejsu sprawiłeś, że Twój kod jest bardziej złożony w praktyce. Teraz wyobraź sobie, że tak naprawdę nie chcesz używać tej ścieżki kodu - więc unikaj jej. Teraz masz trochę nieprzetestowanego kodu, który udaje, że jest częścią zachowania metody. Mogę ci teraz powiedzieć, że prawdopodobnie ma błąd: kiedy przekazujesz listę, ta lista jest mutowana przez metodę. Jeśli jednak tego nie zrobisz, utworzysz lokalnylisty i wyrzuć ją później. Jest to rodzaj niespójnego zachowania, które sprawi, że za pół roku będziesz płakać, próbując wyśledzić niejasny błąd.

Ogólnie rzecz biorąc, programowanie defensywne jest bardzo przydatne. Ale ścieżki kodu dla kontroli obronnych muszą zostać przetestowane, tak jak każdy inny kod. W takim przypadku komplikują kod bez powodu, a zamiast tego wybrałbym taką alternatywę:

if (rows == null) throw new ArgumentNullException(nameof(rows));

Nie chcesz, aby dane wejściowe rowsbyły zerowe i chcesz, aby błąd był widoczny dla wszystkich dzwoniących tak szybko, jak to możliwe .

Istnieje wiele wartości, które należy pogodzić przy tworzeniu oprogramowania. Nawet solidność sama w sobie jest bardzo złożoną cechą - na przykład nie uważałbym twojej obrony za bardziej solidną niż rzucenie wyjątku. Wyjątki są bardzo przydatne, aby zapewnić bezpieczne miejsce do ponownego spróbowania w bezpiecznym miejscu - problemy z uszkodzeniem danych są zwykle znacznie trudniejsze do wykrycia niż wczesne rozpoznanie problemu i bezpieczne rozwiązanie go. W końcu dają złudzenie solidności, a miesiąc później zauważasz, że zniknęła jedna dziesiąta twoich spotkań, ponieważ nigdy nie zauważyłeś, że inna lista została zaktualizowana. Ojej

Rozróżnij oba. Programowanie defensywne jest przydatną techniką do wychwytywania błędów w miejscu, w którym są one najbardziej istotne, znacznie pomagając w debugowaniu oraz z dobrą obsługą wyjątków, zapobiegając „podstępnej korupcji”. Porażka wczesna, porażka szybka. Z drugiej strony to, co robisz, jest bardziej jak „ukrywanie błędów” - żonglujesz danymi wejściowymi i zakładasz, co miał na myśli rozmówca. Jest to bardzo ważne w przypadku kodu skierowanego do użytkownika (np. Sprawdzanie pisowni), ale należy zachować ostrożność, widząc go w przypadku kodu skierowanego do programisty.

Głównym problemem jest to, że bez względu na to, jaką abstrakcję stworzysz, wycieknie („Chciałem pisać, nie, nie! Głupi moduł sprawdzania pisowni!”), A kod do obsługi wszystkich specjalnych przypadków i poprawek nadal zawiera kod musisz utrzymywać i rozumieć oraz kod, który musisz przetestować. Porównaj wysiłek, aby upewnić się, że lista nie jest zerowa, z naprawieniem błędu, który masz rok później w produkcji - nie jest to dobry kompromis. W idealnym świecie chciałbyś, aby każda metoda działała wyłącznie z własnym wkładem, zwracając wynik i nie modyfikując żadnego stanu globalnego. Oczywiście w prawdziwym świecie znajdziesz wiele przypadków, w których tak nie jestnajprostsze i najczystsze rozwiązanie (np. podczas zapisywania pliku), ale uważam, że utrzymywanie metod „czystych”, gdy nie ma powodu, aby czytały lub manipulowały stanem globalnym, znacznie ułatwiają rozumowanie kodu. Daje też więcej naturalnych punktów za podzielenie metod :)

Nie oznacza to, że wszystko nieoczekiwane powinno spowodować awarię aplikacji, wręcz przeciwnie. Jeśli dobrze wykorzystujesz wyjątki, w naturalny sposób tworzą one bezpieczne punkty obsługi błędów, w których możesz przywrócić stabilny stan aplikacji i pozwolić użytkownikowi kontynuować to, co robią (najlepiej unikając utraty danych przez użytkownika). W tych punktach obsługi zobaczysz możliwości naprawienia problemów („Nie znaleziono numeru zamówienia 2212. Miałeś na myśli 2212b?”) Lub przekazania użytkownikowi kontroli („Błąd połączenia z bazą danych. Spróbuj ponownie?”). Nawet gdy żadna taka opcja nie jest dostępna, przynajmniej da ci szansę, że nic się nie zepsuje - zacząłem doceniać kod, który używa usingi try... finallyo wiele więcej niż try...catch, daje wiele możliwości utrzymania niezmienników nawet w wyjątkowych warunkach.

Użytkownicy nie powinni tracić swoich danych i pracy. To wciąż musi być zrównoważone kosztami rozwoju itp., Ale jest to całkiem dobra ogólna wskazówka (jeśli użytkownicy decydują, czy kupić oprogramowanie, czy nie - oprogramowanie wewnętrzne zwykle nie ma tego luksusu). Nawet awaria całej aplikacji staje się o wiele mniejszym problemem, jeśli użytkownik może po prostu zrestartować i wrócić do tego, co robił. To prawdziwa niezawodność - program Word stale zapisuje twoją pracę, nie uszkadzając dokumentu na dysku i dając ci opcjęaby przywrócić te zmiany po ponownym uruchomieniu programu Word po awarii. Czy to lepsze niż brak błędu? Prawdopodobnie nie - choć nie zapominaj, że praca poświęcona na złapanie rzadkiego błędu może być lepiej wszędzie. Jest to jednak znacznie lepsze niż alternatywy - np. Uszkodzony dokument na dysku, cała praca od ostatniego zapisu utracona, dokument automatycznie zastąpiony zmianami przed awarią, którym były Ctrl + A i Delete.

Luaan
źródło
1

Odpowiem na to w oparciu o twoje założenie, że solidny kod przyniesie ci „lata” od teraz. Jeśli Twoim celem są długoterminowe korzyści, priorytetem byłby projekt i łatwość konserwacji nad solidnością.

Kompromisem między designem a solidnością jest czas i koncentracja. Większość programistów wolałaby mieć zestaw dobrze zaprojektowanego kodu, nawet jeśli oznacza to przejście przez pewne problemy i wykonanie dodatkowych warunków lub obsługę błędów. Po kilku latach użytkowania miejsca, których naprawdę potrzebujesz, zostały prawdopodobnie zidentyfikowane przez użytkowników.

Zakładając, że projekt ma taką samą jakość, łatwiej jest utrzymać mniej kodu. Nie oznacza to, że lepiej nam będzie, jeśli pozwolisz odejść znanym problemom przez kilka lat, ale dodanie rzeczy, o których wiesz, że nie potrzebujesz, utrudnia. Wszyscy spojrzeliśmy na starszy kod i znaleźliśmy niepotrzebne części. Musisz mieć kod zmieniający poziom zaufania, który działał przez lata.

Więc jeśli uważasz, że Twoja aplikacja jest zaprojektowana tak, jak jest, może być łatwa w utrzymaniu i nie zawiera żadnych błędów, znajdź coś lepszego niż dodawanie niepotrzebnego kodu. To najmniej można zrobić z szacunku dla wszystkich innych programistów pracujących godzinami nad bezsensownymi funkcjami.

JeffO
źródło
1

Nie , nie powinieneś. W rzeczywistości odpowiadasz na własne pytanie, gdy twierdzisz, że ten sposób kodowania może ukrywać błędy . Nie sprawi to, że kod będzie bardziej niezawodny - raczej zwiększy podatność na błędy i utrudni debugowanie.

Podajesz swoje bieżące oczekiwania co do rowsargumentu: albo jest on zerowy, albo w przeciwnym razie ma co najmniej jeden element. Pytanie brzmi: czy dobrym pomysłem jest napisanie kodu, który dodatkowo poradzi sobie z nieoczekiwanym trzecim przypadkiem, w którym rowsma zero pozycji?

Odpowiedź brzmi nie. Zawsze powinieneś zgłaszać wyjątek w przypadku nieoczekiwanych danych wejściowych. Zastanów się: jeśli niektóre inne części kodu nie spełniają oczekiwań (tj. Umowy) Twojej metody , oznacza to, że wystąpił błąd . Jeśli istnieje błąd, który chcesz znać jak najwcześniej, aby go naprawić, a wyjątek pomoże ci to zrobić.

To, co obecnie robi kod, to zgadywanie, jak naprawić błąd, który może, ale nie musi istnieć w kodzie. Ale nawet jeśli wystąpi błąd, nie możesz wiedzieć, jak go w pełni odzyskać. Błędy z definicji mają nieznane konsekwencje. Być może jakiś kod inicjujący nie działał zgodnie z oczekiwaniami, może mieć wiele innych konsekwencji niż tylko brakujący wiersz.

Więc twój kod powinien wyglądać tak:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    if (rows != null && rows.Count == 0) throw new ArgumentException("Rows is empty."); 

    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

Uwaga: Istnieją pewne szczególne przypadki, w których sensowne jest „odgadnięcie”, jak obsługiwać nieprawidłowe dane wejściowe, a nie tylko zgłaszanie wyjątku. Na przykład, jeśli obsługujesz wejścia zewnętrzne, nie masz nad nimi kontroli. Przeglądarki internetowe są niesławnym przykładem, ponieważ próbują z wdziękiem obsługiwać wszelkiego rodzaju zniekształcone i nieprawidłowe dane wejściowe. Ma to jednak sens tylko w przypadku zewnętrznego wejścia, a nie wywołań z innych części programu.


Edycja: Niektóre inne odpowiedzi mówią, że programujesz w obronie . Nie zgadzam się. Programowanie defensywne oznacza, że ​​nie ufasz automatycznie, że dane wejściowe są prawidłowe. Dlatego sprawdzanie poprawności parametrów (jak wyżej) jest defensywną techniką programowania, ale nie oznacza to, że powinieneś zmienić nieoczekiwane lub nieprawidłowe dane wejściowe poprzez zgadywanie. Solidne podejście obronne polega na sprawdzeniu poprawności danych wejściowych, a następnie zgłoszeniu wyjątku w przypadku nieoczekiwanych lub nieprawidłowych danych wejściowych.

JacquesB
źródło
1

Czy powinienem teraz dodać zbędny kod na wypadek, gdyby był potrzebny w przyszłości?

W żadnym momencie nie należy dodawać zbędnego kodu.

Nie należy dodawać kodu, który będzie potrzebny tylko w przyszłości.

Należy upewnić się, że kod zachowuje się dobrze bez względu na to, co się stanie.

Definicja „zachowuj się” zależy od twoich potrzeb. Jedną z technik, które lubię stosować, są wyjątki „paranoi”. Jeśli jestem w 100% pewien, że określony przypadek nigdy się nie wydarzy, nadal programuję wyjątek, ale robię to w sposób, który a) wyraźnie mówi wszystkim, że nigdy się tego nie spodziewam, oraz b) jest wyraźnie wyświetlany i rejestrowany oraz dlatego nie prowadzi to później do pełzającej korupcji.

Przykładowy pseudo-kod:

file = File.open(">", "bla")  or raise "Paranoia: cannot open file 'bla'"

file.write("xytz") or raise "Paranoia: disk full?"

file.close()  or raise "Paranoia: huh?!?!?"

To wyraźnie oznacza, że ​​jestem w 100% pewien, że zawsze mogę otworzyć, zapisać lub zamknąć plik, tj. Nie przechodzę do zakresu tworzenia skomplikowanej obsługi błędów. Ale jeśli (nie: kiedy) nie mogę otworzyć pliku, mój program nadal zawiedzie w kontrolowany sposób.

Interfejs użytkownika oczywiście nie wyświetla takich wiadomości użytkownikowi, będą one logowane wewnętrznie wraz ze śledzeniem stosu. Ponownie, są to wyjątki wewnętrzne „paranoi”, które tylko upewniają się, że kod „zatrzymuje się”, gdy wydarzy się coś nieoczekiwanego. Ten przykład jest nieco sprzeczny, w praktyce oczywiście wdrażałbym obsługę błędów podczas otwierania plików, ponieważ zdarza się to regularnie (zła nazwa pliku, pamięć USB podłączona tylko do odczytu, cokolwiek).

Bardzo ważnym pokrewnym wyszukiwanym terminem byłoby „szybko zawieść”, jak zauważono w innych odpowiedziach, i jest bardzo przydatne do tworzenia solidnego oprogramowania.

AnoE
źródło
-1

Tutaj jest wiele zbyt skomplikowanych odpowiedzi. Prawdopodobnie zadałeś to pytanie, ponieważ nie czułeś się dobrze z tym kodem części, ale nie byłeś pewien, dlaczego lub jak to naprawić. Więc moja odpowiedź jest taka, że ​​problem jest bardzo prawdopodobne w strukturze kodu (jak zawsze).

Najpierw nagłówek metody:

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)

Przypisać spotkanie do jakiego rzędu? Powinno to być natychmiast usunięte z listy parametrów. Bez dalszej wiedzy, spodziewam params metoda wyglądać następująco: (Appointment app, CalendarRow row).

Następnie „kontrole danych wejściowych”:

//1. Is rows equal to null? - This will be the case if this is the first appointment.
if (rows == null) {
    rows = new List<CalendarRow> ();
}

//2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
if(rows.Count == 0)
{
    rows.Add (new CalendarRow (0));
    rows [0].Appointments.Add (app);
}

To bzdury.

  1. sprawdź) Program wywołujący metodę powinien tylko upewnić się, że nie przekazuje niezainicjowanych wartości wewnątrz metody. Odpowiedzialność programisty (aby spróbować) nie była głupia.
  2. sprawdź) Jeśli nie wezmę pod uwagę, że przekazanie rowsdo metody jest prawdopodobnie błędne (patrz komentarz powyżej), wówczas nie powinna ponosić odpowiedzialności za wywołaną metodę AssignAppointmentToRowmanipulowania wierszami w jakikolwiek inny sposób niż przypisanie spotkania gdzieś.

Ale cała koncepcja przypisywania spotkań gdzieś jest dziwna (chyba że jest to część kodu GUI). Twój kod wydaje się zawierać (lub przynajmniej próbuje) jawną strukturę danych reprezentującą kalendarz (to jest List<CalendarRows><- który powinien być zdefiniowany Calendargdzieś, jeśli chcesz iść w ten sposób, wtedy przejdziesz Calendar calendardo swojej metody). Jeśli pójdziesz tą drogą, spodziewam się, że calendarbędą one wypełnione przedziałami czasu, w których następnie umieszczasz (przypisujesz) spotkania (np. calendar[month][day] = appointmentBędzie to odpowiedni kod). Ale potem możesz również całkowicie usunąć strukturę kalendarza z głównej logiki i po prostu mieć miejsce, w List<Appointment>którym Appointmentobiekty zawierają atrybutdate. A jeśli chcesz wyrenderować kalendarz gdzieś w graficznym interfejsie użytkownika, możesz utworzyć tę strukturę „wyraźnego kalendarza” tuż przed renderowaniem.

Nie znam szczegółów twojej aplikacji, więc niektóre z nich mogą nie dotyczyć ciebie, ale obie te kontrole (głównie druga) mówią mi, że coś jest nie tak z rozdzieleniem problemów w twoim kodzie.

klimat
źródło
-2

Dla uproszczenia załóżmy, że albo będziesz potrzebować tego fragmentu kodu za N dni (nie później lub wcześniej), albo w ogóle nie będzie potrzebny.

Pseudo kod:

let C_now   = cost of implementing the piece of code now
let C_later = ... later
let C_maint = cost of maintaining the piece of code one day
              (note that it can be negative)
let P_need  = probability that you will need the code after N days

if C_now + C_maint * N < P_need*C_later then implement the code else omit it.

Czynniki dla C_maint:

  • Czy ogólnie poprawia kod, czyniąc go bardziej samodokumentującym, łatwiejszym do przetestowania? Jeśli tak, C_maintoczekiwano ujemnego
  • Czy sprawia, że ​​kod jest większy (stąd trudniejszy do odczytania, dłuższy do kompilacji, wdrożenia testów itp.)?
  • Czy czekają jakieś refaktoryzacje / przeprojektowania? Jeśli tak, uderz C_maint. Ten przypadek wymaga bardziej złożonej formuły, z większą liczbą zmiennych takich jak N.

Tak wielka rzecz, która tylko obciąża kod i może być potrzebna tylko za 2 lata z małym prawdopodobieństwem, powinna zostać pominięta, ale drobiazg, który sugeruje również przydatne twierdzenia i 50%, że będzie potrzebny za 3 miesiące, powinien zostać wdrożony .

Vi0
źródło
Musisz także uwzględnić koszt błędów, które mogą wkraść się do programu, ponieważ nie odrzucasz nieprawidłowych danych wejściowych. Jak więc szacujesz koszt potencjalnych trudnych do znalezienia błędów?
JacquesB