Dobry styl kodu do wprowadzania kontroli danych wszędzie?

10

Mam projekt, który jest wystarczająco duży, że nie mogę już dłużej zachować każdego aspektu w głowie. Mam do czynienia z wieloma klasami i funkcjami, a także przekazuję dane.

Z czasem zauważyłem, że ciągle pojawiają się błędy, ponieważ zapomniałem, jaką dokładną formę muszą mieć dane, gdy przekazuję je różnym funkcjom ( np. Jedna funkcja akceptuje i wyświetla tablicę ciągów, inna funkcja, którą napisałem znacznie później, akceptuje ciągi przechowywane w słowniku itp., więc muszę przekształcić ciągi, z którymi pracuję, od umieszczenia ich w tablicy do umieszczenia ich w słowniku ).

Aby uniknąć konieczności ciągłego ustalania, co się zepsuło, zacząłem traktować każdą funkcję i klasę jako „izolowany byt” w tym sensie, że nie może polegać na kodzie zewnętrznym, podając poprawne dane wejściowe i musi sam sprawdzać dane wejściowe (lub, w niektórych przypadkach przekształć dane, jeśli dane są podane w niewłaściwej formie).

To znacznie skróciło czas, jaki spędzam, upewniając się, że przekazywane przeze mnie dane „pasują” do każdej funkcji, ponieważ same klasy i funkcje ostrzegają mnie teraz, gdy niektóre dane wejściowe są złe (a czasem nawet poprawiają to), a ja nie muszę już przejść z debuggerem przez cały kod, aby dowiedzieć się, gdzie coś poszło nie tak.

Z drugiej strony zwiększyło to również ogólny kod.
Moje pytanie brzmi, czy ten styl kodu jest odpowiedni do rozwiązania tego problemu?
Oczywiście najlepszym rozwiązaniem byłoby całkowite przefakturowanie projektu i upewnienie się, że dane mają jednolitą strukturę dla wszystkich funkcji - ale ponieważ ten projekt stale się rozwija, musiałbym więcej wydawać i martwić się czystym kodem niż dodawać nowe rzeczy .

(FYI: Nadal jestem początkującym, więc przepraszam, jeśli to pytanie było naiwne; mój projekt jest w Pythonie).

użytkownik7088941
źródło
1
Możliwy duplikat tego, jak powinniśmy się bronić?
komar
3
@gnat Jest podobnie, ale zamiast odpowiedzieć na moje pytanie, zawiera on porady („bądź tak defensywny, jak tylko możesz”) dla tego konkretnego wystąpienia , o którym wspominał OP, który różni się od mojego bardziej ogólnego zapytania.
user7088941,
2
„ale ponieważ ten projekt stale się rozwija, musiałbym więcej wydawać i martwić się czystym kodem niż dodawać nowe rzeczy” - brzmi to tak, jakbyś musiał zacząć martwić się o czysty kod. W przeciwnym razie produktywność będzie spowalniała i zwalniała, ponieważ każdy nowy element funkcjonalności jest coraz trudniejszy do dodania z powodu istniejącego kodu. Nie wszystkie refaktoryzacje muszą być „kompletne”, jeśli dodanie czegoś nowego jest trudne ze względu na istniejący kod, którego dotyka, refaktoryzuj tylko ten dotykający kod i zanotuj, co chciałbyś odwiedzić później
matowy darmowy
3
Jest to problem, z którym często spotykają się ludzie używający słabo napisanych języków. Jeśli nie chcesz lub możesz zmienić język na bardziej ścisły, odpowiedź brzmi: „tak, ten styl kodu jest odpowiedni do rozwiązania tego problemu” . Następne pytanie?
Doc Brown
1
W ściśle określonym języku, z właściwymi typami danych, kompilator zrobiłby to za Ciebie.
SD

Odpowiedzi:

4

Lepszym rozwiązaniem jest lepsze wykorzystanie funkcji i narzędzi języka Python.

