Jak konieczne jest stosowanie defensywnych praktyk programistycznych dla kodu, który nigdy nie będzie publicznie dostępny?

45

Piszę implementację Java gry karcianej, więc stworzyłem specjalny typ kolekcji, którą nazywam Strefą. Wszystkie metody modyfikacji kolekcji Java są nieobsługiwane, ale w API strefy istnieje metoda move(Zone, Card), która przenosi kartę z danej strefy do siebie (osiągane przy pomocy technik pakietowych). W ten sposób mogę zapewnić, że żadne karty nie zostaną wyjęte ze strefy i po prostu znikną; można je przenieść tylko do innej strefy.

Moje pytanie brzmi: jak konieczne jest tego rodzaju kodowanie obronne? Jest „poprawny” i wydaje się, że jest to właściwa praktyka, ale nie jest tak, że Zone API będzie kiedykolwiek częścią biblioteki publicznej. To jest tylko dla mnie, więc to trochę tak, jakbym chronił swój kod przed samym sobą, gdy prawdopodobnie mógłbym być bardziej wydajny, korzystając ze standardowych kolekcji.

Jak daleko powinienem zająć się tą strefą? Czy ktoś może mi doradzić, jak bardzo powinienem pomyśleć o zachowaniu umów na zajęciach, które piszę, szczególnie dla tych, które tak naprawdę nie będą publicznie dostępne?

łamacz kodów
źródło
4
= ~ s / konieczne / zalecane / gi
GrandmasterB
2
Typy danych powinny być poprawne ze względu na konstrukcję, albo na czym budujesz? Powinny być kapsułkowane w taki sposób, aby, niezależnie od tego, czy były zmienne, mogły znajdować się tylko w ważnych stanach. Tylko wtedy, gdy niemożliwe jest wymuszenie tego statycznie (lub nieuzasadnione trudne), należy zgłosić błąd w czasie wykonywania.
Jon Purdy
1
Nigdy nie mów nigdy. O ile Twój kod nigdy nie zostanie użyty, nigdy nie będziesz mieć pewności, gdzie Twój kod się skończy. ;)
Izkata,
1
Komentarz @codebreaker GrandmasterB jest wyrażeniem zastępującym. Oznacza to: zamień „konieczne” na „zalecane”.
Ricardo Souza,
1
Kod bezkodowy # 116 Zaufanie, że nikt nie jest szczególnie odpowiedni tutaj.

Odpowiedzi:

72

Nie zamierzam rozwiązywać problemu projektowego - wystarczy pytanie, czy robić rzeczy „poprawnie” w niepublicznym interfejsie API.

to tylko dla mnie, więc trochę chronię swój kod przed sobą

Właśnie o to chodzi. Być może istnieją koderzy, którzy pamiętają niuanse każdej klasy i metody, jaką kiedykolwiek napisali, i nigdy nie mylą się przy nich z niewłaściwą umową. Nie jestem jednym z nich. Często zapominam, jak napisany kod powinien działać w ciągu kilku godzin od napisania. Po tym, jak pomyślisz, że raz dobrze to zrozumiałeś, twój umysł będzie miał tendencję do zmiany biegów na problem, nad którym teraz pracujesz .

Masz narzędzia do walki z tym. Narzędzia te obejmują (w nieokreślonej kolejności) konwencje, testy jednostkowe i inne testy automatyczne, sprawdzanie warunków wstępnych i dokumentację. Ja sam przekonałem się, że testy jednostkowe są nieocenione, ponieważ oba zmuszają do przemyślenia, w jaki sposób zostanie wykorzystana twoja umowa i dostarczenia dokumentacji później, w jaki sposób interfejs został zaprojektowany.

Michael K.
źródło
Dobrze wiedzieć. W przeszłości programowałem tak efektywnie, jak tylko mogłem, więc czasami mam trudności z przyzwyczajeniem się do takich pomysłów. Cieszę się, że idę w dobrym kierunku.
codebreaker
15
„Skutecznie” może oznaczać wiele różnych rzeczy! Z mojego doświadczenia wynika, że ​​nowicjusze (nie twierdzę, że jesteś jednym z nich) często przeoczają, jak skutecznie będą w stanie wspierać program. Kod zwykle spędza znacznie dłużej w fazie wsparcia cyklu życia produktu niż w fazie „pisania nowego kodu”, więc uważam, że jest to wydajność, którą należy dokładnie rozważyć.
Charlie Kilian,
2
Zdecydowanie się zgadzam. Jednak w college'u nigdy nie musiałem o tym myśleć.
codebreaker
25

Zwykle przestrzegam kilku prostych zasad:

  • Staraj się zawsze programować na podstawie umowy .
  • Jeśli metoda jest publicznie dostępna lub otrzymuje informacje ze świata zewnętrznego , należy zastosować pewne środki obronne (np IllegalArgumentException.).
  • Dla wszystkiego, co jest dostępne tylko wewnętrznie, użyj asercji (np assert input != null.).

