Code Contracts–programowanie defensywne

Code Contracts stanowią kolejny mechanizm ułatwiający programowanie defensywne – sposób wytwarzania oprogramowania odporny na wszelkie niespodziewane wartości (NULL, dzielenie przez zero, wartości skrajne itd.) Rozważmy klasyczny przykład – funkcja dzielenia:

private float Divide(float dividend, float divisor)
{
  if (divisor == 0)
      throw new DivideByZeroException();
  return dividend / divisor;
}

Funkcja jest zaimplementowana poprawnie – sprawdza czy divisor nie jest zerem. Są jeszcze inne skrajne przypadki jak float.NaN lub float.Inf ale ograniczmy się chwilowo tylko do dzielenia przez zero. Code Contracts umożliwia zdefiniowanie warunków wejściowych, wyjściowych oraz tzw. invariant w prostszy sposób niż zwykła walidacja za pomocą instrukcji IF. Od wersji Visual Studio 2010, CS zostały wbudowane w .NET, wcześniej należało instalować osobną paczkę. Zostały one opracowane w Microsoft Research (patrz Spec#). Z użyciem Code Contracts powyższa funkcja wygląda następująco:

private float Divide(float dividend, float divisor)
{
  Contract.Requires(divisor != 0);

  return dividend / divisor;
}

Składnia trochę prostsza ale to chyba niewystarczający argument aby przekonać się do Code Contracts. Osobiście w Code Contracts (CS) lubię statyczne sprawdzanie warunków. Niestety trzeba ściągnąć osobną instalkę ale warto…  Po instalacji otwórzmy właściwości projektu i przejdźmy do zakładki Code Contracts:

1

 

W opcjach m.in. możemy ustawić sprawdzanie kontraktów runtime i static. Uruchomienie aplikacji z divisor równym zero spowoduje wyrzucenie wyjątku przez kontrakt:

image

 

Możemy ustawić np. pełne sprawdzanie kontraktów dla wersji DEBUG oraz pominięcie dla wersji RELEASE (wydajność). Statyczne sprawdzanie działa w tle i wszelkie wykryte błędy zostaną pokazane w formie warning:

2

Sprawdźmy jak zadziała statyczne sprawdzanie dla poniższego kodu:

Random random = new Random();
Divide(3, randim.Next());

Oczywiście divosor w tym przypadku może zostań wylosowany zero. Podobna sytuacja przytrafi się gdy użytkownik przekazuje tą wartość za pomocą UI. Statyczne sprawdzanie wyświetli komunikat “CodeContracts: requires unproven: divisor !=0”:

3

Dzięki poprawnemu zdefiniowaniu CS możemy uniknąć trudnych do zidentyfikowania błędów. Nie musimy w końcu pisać kodu, który obsługuje wszystkie zakresy parametrów. Jeśli wiemy, że nasza funkcja nigdy nie będzie wywołana np. z wartością NULL to po co implementować stosowną obsługę? Wystarczy zdefiniować kontrakt a następnie statyczne lub dynamiczne sprawdzanie zidentyfikuje czy aktualny kod pozwoli na złamanie kontraktu. Nie musimy pisać wszechstronnych funkcji, za to musimy być świadomi problemów z tym związanych i jasno sprecyzować nasze założenia (Requieres, Assert, Assume) na podstawie których projektowaliśmy funkcję.

Co do warunków wstępnych warto również wspomnieć, że można nakazać wyrzucić stosowny wyjątek w przypadku złamania kontraktu:

  Contract.Requires<DivideByZeroException>(divisor != 0);

Kolejną kwestią są warunki wyjściowe, które sprawdzane są po zakończeniu działania funkcji. Definiuje się je jednak na początku ciała metody. Zobaczmy to na przykładzie:

private int Add(int numberA,int numberB)
{
  Contract.Ensures(numberA > 5);
  Contract.Ensures(Contract.OldValue(Name) == Name);
  Contract.Ensures(Contract.Result<int>() == numberA+numberB);
  Name+="test";

  return numberA + numberB;
}

Pierwszy warunek gwarantuje, że numberA po zakończeniu funkcji będzie większy od 5. Kolejny gwarantuje, że pole Name nie zostanie zmienione podczas wykonywania funkcji Add. Ostatni, że wynik funkcji będzie wynosił numberA+numberB. Podobnie jak wartość zwracana (Result) można walidować parametry wyjściowe (out,ref) za pomocą Contract.ValueAtReturn. Analogicznie do Contract.Requires można również wyrzucić konkretny wyjątek w przypadku złamania kontraktu:

Contract.EnsuresOnThrow<InvalidOperationException>(numberA > 5);

Invariant stanowią warunki, które sprawdzane są po zakończeniu każdej metody publicznej. Możemy za pomocą invariant zdefiniować warunki, które powinny być prawdziwe zawsze, niezależnie od tego, która aktualnie metoda jest wykonywana:

[ContractInvariantMethod]
private void Invariant () 
{
    Contract.Invariant ( this.Width >= 0 );
    Contract.Invariant ( this.Height >=500);
}

Jak widać invariant definiuje się za pomocą osobnej funkcji ze specjalnym atrybutem. Powyższej metody nie musimy nigdzie wywoływać – za to jest odpowiedzialny CS.

Na zakończenie wypada wspomnieć również o Contract.Assert oraz Contract.Assume. Contract.Assert jest bardzo podobne do Debug.Assert. Umożliwia po prostu sprawdzanie warunków w określonych momentach kodu np:

Random random = new Random();
int divisor = random.Next();

Contract.Assert(divisor != 0);
Divide(10, divisor);
//........
private float Divide(float dividend, float divisor)
{
  Contract.Requires(divisor != 0);

  return dividend / divisor;
}

W powyższym przykładzie za pomocą asercji sprawdzamy czy divisor jest różny od zera. Static checker od razu zwróci uwagi ponieważ nie można zagwarantować powyższej asercji – co w przypadku gdy Next zwróci 0? Metoda Divide również zwróci uwagę ze względu na Contract.Requires

Assume jest funkcją bardzo podobną a w przypadku runtime checking identyczną. Różnica istnieje tylko w statycznym sprawdzaniu. Załóżmy, że  ze nasz generator Random zawsze zwraca wartości > 0 i możemy przyjąć takie założenie. W takim przypadku Divide i kontrakt Requires nie powinien zwracać uwagi. Za pomocą Assume możemy zdefiniować nasze założenie:

Random random = new Random();
int divisor = random.Next();

Contract.Assume(divisor != 0);
Divide(10, divisor);

Powyższy kod nie zwróci żadnej uwagi. Oczywiście jak wspomniałem, w przypadku Runtime Assume działa jak Assert i nasze założenia zostaną i tak zweryfikowane. Różnica jest tylko na etapie Design time. Może lepszym przykładem byłoby użycie coś w stylu FetchData niż Random. Bardzo prawdopodobne, że FetchData zawsze zwróci wartość różną od 0 (w bazie mamy np. same dodatnie wartości) i wtedy warto zdefiniować assumption aby nie łamać wszystkich następnych kontraktów w kodzie. Assume po prostu na etapie design time dodaje pewny warunek do kolekcji faktów.

3 thoughts on “Code Contracts–programowanie defensywne”

Leave a Reply

Your email address will not be published.