Na przykład w funkcji 1 oczekiwanym wejściem jest tablica ciągów, gdzie pierwszy ciąg oznacza tytuł czegoś, a drugi odniesienie bibliograficzne. W funkcji 2 oczekiwane dane wejściowe są nadal tablicą ciągów, ale teraz role ciągów są odwrócone.

Ten problem został złagodzony za pomocą namedtuple. Jest lekki i nadaje członkom tablicy łatwe semantyczne znaczenie.

Aby skorzystać z funkcji automatycznego sprawdzania typów bez przełączania języków, możesz skorzystać z podpowiedzi do typów . Dobry IDE może to wykorzystać, aby poinformować cię, gdy zrobisz coś głupiego.

Wygląda na to, że martwisz się, że funkcje przestaną działać, gdy zmienią się wymagania. Można to wykryć za pomocą automatycznych testów .

Chociaż nie twierdzę, że ręczne sprawdzanie nigdy nie jest właściwe, lepsze wykorzystanie dostępnych funkcji językowych może pomóc w rozwiązaniu tego problemu w łatwiejszy do utrzymania sposób.


źródło
+1 za wskazanie mnie namedtuplei wszystkich innych fajnych rzeczy. Nie o tym nie namedtuplewiedziałem - i chociaż wiedziałem o automatycznych testach, nigdy tak naprawdę nie korzystałem z niego zbyt często i nie zdawałem sobie sprawy, jak bardzo by mi to pomogło w tym przypadku. Wszystko to wydaje się być tak dobre jak analiza statyczna. (Automatyczne testowanie może być nawet lepsze, ponieważ mogę uchwycić wszystkie subtelne rzeczy, które nie zostałyby złapane w analizie statycznej!) Jeśli znasz jakieś inne, daj mi znać. Pozostawię pytanie otwarte dłużej, ale jeśli nie otrzymam innych odpowiedzi, zaakceptuję twoje.
user7088941,
9

OK, rzeczywisty problem jest opisany w komentarzu pod tą odpowiedzią:

Na przykład w funkcji 1 oczekiwanym wejściem jest tablica ciągów, gdzie pierwszy ciąg oznacza tytuł czegoś, a drugi odniesienie bibliograficzne. W funkcji 2 oczekiwane dane wejściowe są nadal tablicą ciągów, ale teraz role ciągów są odwrócone

Problemem jest tutaj użycie listy ciągów, w których kolejność oznacza semantykę. To bardzo podatne na błędy podejście. Zamiast tego należy utworzyć niestandardową klasę z dwoma polami o nazwach titlei bibliographical_reference. W ten sposób nie będziesz ich mieszać i unikniesz tego problemu w przyszłości. Oczywiście wymaga to refaktoryzacji, jeśli już używasz list ciągów znaków w wielu miejscach, ale wierz mi, na dłuższą metę będzie to tańsze.

Powszechnym podejściem w językach dynamicznych jest „pisanie kaczką”, co oznacza, że ​​tak naprawdę nie zależy ci na „typie” przekazywanego obiektu, zależy ci tylko na tym, czy obsługuje metody, które na nim wywołujesz. W twoim przypadku po prostu przeczytasz pole wywoływane, bibliographical_referencegdy jest ono potrzebne. Jeśli to pole nie istnieje na przekazanym obiekcie, pojawi się błąd, co oznacza, że ​​do funkcji został przekazany niewłaściwy typ. Jest to tak samo dobra kontrola typu, jak każda inna.