Jeśli klient naprawdę się w to angażuje, zawsze znajdzie sposób, aby sprawić, że Twój kod będzie źle działał. Zawsze mogą to zrobić przynajmniej poprzez refleksję. Ale to piękno projektowania na podstawie umowy . Nie akceptujesz takiego wykorzystania swojego kodu, więc nie możesz zagwarantować, że będzie on działał w takich sytuacjach.

Jeśli chodzi o konkretny przypadek, jeśli Zonenie ma być używany i / lub dostępny dla osób postronnych, należy uczynić pakiet klasowym prywatnym (i ewentualnie final), lub najlepiej użyć kolekcji, które już udostępnia Java. Są przetestowane i nie trzeba wymyślać koła na nowo. Zauważ, że nie przeszkadza to w używaniu asercji w całym kodzie, aby upewnić się, że wszystko działa zgodnie z oczekiwaniami.

afsantos
źródło
1
+1 za wzmiankę o Projekcie według umowy. Jeśli nie możesz całkowicie zakazać zachowania (a to jest trudne do zrobienia), przynajmniej wyraź, że nie ma gwarancji złego zachowania. Lubię również zgłaszać wyjątek IllegalStateException lub UnsupportedOperationException.
user949300,
@ user949300 Pewnie. Lubię wierzyć, że takie wyjątki zostały wprowadzone w sensowny sposób. Honorowanie umów wydaje się pasować do takiej roli.
afsantos,
16

Programowanie obronne jest bardzo dobrą rzeczą.
Aż zacznie przeszkadzać w pisaniu kodu. To nie jest taka dobra rzecz.


Mówiąc trochę bardziej pragmatycznie ...

Wygląda na to, że jesteś o krok od posunięcia się za daleko. Wyzwanie (i odpowiedź na twoje pytanie) polega na zrozumieniu, jakie są reguły biznesowe lub wymagania programu.

Korzystając z interfejsu API gry karcianej jako przykładu, istnieją środowiska, w których wszystko, co można zrobić, aby zapobiec oszustwom, ma kluczowe znaczenie. Mogą być w to zaangażowane duże kwoty prawdziwych pieniędzy, dlatego warto wprowadzić dużą liczbę czeków, aby upewnić się, że oszustwo nie będzie możliwe.

Z drugiej strony musisz pamiętać o zasadach SOLID, a zwłaszcza o pojedynczej odpowiedzialności. Poproszenie klasy kontenera o skuteczne sprawdzenie, dokąd idą karty, może być trochę za dużo. Lepiej może być warstwa audytu / kontrolera między kontenerem karty a funkcją odbierającą żądania przeniesienia.


W związku z tymi obawami musisz zrozumieć, które elementy interfejsu API są publicznie udostępnione (a tym samym narażone) w porównaniu z tym, co jest prywatne i mniej narażone. Nie jestem zwolennikiem „twardej powłoki zewnętrznej z miękkim wnętrzem”, ale najlepszym zwrotem twojego wysiłku jest utwardzenie zewnętrznej strony twojego API.

Nie sądzę, że zamierzony użytkownik końcowy biblioteki jest tak krytyczny wobec ustalenia, ile programów obronnych wprowadziłeś. Nawet w przypadku modułów, które piszę na własny użytek, wciąż staram się sprawdzić, aby upewnić się, że w przyszłości nie popełnię jakiegoś niezamierzonego błędu w wywołaniu biblioteki.


źródło
2
+1 dla „Dopóki nie zacznie przeszkadzać w pisaniu kodu”. Szczególnie w przypadku krótkoterminowych projektów osobistych kodowanie obronne może zająć znacznie więcej czasu, niż jest to warte.
Corey,
2
Zgadzam się, chociaż chciałbym dodać, że dobrze jest być / zdolnym / programować obronnie, ale ważne jest również, aby móc programować w sposób prototypowy. Możliwość zrobienia obu pozwoli ci wybrać najbardziej odpowiednią akcję, która jest o wiele lepsza niż wielu programistów, których znam, którzy są w stanie programować (w pewnym sensie) obronnie.
David Mulder,
13

Kodowanie obronne to nie tylko dobry pomysł na kod publiczny. To świetny pomysł na każdy kod, który nie jest natychmiast wyrzucany. Jasne, wiesz, jak ma się teraz nazywać , ale nie masz pojęcia, jak dobrze będziesz pamiętać te sześć miesięcy, kiedy wrócisz do projektu.

Podstawowa składnia języka Java zapewnia dużo wbudowanej obrony w porównaniu do języka niższego poziomu lub języka interpretowanego, takiego jak odpowiednio C lub JavaScript. Zakładając, że nazywasz jasno swoje metody i nie masz zewnętrznego „sekwencjonowania metod”, prawdopodobnie możesz uniknąć podania argumentów jako poprawnego typu danych i uwzględnienia rozsądnego zachowania, jeśli poprawnie wpisane dane mogą być nadal nieprawidłowe.

