kod umów / stwierdzeń: co ze zduplikowanymi czekami?

10

Jestem wielkim fanem pisania stwierdzeń, umów lub wszelkiego rodzaju czeków dostępnych w języku, którego używam. Jedną z rzeczy, która mnie trochę niepokoi, jest to, że nie jestem pewien, jaka jest powszechna praktyka postępowania z duplikatami kontroli.

Przykładowa sytuacja: najpierw piszę następującą funkcję

void DoSomething( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  //code using obj
}

potem kilka godzin później piszę inną funkcję, która wywołuje pierwszą. Ponieważ wszystko jest jeszcze w pamięci, postanawiam nie powielać kontraktu, ponieważ wiem, że DoSomethingsprawdzę już, czy nie ma już obiektu zerowego:

void DoSomethingElse( object obj )
{
  //no Requires here: DoSomething will do that already
  DoSomething( obj );
  //code using obj
}

Oczywisty problem: DoSomethingElseteraz zależy od DoSomethingsprawdzenia, czy obiekt nie jest pusty. Więc powinienem DoSomethingkiedykolwiek zdecydować, że nie będę już sprawdzać, albo jeśli zdecyduję się użyć innej funkcji, obiekt może nie być już sprawdzany. Co w końcu prowadzi mnie do napisania tej implementacji:

void DoSomethingElse( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  DoSomething( obj );
  //code using obj
}

Zawsze bezpieczny, nie martw się, z wyjątkiem tego, że jeśli sytuacja się rozwinie, ten sam obiekt może być sprawdzony wiele razy i jest to forma powielania i wszyscy wiemy, że nie jest tak dobrze.

Jaka jest najczęstsza praktyka w takich sytuacjach?

stijn
źródło
3
ArgumentBullException? To nowy :)
CV z
lol @ moje umiejętności pisania na klawiaturze ... Zmienię to.
stijn

Odpowiedzi:

13

Osobiście sprawdziłbym wartość null w każdej funkcji, która się nie powiedzie, jeśli otrzyma wartość null, a nie w żadnej funkcji, która by tego nie zrobiła.

Więc w powyższym przykładzie, jeśli doSomethingElse () nie musi wyłuskiwać obj, to nie sprawdzałbym obj tam.

Jeśli DoSomething () powoduje dereferencję obj, powinien sprawdzić, czy nie ma wartości null.

Jeśli obie funkcje odznaczają to, to powinny one sprawdzić. Więc jeśli DoSomethingElse dereferencje obj, to powinno sprawdzić, czy null, ale DoSomething powinien również sprawdzić, czy null, ponieważ może być wywołany z innej ścieżki.

W ten sposób możesz pozostawić kod dość czysty i nadal gwarantować, że kontrole są we właściwym miejscu.

Luke Graham
źródło
1
W pełni się zgadzam. Warunki wstępne każdej metody powinny być niezależne. Wyobraź sobie, że przepisujesz w DoSomething()taki sposób, że warunek wstępny nie jest już wymagany (mało prawdopodobne w tym konkretnym przypadku, ale może się zdarzyć w innej sytuacji), i usuwasz sprawdzanie warunków wstępnych. Teraz niektóre pozornie całkowicie niezwiązane metody zostały zerwane z powodu brakującego warunku wstępnego. Zrobię trochę duplikacji kodu, aby uzyskać przejrzystość w przypadku takich dziwnych błędów, jak chęć zaoszczędzenia kilku linii kodu, każdego dnia.
CVn
2

Świetny! Widzę, że dowiedziałeś się o kontraktach Code for .NET. Kontrakty kodowe wykraczają daleko poza przeciętne stwierdzenia, których najlepszym przykładem jest sprawdzanie statyczne . Może to nie być dostępne, jeśli nie masz zainstalowanego programu Visual Studio Premium lub nowszego, ale ważne jest, aby zrozumieć zamiar, który za tym stoi, jeśli zamierzasz korzystać z kontraktów kodowych.

Kiedy zastosujesz umowę do funkcji, dosłownie jest to umowa . Ta funkcja gwarantuje zachowanie zgodnie z umową i gwarantuje się, że będzie używana tylko zgodnie z umową.

W podanym przykładzie DoSomethingElse()funkcja nie spełnia warunków umowy określonych przez DoSomething(), ponieważ można przekazać wartość NULL, a moduł sprawdzania statycznego wskaże ten problem. Sposobem na rozwiązanie tego jest dodanie tej samej umowy DoSomethingElse().

Teraz oznacza to, że nastąpi duplikacja, ale ta duplikacja jest konieczna, gdy zdecydujesz się udostępnić funkcjonalność w dwóch funkcjach. Funkcje te, choć prywatne, mogą być wywoływane również z różnych miejsc w klasie, więc jedynym sposobem na zagwarantowanie, że z każdego wywołania argument nigdy nie będzie zerowy, jest powielenie umów.

To powinno sprawić, że ponownie rozważysz, dlaczego podzieliłeś zachowanie na dwie funkcje. Zawsze uważałem ( wbrew powszechnemu przekonaniu ), że nie należy dzielić funkcji, które są wywoływane tylko z jednego miejsca . Ujawnienie enkapsulacji poprzez zastosowanie umów staje się jeszcze bardziej widoczne. Wygląda na to, że znalazłem dodatkową argumentację dla mojej sprawy! Dziękuję Ci! :)

Steven Jeuris
źródło
w odniesieniu do ostatniego akapitu: w rzeczywistym kodzie obie funkcje były członkami dwóch różnych klas i dlatego są podzielone. Poza tym tyle razy byłem w następującej sytuacji: napisz długą funkcję, zdecyduj, aby jej nie dzielić. Później dowiedz się, że część logiki jest zduplikowana w innym miejscu, więc podziel ją. Lub rok później przeczytaj go ponownie i okaż, że jest nieczytelny, więc podziel go mimo to. Lub podczas debugowania: funkcje podzielone = mniej uderzania w klawisz F10. Jest więcej powodów, więc osobiście wolę podział, nawet jeśli oznacza to, że czasami może być zbyt ekstremalny.
stijn
(1) „Później ustal, że część logiki jest zduplikowana gdzie indziej” . Dlatego uważam, że ważniejsze jest, aby zawsze „rozwijać się w kierunku API” niż po prostu dzielić funkcje. Nieustannie myśl o ponownym użyciu, nie tylko w obecnej klasie. (2) „Czy rok później przeczytaj go ponownie i okaże się nieczytelny” Ponieważ funkcje mają nazwy, czy to jest lepsze? Masz jeszcze większą czytelność, jeśli użyjesz tego, co komentator na moim blogu opisał jako „akapity kodowe”. (3) „Dzielone funkcje = mniej walić w klawisz F10” ... Nie rozumiem dlaczego.
Steven Jeuris,
(1) uzgodniona (2) czytelność jest osobistą preferencją, więc tak naprawdę nie jest to dla mnie przedmiotem dyskusji. (3) przejście przez funkcję 20 linii wymaga wciśnięcia F10 20 razy. Przejście przez funkcje, które mają 10 z tych linii w funkcji podziału, daje mi wybór, że muszę naciskać F10 tylko 11 razy. Tak, w pierwszym przypadku mogę wstawić punkty przerwania lub wybrać „skok do kursora”, ale to więcej wysiłku niż w drugim przypadku.
stijn
@stijn: (2) zgodził się; p (3) dziękuję za wyjaśnienie!
Steven Jeuris,