JacquesB
źródło
Czasami problem jest jeszcze bardziej subtelny: przekazuję poprawny typ, ale „struktura wewnętrzna” moich danych wejściowych zaburza funkcję: Na przykład w funkcji 1 oczekiwanym wejściem jest tablica ciągów, gdzie pierwszy ciąg oznacza tytuł czegoś, a drugi odniesienie bibliograficzne. W funkcji 2 oczekiwane dane wejściowe są nadal tablicą ciągów, ale teraz role ciągów są odwrócone: pierwszy ciąg powinien być odniesieniem bibliograficznym, a drugi powinien być odniesieniem bibliograficznym. Myślę, że te kontrole są odpowiednie?
user7088941,
1
@ user7088941: Opisany problem można łatwo rozwiązać, mając klasę z dwoma polami: „tytuł” ​​i „odniesienie bibliograficzne”. Nie pomieszasz tego. Poleganie na kolejności na liście ciągów wydaje się bardzo podatne na błędy. Może to jest podstawowy problem?
JacquesB,
3
Oto odpowiedź. Python jest językiem zorientowanym obiektowo, a nie językiem z listy słowników z ciągami na liczby całkowite (lub czymkolwiek). Więc używaj obiektów. Obiekty są odpowiedzialne za zarządzanie własnym stanem i egzekwowanie własnych niezmienników, inne obiekty nigdy nie mogą ich uszkodzić (jeśli są poprawnie zaprojektowane). Jeśli nieustrukturyzowane lub częściowo ustrukturyzowane dane dostaną się do twojego systemu z zewnątrz, sprawdzasz poprawność i parsujesz jeden raz na granicy systemu i jak najszybciej konwertujesz do bogatych obiektów.
Jörg W Mittag,
3
„Naprawdę unikałbym ciągłego refaktoryzacji” - ten problem mentalny jest twoim problemem. Dobry kod powstaje tylko w wyniku refaktoryzacji. Dużo refaktoryzacji. Obsługiwane przez testy jednostkowe. Zwłaszcza gdy elementy muszą zostać rozszerzone lub rozwinięte.
Doc Brown
2
Mam to teraz. +1 za wszystkie miłe spostrzeżenia i komentarze. I dzięki wszystkim za ich niezwykle pomocne komentarze! (Kiedy korzystałem z niektórych klas / obiektów, przeplatałem je wspomnianymi listami, co, jak widzę, nie było dobrym pomysłem. Pozostało pytanie, jak najlepiej to zaimplementować, wykorzystując konkretne sugestie z odpowiedzi JETM , co naprawdę zrobiło radykalną różnicę pod względem szybkości osiągnięcia stanu wolnego od błędów.)
user7088941,
3

Po pierwsze, teraz odczuwasz zapach kodu - spróbuj zapamiętać, co doprowadziło cię do świadomości tego zapachu i spróbuj wyostrzyć swój „mentalny” nos, ponieważ im szybciej zauważysz zapach kodu, tym szybciej - i łatwiej - jesteś w stanie rozwiązać podstawowy problem.

Aby uniknąć konieczności ciągłego zastanawiania się, co się zepsuło, zacząłem traktować każdą funkcję i klasę jako „izolowany byt” w tym sensie, że nie może polegać na zewnętrznym kodzie, podając poprawne dane wejściowe i sam musi je sprawdzać.

Programowanie obronne - jak nazywa się tę technikę - jest ważnym i często używanym narzędziem. Jednak, podobnie jak w przypadku wszystkich rzeczy, ważne jest, aby używać właściwej kwoty, zbyt małej liczby czeków i nie złapiecie problemów, zbyt wielu, a kod będzie przepełniony.

(lub, w niektórych przypadkach, przekształcenie danych, jeśli dane są podane w niewłaściwej formie).

To może być mniej dobry pomysł. Jeśli zauważysz, że część twojego programu wywołuje funkcję z niepoprawnie sformatowanymi danymi, NAPRAW TĄ CZĘŚĆ , nie zmieniaj wywoływanej funkcji, aby móc i tak trawić złe dane.

To znacznie skróciło czas, jaki spędzam, upewniając się, że przekazywane przeze mnie dane „pasują” do każdej funkcji, ponieważ same klasy i funkcje ostrzegają mnie teraz, gdy niektóre dane wejściowe są złe (a czasem nawet poprawiają to), a ja nie muszę już przejść z debuggerem przez cały kod, aby dowiedzieć się, gdzie coś poszło nie tak.

Poprawa jakości i łatwości konserwacji twojego kodu to na dłuższą metę oszczędność czasu (w tym sensie muszę ponownie ostrzec przed funkcją autokorekty wbudowaną w niektóre z twoich funkcji - mogą być podstępnym źródłem błędów. Tylko dlatego, że twój program nie ulega awarii i nagrywanie nie oznacza, że ​​działa poprawnie ...)