(Nawiasem mówiąc, jeśli karty zawsze muszą znajdować się w strefie, myślę, że otrzymujesz lepszy huk za to, że wszystkie karty w grze są powiązane przez kolekcję globalną z twoim obiektem gry, a strefa jest własnością każdej karty. Ale ponieważ nie wiem, co robią Twoje Strefy poza trzymaniem kart, trudno jest ustalić, czy jest to właściwe.)

DougM
źródło
1
Uważałem, że strefa jest własnością karty, ale ponieważ moje Karty działają lepiej jako niezmienne obiekty, zdecydowałem, że ten sposób będzie najlepszy. Dzięki za radę.
codebreaker
3
@codebreaker jedną rzeczą, która może pomóc w takim przypadku, jest kapsułkowanie karty w innym obiekcie. Ace of Spades jest tym, czym jest. Lokalizacja nie określa jej tożsamości, a karta prawdopodobnie powinna być niezmienna. Może mieć Strefę zawierającą karty: może mieć kartę CardDescriptorzawierającą kartę, jej lokalizację, status odkryty / zakryty, a nawet rotację w przypadku gier, które o to dbają. Są to wszystkie zmienne właściwości, które nie zmieniają tożsamości karty.
1

Najpierw stwórz klasę, która prowadzi listę Stref, abyś nie stracił Strefy ani zawartych w niej kart. Następnie możesz sprawdzić, czy transfer znajduje się w Twojej ZoneList. Ta klasa prawdopodobnie będzie rodzajem singletonu, ponieważ będziesz potrzebować tylko jednego wystąpienia, ale możesz chcieć później zestawów Stref, więc zachowaj opcje.

Po drugie, nie wprowadzaj kolekcji Zone ani ZoneList ani niczego innego, chyba że spodziewasz się, że będzie to potrzebne. Oznacza to, że jeśli strefa lub ZoneList zostaną przekazane do czegoś, co oczekuje kolekcji, a następnie wdrożyć go. Możesz wyłączyć kilka metod, każąc im zgłaszać wyjątek (UnimplementedException lub coś w tym rodzaju) lub zmuszając je po prostu do niczego. (Zastanów się naprawdę, zanim użyjesz drugiej opcji. Jeśli to zrobisz, ponieważ łatwo zauważysz, że brakuje Ci błędów, które mogłeś wcześniej wykryć).

Istnieją prawdziwe pytania dotyczące tego, co jest „prawidłowe”. Ale kiedy zrozumiesz, co to jest, będziesz chciał robić rzeczy w ten sposób. Za dwa lata zapomnisz o tym wszystkim, a jeśli spróbujesz użyć kodu, zirytujesz się facetem, który napisał go w tak sprzeczny z intuicją sposób i niczego nie wyjaśnił.

RalphChapin
źródło
2
Twoja odpowiedź koncentruje się trochę za bardzo na danym problemie zamiast na szerszych pytaniach, jakie OP zadaje ogólnie na temat programowania obronnego.
Właściwie przekazuję Strefy metodom, które pobierają Kolekcje, więc implementacja jest konieczna. Ciekawym pomysłem jest jednak rejestr stref w grze.
codebreaker
@ GlenH7: Uważam, że praca z konkretnymi przykładami często pomaga bardziej niż teorii abstrakcyjnej. OP zapewnił dość interesujący, więc poszedłem z tym.
RalphChapin,
1

Defensywne kodowanie w projektowaniu API ogólnie polega na sprawdzaniu poprawności danych wejściowych i starannym wyborze odpowiedniego mechanizmu obsługi błędów. Warto również zwrócić uwagę na inne odpowiedzi, o których wspominają.

W rzeczywistości nie o to chodzi w twoim przykładzie. Ograniczasz swoją powierzchnię API z bardzo konkretnego powodu. Jak wspomina GlenH7 , kiedy zestaw kart ma być używany w rzeczywistej grze, na przykład z talią („używaną” i „nieużywaną”), na przykład stołem i rękami, zdecydowanie chcesz umieścić czeki, aby upewnić się, że każda karta z zestawu jest obecna tylko raz.

To, że zaprojektowałeś to z „strefami”, jest arbitralnym wyborem. W zależności od implementacji (strefa może być tylko ręką, talią lub stołem w powyższym przykładzie) może to być bardzo dokładny projekt.

Jednak ta implementacja brzmi jak wyprowadzony typ bardziej Collection<Card>podobnego zestawu kart z mniej restrykcyjnym interfejsem API. Na przykład, jeśli chcesz zbudować kalkulator wartości kart lub sztuczną inteligencję, z pewnością chcesz mieć swobodę wyboru, które karty i ile kart iterujesz.

Warto więc ujawnić taki restrykcyjny interfejs API, jeśli jedynym celem tego interfejsu API jest upewnienie się, że każda karta zawsze znajduje się w strefie.

CodeCaster
źródło