Aby w końcu odpowiedzieć na twoje pytanie: Tak, programowanie defensywne (tj. Weryfikacja poprawności podanych parametrów) jest - w zdrowym stopniu - dobrą strategią. To powiedziawszy , jak sam powiedziałeś, twój kod jest niespójny, i zdecydowanie polecam, abyś poświęcił trochę czasu na refaktoryzację zapachowych części - powiedziałeś, że nie chcesz martwić się o czysty kod cały czas, poświęcając więcej czasu na „czyszczenie” niż w przypadku nowych funkcji ... Jeśli nie utrzymasz kodu w czystości, możesz poświęcić dwa razy więcej czasu na „oszczędzanie” na nie utrzymywaniu czystego kodu na błędach związanych ze zgnieceniem ORAZ trudności z implementacją nowych funkcji - dług techniczny może zmiażdżyć cię.

CharonX
źródło
1

W porządku Kiedyś kodowałem w FoxPro, gdzie miałem bloki TRY..CATCH prawie w każdej dużej funkcji. Teraz koduję w JavaScript / LiveScript i rzadko sprawdzam parametry w funkcjach „wewnętrznych” lub „prywatnych”.

„Ile sprawdzić” zależy bardziej od wybranego projektu / języka niż umiejętności kodowania.

Michael Quad
źródło
1
Myślę, że to było SPRÓBOWAĆ ... ŁOWIĆ ... IGNOROWAĆ. Zrobiłeś coś przeciwnego do tego, o co prosi OP. IMHO ich celem jest unikanie niespójności, podczas gdy twoja dba o to, aby program nie wybuchł po uderzeniu w jeden.
maaartinus
1
@maaartinus, który jest poprawny. Języki programowania zwykle dają nam konstrukcje, które są proste w użyciu, aby zapobiec aplikacji wysadzenia - ale konstrukcje języków programowania pozwalają nam zapobiegać niespójnościom, wydają się być znacznie trudniejsze w użyciu: o ile wiem, stale refaktoryzuj wszystko i używaj klas, które najlepiej konteneryzują przepływ informacji w Twojej aplikacji. O to właśnie pytam - czy jest łatwiejszy sposób to naprawić.
user7088941,
@ user7088941 Dlatego unikam języków słabo wpisanych. Python jest po prostu fantastyczny, ale jeśli chodzi o coś większego, nie mogę śledzić tego, co zrobiłem gdzie indziej. Dlatego wolę Javę, która jest dość gadatliwa (nie tyle z funkcjami Lombok i Java 8), ma ścisłe pisanie i narzędzia do analizy statycznej. Sugeruję, abyś wypróbował język ściśle typowy, ponieważ nie wiem, jak go rozwiązać inaczej.
maaartinus
Nie chodzi o parametr o ścisłym / luźnym typie. Chodzi o to, aby wiedzieć, że parametr jest poprawny. Nawet jeśli użyjesz (liczba całkowita 4 bajty), może być konieczne sprawdzenie, czy na przykład jest w zakresie 0..10. Jeśli wiesz, że ten parametr ma zawsze wartość 0..10, nie musisz go sprawdzać. FoxPro nie ma na przykład tablic asocjacyjnych, bardzo trudno jest operować zmiennymi, ich zakresem i tak dalej ... dlatego musisz sprawdzić czek.
Michael Quad
1
@ user7088941 To nie jest OO, ale jest reguła „fail fast”. Każda nieprywatna metoda musi sprawdzić swoje argumenty i wyrzucić, gdy coś jest nie tak. Bez próby złapania, bez próby naprawy, po prostu wysadzić go w powietrze. Pewnie, na pewnym wyższym poziomie wyjątek zostanie zarejestrowany i obsłużony. Ponieważ w testach znajduje się większość problemów z wyprzedzeniem, a żadne problemy nie zostają ukryte, kod staje się rozwiązaniem bezbłędnym znacznie szybciej niż w przypadku tolerancji na błędy.
maaartinus,