Aktualizacja 2009-06-04 (oryginał z 2009-03-24 01:17). To, co mnie zawsze denerwuje w postach w sieci to to iż 30% z nich odwołuje się do rzeczy, które nie istnieją gdyż od wersji beta/ctp/rc uległy zmianie. Dlatego też stwierdziłem, iż zaktualizuje ten post o kilka drobnych zmian, które weszły w życie 19 maja 2009 roku. Na przekreślone czerwono rzeczy, które zostały usunięte od ostatniej wersji postu. Na zielono rzeczy, które zostały dodane od ostatniej wersji postu. Na czarno rzeczy, które nie uległy zmianie od ostatniej wersji postu.

Wstęp

Od kiedy zacząłem programować zawsze miałem ten sam problem, jak efektywnie oprogramować walidację parametrów metody? Czy czegoś nie pominąłem? Czy dokumentacja jest aktualna? Ostatnio nawet Nuwanda stworzył plugin dla R#, którego celem jest ułatwienie zarządzania dokumentowaniem wyjątków w kodzie.

Jednak tak jak i moja pierwsza przygoda z Mac tak też zakończyła się moja przygoda z R#. Od razu dostałem critical error, zamknąłem szczękę i stwierdziłem, że tego używać nie będę ;) Tak, dla niewierzących, dostałem taki błąd w R#, Procent świadkiem bo mu stack trace przesłałem :)

W czasie, kiedy coraz częściej piszemy kod, który jest używany przez kolejne osoby i w kolejnych projektach musimy więcej czasu poświęcić na to by poprawnie obsłużyć wyjątki, walidację, weryfikację a na końcu lub na początku błędy.

Jest to tym trudniejsze im większy jest projekt, im więcej zmian w nim robimy. Kto się spotkał z sytuacją że XML Documentation zwracała mu ponad 500 błędów/braków? Nawet kiedy piszę się dokumentację od razu, to potem nagle trzeba coś na szybko zmienić i nie aktualizujemy dokumentacji. Miło, że mamy takie toole jak R# czy nawet weryfikator dokumentacji w VS, ale one za nas nic nie zrobią, tylko poinformują – kliknij tam by coś poprawić.

Oczywiście, możemy zawsze skorzystać z GhostDoc, ale to znów wymaga klikania przez nas i aktualizacji dokumentacji w zależności od zmian w kodzie.

Także dość często sygnatura naszej metody ulega zmianie, dochodzą nowe parametry, które trzeba przetestować, albo zmienia się ich wymaganie co do wartości jakie mogą przyjmować. Czy mieliście sytuację, iż początek naszej metody to 8 ifów sprawdzających czy ciąg znaków jest pusty? A czy klient wie że każdy z tych parametrów jest przez was weryfikowany?

Czy wywołując metodę zastanawialiście się jakie wartości ona może wam zwrócić? Czy aby na pewno to jest ta metoda, którą wy chcecie wywołać?

Ja osobiście miałem dużo takich sytuacji i sam nie raz się głowiłem jak mam opisać/przekazać klientowi co dokładnie robi metoda by potrafił on z niej od razu skorzystać a nie brał za słuchawkę i dzwonił, lub pisał na forum pytania o „tego autora który napisał tą książkę w zeszłym roku, po tym jak jedna z jego ekranizacji dostała nagrodę Emmy”.

Tutaj właśnie znajdują zastosowania kontrakty! A ich implementacja wykonana przez MS, zezwoli nie tylko na utrzymanie poprawnej dokumentacji wraz z informacją jakie wartości może metoda przyjmować ale także jakie wartości ona zwróci nam kiedy spełnimy warunek początkowy – poprawny parametr. Nie trzeba pisać dokumentacji, a ni nic. Wystarczy, że określimy warunki brzegowe.

Jeżeli jesteśmy leniwi ;) to można włączyć analizator statyczny, który od razu nas poinformuje, że w danej metodzie przydałoby się stworzyć walidację gdyż na tym właśnie kontrakt polega –na formalnej weryfikacji i specyfikacji.

A dzięki integracji z PEX, testowanie kodu będzie czystą przyjemnością. Kontrakt określi nam, jakiego typu parametrów się spodziewa, więc PEX może łatwo sprawdzić czy inne parametry nie spowodują błędnego wykonania kodu, jak i czy ten parametr, który oczekujemy spełnia nasz wyjściowy warunek.

Dlaczego o tym piszę i dlaczego wstęp zajmuje aż stronę :) A to dlatego iż coraz więcej jest technologii a coraz mniej informacji do czego ona się może przydać. Nawet ostatnio słyszałem opinie o kilku fajnych produktach, które może wyjdą, ale mają jeden wielki problem, nawet architekt danego produktu nie wie do czego go można wykorzystać bo słowo „wszystkiego” nikogo niezadowala ;)

Co to jest – Code Contracts?

Code Contracts a dokładnie Design By Contract (nazwa zastrzeżona przez Interactive Software Engineering, właściciela języka Eiffel) to metodyka tworzenia API w sposób formalny, dokładny oraz weryfikowalny. Działa ona głownie na zasadzie logiki Hoara, która określić można za pomocą wyrażenia {P} C {Q} gdzie P (pre-condition) jest warunkiem który musi być spełniony by C (command) mogło zajść i zakończyć swoje działanie tak by przeszło weryfikacje Q (post-condition). Czyli mówiąc językiem bardziej zrozumiałym, jeżeli chcemy zarezerwować pokój w hotelu, to:

  1. Pre-condition: Musimy mieć kartę kredytową by móc zarezerwować pokój;
  2. C: Hotel wykonuje rezerwację;
  3. Q: Hotel zarezerwował nam pokój a liczba wolnych pokoi zmniejszyła się o 1.

Czyli, pre-condition, to jest to co klient korzystający z naszego API musi spełnić. Command to coś co my wykonujemy by spełnić post-condition. Jeżeli w jakimkolwiek przypadku nastąpi naruszenie zasady, zostanie przekazana nam informacja zwrotna, na przykład: brak miejsc, karta nie ważna, critical error ;)

Dodatkowo kontrakty, oprócz logiki Hoara, dostarczają nam tak zwane inwarianty. Inwariant jest to warunek, który musi być prawdziwy zarówno na początku jak i na końcu. Wracając do naszego przypadku, inwariantem przy rezerwacji pokoju hotelowego może być istniejący budynek hotelu. Musi on istnieć w trakcie naszej rezerwacji jak po jej zakończeniu, gdyż w przeciwnym wypadku nie moglibyśmy w naszym pokoju zamieszkać.

Wikipedia, trochę bardziej się rozpisuje na temat Design By Contract, dodając do logiki Hoara iinwariantów, formalną weryfikację i formalną specyfikację czyli tak naprawdę opisanie P i Q w trochę bardziej zawiły sposób ;)

Dodatkowo, jak sama Wikipedia wskazuje a ja się z tym zgadzam ;) DbC odróżnia się odprogramowania defensywnego tym iż w DbC wszystko jest jasne, klient wie co ma przekazać by dostawca mógł wykonać operację i zwrócić klientowi to czego on się spodziewa.

Jak działają Code Contracts w .NET?

No dobrze, to tyle słów wstępu na temat co to w ogóle jest ;) Pora przejść do tego co MS stworzył i to co jest już dostępne dla .NET Framework 3.5, zaś co będzie częścią .NET Framework 4.0. Mowa oczywiście o Code Contracts! ;) Tak jakbym już o tym nie mówił ;)

.NET Framework 4.0 będzie zawierał zaimplementowane kontrakty bezpośrednio w mscorlib.dll, zaś aktualnie jeżeli z tego chcemy skorzystać to Microsoft Research opublikował dla nas bibliotekę, którą możemy załączyć do projektu i z niej skorzystać. Występuje ona w dwóch różnych wersjach:

  1. Wersja akademicka, bez licencji na tworzenie oprogramowania komercyjnego;
  2. Wersja z licencją na tworzenie oprogramowania komercyjnego.

Jest to pakiet MSI, który nie tylko instaluje bibliotekę ale także integruje się z Visual Studio, dodając nam nową zakładkę w właściwościach projektu.

To co uzyskujemy po zainstalowaniu MSI to:

  • Biblioteka Microsoft.Contracts.dll, którą musimy załączyć do projektu w celu możliwości korzystania z kontraktów (występuje na zakładce .NET więc nie trzeba jej szukać za pomocą Browse, nazywa się Microsoft Contracts Library, zaś znajduje się w przestrzeni nazw System.Diagnostic.Contracts);
  • Integracja z ustawieniami projektu, dzięki czemu możemy włączyć lub wyłączyć analizę pod względem poprawności DbC w naszym kodzie;
  • Analizator statyczny kodu weryfikujący kod, który piszemy by był zgodny z zasadami DbC – analiza wykonywana jest w trakcie kompilacji;
  • Analizator run-time, który działa w trakcie uruchomienia aplikacji.

W planach zaś twórcy Code Contracts chcą dodać automatyczne generowanie dokumentacji w zależności od użytych kontraktów w kodzie i lepszą integrację z intellisense (tak by jak będziemy korzystać z API od razu pojawiała nam się informacja, że znów coś źle robimy;)).

Zanim jeszcze może przejdę do omawiania już czystego Code Contracts, warto wspomnieć o tym, iż sam projekt wywodzi się z języka Spec#, stworzonego przez Microsoft Research (mowa o CC, DbC wywodzi się od języka Eiffel). Jest to tak naprawdę C#, który został rozszerzony o kilka dodatkowych słów kluczowych, konstrukcji językowych, nie nullowalnych typów danych itp. w celu dostarczenia kontraktów bezpośrednio w języku. Dzięki czemu grupa MR miała dobre pole do trenowania i wyciągania wniosków, co może się sprawdzić a co nie, jak coś zaimplementować i czego nie implementować, bo mija się z celem. W C# 4.0 to nie zostało zaimplementowane ze względów dość zrozumiałych, nie ma sensu tego dostarczać per język, lepiej dostarczyć środowisko/platformę, którą każdy język (VB.NET, C#, J# itp.) będzie mógł wykorzystać.

Taka małą uwaga, nie instalujcie Spec# i Code Contracts jednocześnie, ich zakładki do właściwości projektów się gryzą i ani to, ani to poprawnie działać nie będzie :)

No dobrze, koniec :) pora na trochę szczegółów :)

CC składa się z metod statycznych, nie tak jak by większość mogłaby przypuszczać z atrybutów, MR zdecydował się na taką implementację ze względu na:

  • Wsparcie IDE – gdyby zastosowali kontrakty jako atrybuty nie dałoby się w trakcie pisania tworzyć odpowiedniego intellisense dla metod;
  • Parsowanie danych – własne atrybuty mają ograniczenie, co do wartości, jakie mogą być tam przekazywane, co prowadzi w końcu do zapisania wartości, jako ciągi znaków, które potem muszą być parsowane, przez co duplikuje się funkcjonalność którą kompilator już posiada;
  • Wsparcie run-time – bez wsparcia aplikacji do przepisywania kodu binarnego, atrybuty nie mogą być weryfikowane w trakcie działania aplikacji, przez co jeżeli byśmy chcieli by nasz kontrakt był weryfikowany w trakcie działania aplikacji, musielibyśmy albo z duplikować funkcjonalność kontraktu w kodzie, albo stworzyć aplikację do przepisywania kodu binarnego.

No dobrze, skoro nasze kontrakty są metodami statycznymi to jak z nich korzystamy? Dość prosto, zaraz po definicji metody umieszczamy odpowiednie wywołanie metody z CC. Przykładowy kontrakt sprawdzający czy parametr przekazany do metody jest większy niż 10 i mniejszy niż 20 a parametr zwracany z metody mniejszy niż 10, wygląda następująco:

public int Test(int a)
{
    Contract.Requires(a > 10);
    Contract.Requires(a < 20);
    Contract.Ensures(Contract.Result<int>() < 10);
 
    a -= 10;
 
    return a;
}

Jak zauważyliście kontrakty nie zależnie od tego czy są pre czy post, są deklarowane na początku metody. To co robi MR, to bierze kod napisany i skompilowany do postaci IL i wykonuje na nim operacje przepisania, przestawiając tak post-conditions by były one umieszczone na końcu kodu. Powyższy kod w .NET Reflector wygląda następująco:

public int Test(int a)
{
    __ContractsRuntime.Requires(a > 10, null, "a > 10");
    __ContractsRuntime.Requires(a < 20, null, "a < 20");
    a -= 10;
    int CS$1$0000 = a;
    int Contract.Result<int>() = CS$1$0000;
    __ContractsRuntime.Ensures(Contract.Result<int>() < 10, null, "Contract.Result<int>() < 10");
    this.ObjectInvariant();
    return Contract.Result<int>();
}

Czyli nasze pre-conditions zostały zamienione na wywołanie metody Requires, która sprawdza pierwszy parametr, który jeżeli nie zakończy się jako true to zostaje zwrócony błąd z opisem po prawej stronie. Zaś nasz zwrot return został zamieniony na kilka dodatkowych linijek, pierwsza tworzy zmienną tymczasową, która przypisuje do „wyniku” kontraktu wartość zwracaną. Wynik kontraktu jest następnie weryfikowany i jeżeli on przejdzie weryfikacje sprawdzany jest inwariant. Na końcu zwracana jest wartość, którą my zwrócilibyśmy bez weryfikacji.

Plus rozwiązania tego za pomocą przepisywania kodu, jest taki iż możemy sami sobie napisać naszą własną metodę, która będzie za to odpowiedzialna, czyli jeżeli chcemy stworzyć różne buildy w różnych systemach, możemy wykorzystać nasz własny „przepisywacz” by spełnić warunki wymagane przez oprogramowanie firmy trzeciej – np.: nie wyrzucać wyjątków a jedynie wypisać je na konsole:

public static class RuntimeRewriterMethods
{
    [DebuggerStepThrough]
    public static void Requires(bool cond, string userMsg, string condText)
    {
        if (!cond)
            Console.WriteLine("no nie... chyba nikt już nic nie czyta");
    }
 
    // nowa metoda
    [DebuggerStepThrough]
    public static void Requires<TException>(bool cond, string userMsg, string condText) where TException : Exception
    {
        if (!cond)
            Console.WriteLine("no nie... chyba nikt już nic nie czyta");
    }
 
    [DebuggerStepThrough]
    public static void Ensures(bool cond, string userMsg, string condText)
    {
        if (!cond)
            Console.WriteLine(condText);
    }
    [DebuggerStepThrough]
    public static void Assert(bool cond, string userMsg, string condText)
    {
        if (!cond)
            Console.WriteLine(userMsg);
    }
    [DebuggerStepThrough]
    public static void Assume(bool cond, string userMsg, string condText)
    {
        if (!cond)
            Console.WriteLine("zakladanie czegos konczy sie wlasnie o TAK");
    }
    [DebuggerStepThrough]
    public static void Invariant(bool cond, string userMsg, string condText)
    {
        if (!cond)
            Console.WriteLine("a to mialo byc prawda i tylko prawda");
    }
}

Tak naprawdę to, co robi VS po tym jak z builduje normalnie naszą aplikację to wykonuje polecenie:

"C:Program FilesMicrosoftContractsBinccrewrite" /rewrite "/libpaths:C:Program FilesMicrosoftContractsContracts" "C:UsersGutekDesktopSamplesApiProtocolsbinDebugApiProtocols.exe"

Nie będę wchodził w szczegóły – na końcu dam kilka źródeł skąd można przeczytać więcej na ten temat. Chodziło mi tylko o to by pokazać iż jest to „przepisywacz” :)

Koniec „teorii” pora na trochę praktyki. Nie będzie jej tutaj aż tak dużo jakby można przypuszczać a to ze względu na to iż źródła na które was skieruje zajmują 25 stron A4 łącznie i po ich przeczytaniu :) będziecie masta :)

To co IMO jest najważniejsze to opanowanie metod, które opisałem poniżej.

Requires – tak zwane pre-condition

Za każdym razem kiedy chcemy by do naszej metody były przekazywane określone parametry, spełniające nasze oczekiwanie piszemy:

Contract.Requires(condition);
Contract.Requires(condition, "error message");

Nasz warunek jest to dowolne wyrażenie zawierające parametr metody i zwracające true lubfalse. W przypadku ewaluowania wyrażenia do false zostanie nam zwrócony błąd w postaci domyślnej (przykład a < 20), lub dowolnej jaką przedstawimy jako ciąg znaków.

cc01

Requires<TException> – tak zwane pre-condition z własnym wyjątkiem NOWE

Różnica pomiędzy Requires a Requires<TException> jest taka, iż deklarujemy typ wyjątku, jaki chcemy wyrzucić:

Contract.Requires<ArgumentNullException>(condition);
Contract.Requires<ArgumentNullException>(condition, "param name");

Nasz warunek jest to dowolne wyrażenie zawierające parametr metody.

RequiresAlways – mhhhh :) deprecated

Prawie niczym się nie różni od Requires. Dosłownie mówiąc wykonuje to samo dla każdego typu kompilacji i tu jest właśnie pies pogrzebany :) W zależności od ustawień kompilacji Requires będzie brany pod uwagę lub nie, zaś RequiresAlways zawsze będzie brany :) Należy więc z niego korzystać rozsądnie :)

Ensures – nasze Q (post-condition)

Tak jak możemy pisać pre-conditions za pomocą Requires lub RequiresAlways, tak nasze post-conditions piszemy wykorzystując metodę Ensures:

Contract.Ensures(condition);
Contract.Ensures(condition, "error message");

Tak jak w Requires, warunek musi być spełniony by nie został zwrócony błąd.

cc02

Dla Ensures, dostępne są dodatkowe metody, które możemy wywołać podczas wykorzystania warunku:

  • Contract.Result<T>() – zawiera wartość zwracaną z metody, jeżeli chcemy sprawdzić czy wartość zwracana spełnia jakiś warunek to dzięki tej metodzie możemy to zrobić. T oznacza typ zwracanych danych. Oczywiście kiedy nic nie zwracamy, nie możemy skorzystać z tej metody ;)
  • Contract.OldValue<T>(T value) – umożliwia weryfikację czy poprzedni stan danej wartości jest równy/nie równy nowemu stanowi, dla przykładu modyfikujemy własność klasy A tak by była równa B, a B równa A, więc to co my możemy zrobić to napisać kod:
Contract.Ensures(Contract.OldValue<int>(this.A) == this.B && this.A ==Contract.OldValue<int>(this.B));
  • Contract.ValueAtReturn<T>(out T value) – w przypadku kiedy nasza metoda korzysta z parametru wyjściowego, możemy wykożystać ValueAtReturn by sprawdzić czy parametr wyjściowy będzie zawierał odpowiednią wartość. W tym przypadku ani OldValue ani Result nie zadziała, jedyną opcją sprawdzenia parametru wyjściowego jest wykonanie metody ValueAtReturn.

EnsuresOnThrow – nasze Q (post-condition) dla wyjątków

Czasami niezależnie jak bardzo się staramy mogą wystąpić różne wyjątki. Możemy na przykład korzystać z biblioteki firmy trzeciej, która z kontraktami ma tyle wspólnego, co reklama mortadeli z Mango. Jednak dostawca był na tyle miły, iż powiedział nam, że może on wyrzucić takie wyjątki mimo, że operacja zakończyła się poprawnie. Za pomocą EnsuresOnThrow możemy zagwarantować, żeby nasz post-condition był spełniony mimo wystąpienia błędu TException:

Contract.EnsuresOnThrow<TException>(condition)
Contract.EnsuresOnThrow<TException>(condition, "error message")

Korzystanie jednak z tej metody niesie za sobą niebezpieczeństwa – TException dotyczy wszystkich wyjątków wraz z uwzględnieniem dziedziczenia, czyli łapanie Exception może spowodować wychwycenie takich wyjątków jak StackOverflowException czy RuntimeException, które np.: nie powinny być przechwycone. W EnsuresOnThrow stosujemy tą samą zasadę jak przy try/catch/finally – łapiemy szczegółowo a nie ogółowo :)

EndContractBlock – kompatybilność wstecz :)

Tak jak wspomniałem już we wstępnie. Do tej pory pisaliśmy zawsze nasze ukochane ify:

if(a <= 10 || a >= 20)
{
	throw new ArgumentOutOfRangeException("a", a, "10 < a < 20");
}

Wraz z wprowadzeniem kontraktów, pisanie ich… mija się z celem, ale co zrobić kiedy mamy je już popisane i nie chcemy tego kasować bo ktoś stworzył CC? MR dostarczyli nam metodę EndContractBlock, którą stosujemy po wszystkich naszych ifach a która zamienia nasze ify na kontrakty, czyli to co mamy powyżej zapisujemy teraz tak:

if(a <= 10 || a >= 20)
{
	throw new ArgumentOutOfRangeException("a", a, "10 < a < 20");
}
Contract.EndContractBlock();

To co uzyskujemy w .NET Reflector to:

bool _preConditionHolds;
if ((a <= 10) || (a >= 20))
{
	_preConditionHolds = false;
}
else
{
	_preConditionHolds = true;
}
if (!_preConditionHolds)
{
	throw new ArgumentOutOfRangeException("a", a, "10 < a < 20");
}

Jest jednak pewna zasada, której trzeba się trzymać inaczej „kompatybilność wstecz” nie zadziała.

  1. Po pierwsze w naszej metodzie nie może być nic wcześniej niż nasze ify;
  2. Po drugie ify nie mogą mieć klauzuli else;
  3. Po trzecie ify muszą się kończyć throw new TException;
  4. Po czwarte na końcu ifów a przed kodem musimy dodać EndContractBlock().

ObjectInvariants – coś co zawsze jest prawdziwe

Ze względu na to iż nie ma atrybutów, a pisanie inwariantów notorycznie w każdej metodzie w której korzystamy z kontraktów mijałoby się z celem, został wprowadzony mechanizm określania wartości które zawsze muszą być prawdziwe:

[ContractInvariantMethod]
protected void ObjectInvariant()
{
	Contract.Invariant(_a != 10 || _a != 20);
}

Na początku definiujemy metodę zwracającą void i określoną atrybutem ContractInvariantMethod. Atrybut ten informuje kompilator, iż ta metoda odpowiedzialna jest za przechowywanie wszystkich wartości, które zawsze powinny spełniać określony warunek. Podobnie jak w Ensures i Requires, Contract.Invariant może wyglądać następująco:

Contract.Invariant(condition);
Contract.Invariant(condition, "error message");

A jak pamiętacie przykład, na jakiej zasadzie działa „przepisywacz” kodu to zapewne zauważyliście wywołanie metody ObjectInvariant:

public int Test(int a)
{
	__ContractsRuntime.Requires(a > 10, null, "a > 10");
	__ContractsRuntime.Requires(a < 20, null, "a < 20");
	a -= 10;
	int CS$1$0000 = a;
	int Contract.Result<int>() = CS$1$0000;
	__ContractsRuntime.Ensures(Contract.Result<int>() < 10, null, "Contract.Result<int>() < 10");
	this.ObjectInvariant();
	return Contract.Result<int>();
}

I tak oto CC załatwia nam sprawę walidacji inwariantów :)

Interface i Abstract

To już ostatni punkt zabawy z CC. Ze względu na to iż C# i inne języki nie zezwalają na przykładowe implementacje interfejsów jak i klas abstrakcyjnych, MR wprowadziło dwa atrybuty:

  • [ContractClass(typeof(ClassName))] – atrybut określający klasę, która zawiera implementację danego interfejsu lub klasy abstrakcyjnej;
  • [ContractClassFor(typeof(InterfaceOrAbstractClassName))] – atrybut określający nazwę interfejsu/klasy abstrakcyjnej, której implementacja kontrkatu znajduje się w klasie oznaczonej tym atrybutem.

Przykład (zaczerpnięty z dokumentacji):

[ContractClass(typeof(IFooContract))]
interface IFoo
{
    int Count { get; }
 
    void Put(int value);
}
 
[ContractClassFor(typeof(IFoo))]
sealed class IFooContract : IFoo
{
    int IFoo.Count
    {
        get
        {
            Contract.Ensures(0 <= Contract.Result<int>());
            return default(int);
        }
    }
    void IFoo.Put(int value)
    {
        Contract.Requires(0 <= value);
    }
}

Od tej pory, każda klasa implementująca dany interfejs będzie podlegała weryfikacji kontraktu :)

Integracja z VS

640x450.aspx

Stwierdziłem, że warto opisać tą zakładkę gdyż głowienie się co oznaczają poszczególne parametry nie ma sensu :)

  • Perform Runtime Contract Checking – powoduje sprawdzenie poprawności wykonywanych kontraktów podczas uruchomienia aplikacji. Daje to nam możliwość testowania kodu, zamiast akceptacji złego parametru możemy zobaczyć jaki „klient” nie spełnia naszych oczekiwań i zobaczyć dlaczego tak się dzieje, może klient pomija analizę parametrów, które nam przekazuje? Ten combox obok checkbox zawiera trzy pięć wartości:
    • Full – testuje wszystkie typy kontraktów (pre i post oraz inwarianty);
    • Preconditions – testuje jedynie kontrakty pre-conditions czyli Require i RequireAlways;
    • RequiresAlways – testuje tylko i wyłącznie kontrakty RequireAlways;
    • Pre and Post – testuje jedynie kontrakty pre i post condition, ale nie testuje inwariantów;
    • ReleaseRequires – testuje jedynie kontrakty pre-condition Require<TException>;
    • None – przydatny do przeprowadzania testów wydajnościowych, nie testuje niczego.
  • Public Surface Contract Checks – opcja przydatna, kiedy chcemy analizować jedynie wywołania kontraktów w assembly, które są publiczne i mogą zostać wywołane z innychassembly;
  • Assert on Contract Failure – domyślnie opcja jest zaznaczona I chodzi jedynie o to czy ma zostać wygenerowany wyjątek przy np.: Requires czy ma się pojawić okno dialogowe jak przy opisie Ensure. Zaznaczenie – widok jak przy Ensure, odhaczenie wywołanie wyjątku;
  • Call-site Requires Checking – mały bajer zezwalający podczas buildowania projektu A na sprawdzenie kontraktów w bibliotece B, z której projekt A korzysta. Opcja się sprawdza, kiedy biblioteka B nie ma lub ma tylko częściowo włączoną opcję runtime contract checking a do tego zawiera bibliotekę kontraktów B.Contracts, dzięki czemu podczas sprawdzania wywołań metod z projekt A w bibliotece B są sprawdzane warunki pre-condition;
  • Perform Static Contract Checking – umożliwia weryfikację kontraktów w trakcie procesu kompilacji kodu. Daje to nam przyjemne informacje w oknie błędów (Error List), które nakierowują nas na problem jaki może istnieć. Sama analiza jest na tyle ciekawa, że bierze pod uwagę cały kod metody a nie tylko pierwsze linijki. Jeżeli analizator zauważy, że przekazujemy parametr a od którego następnie odejmujemy 10, to poleci nam stworzenie warunku, który powinien być spełniony (screenshot poniżej). Włączenie tej konfiguracji powoduje utworzenie dodatkowego pliku DLL zawierającego jedynie API z kontraktami (bez naszego kodu), umożliwia to przekazanie API na podstawie jakiego klient będzie budował swoje rozwiązanie. Dodatkowo możemy określić następujące wartości analizy statycznej:
    • Implicit Non-Null Obligations – analizuje miejsca, w których może wystąpić wartość null zarówno w pre jak i post;
    • Implicit Array Bounds Obligations – to samo co wyżej ale pod względem zakresu przekroczenia zakresu tablicy;
    • Check in Background – moim zdaniem funkcja jeszcze nie działa, chodzi o to by analiza była wykonywana w trakcie pisania kodu a nie po kompilacji :)
    • Implicit Arithmetic Obligations – nie wiem :(
    • Show squiggles – nie wiem :(
    • Baseline – jeżeli opcja jest włączona, należy podać nazwę pliku XML, który jak nie istnieje zostanie utworzony podczas pierwszej kompilacji. Jeżeli chcemy wprowadzić kontrakty do istniejącego już rozwiązania to ilość błędów zwróconych może być przytłaczająca. Baseline umożliwia nam utworzenie listy błędów, która będzie odfiltrowana w trakcie wyświetlania informacji na temat złych kontraktów. Po prostu kompilator będzie wykonywał analizę a następnie pozostawiał tylko te wartości, które nie znajdują sie w pliku baseline.
  • Build a Contract Reference Assembly – jeżeli nie chcemy wykonywać analizy statycznej, a chcemy mieć osobą dll z informacjami na temat kontraktów to należy ten checkbox zaznaczyć :)

cc04

Czy to wszystko?

Nie! :) jeszcze jest kilka metod, które pominąłem jednak dwie ciekawsze z nich jeszcze nie mają wsparcia analizatora :( więc zachęcam do przejrzenia punktu zasoby, gdzie są linki do dokumentów/filmów, które wam przybliża elementy które omówiłem, których nie omówiłem i te które nadchodzą:)

Ograniczenia

Pierwszym ograniczeniem jest ważny aspekt w CC – kontrakty są dziedziczone na zasadzie behavioral subtyping, co oznacza iż w metodzie przeciążonej nie możemy definiować pre-conditions, zaś do woli możemy definiować i specjalizować post-conditions. Dodatkowo nie wszystkie jeszcze wyrażenia (które pominąłem – ForAll, Exists) są sprawdzane przez dostarczone narzędzia. Także aktualne narzędzia analizujące nie radzą sobie z kodem wykorzystującym wyrażenie yield – istnieje obejście tego problemu, ale IMO nie warte nawet zwrócenia na razie uwagi, to powinno zostać naprawione :)

Ciągi znaków wykorzystywane w każdej z metod, gdzie mogą być wykorzystane, muszą być statycznie podane – na razie nie ma wsparcia dla dynamicznie bindowanych wartości, ale planowane jest ich wprowadzenie.

Aktualnie, run-time check, działa na zasadzie Debug.Assert(false); a następnieEnvironment.FailFast("message"); ma to być jednak zmodyfikowane by móc przemienić to na własne wyjątki.

Kontrakty nie działają na strukturach.

Zasoby

Tego jest trochę :)

Podsumowanie

Jest to nowa ciekawa funkcjonalność dodana do .NET Framework na którą warto zwrócić uwagę. Ja już teraz wykorzystuje CC gdyż znalazłem na nie zastosowanie, nie licząc tego iż analizator statyczny powoduje analizę mojego kodu i wychwycenie błędów które mogłem popełnić, to także wiem, że jak już będę z API korzystał to nie przekażę parametru który nie ma prawa być przekazany.

Oczywiście jak i z TDD, nie należy z kontraktami przesadzać. Jeżeli piszemy kod, który nie jest udostępniony klientowi, to nie ma sensu wykorzystywać kontraktów – przynajmniej z logicznego punktu widzenia. Jeżeli chcemy zaś by cały nasz kod był zgodny z kontraktami, to na początku poświęcimy trochę więcej czasu by potem go odzyskać :)

Jak się podoba pomysł kontraktów? Będziecie z nich korzystać? Zapraszam do dyskusji :)

10 KOMENTARZE

  1. Mechanizm z pewnością ciekawy i dobrze że zostanie włączony do .NET. Jedyne czego się obawiam to zachłyśnięcie ową nowością i tworzenie metod, których 3/4 kodu to walidacja parametrów i określanie zwracanych wartości. Ale, jeśli będzie się to stosowało z umiarem i zrozumieniem, z pewnością zwiększy komfort kodowania. Jak ze wszystkim – umiar, zrozumienie intencji i stosowanie zgodnie z przenaczeniem kluczem do efektywnego wykorzystania każdego mechanizmu :).

    Fajny wyczerpujący art, dzięki.

  2. Na Code Contracts czekam już od pewnego czasu. Aczkolwiek jeszcze nie instalowałem. I ciekawi mnie czy będą jakoweś ograniczenia np. w kontekście .NET Compact Framework.

    Nie zgadzam się natomiast, iż w kodzie, który nie jest udostępniany klientowi nie ma sensu tworzyć kontaktu. Odpowiadają one w miarę funkcjonalności Debug.Assert i jako takie powinny być włączone na poziomie Debug. Dzięki temu inni programiści dostają czytelniejszy kod.

    Mnie osobiście zawsze brakowało kontroli parametrów metod i Code Contracts po pierwsze powinno doprowadzić do większej przejrzystości kodu, a po drugie dzięki analizie statycznej część potencjalnych błędów być może zostanie usunięta już na etapie kompilacji.

    Tak czy siak czekam co z tego wyniknie. I jeszcze jedo pytanie. Na stronie widziałem jakiś czas temu wymaganie Visual Studio dla Team System. Dalej tak jest?

  3. @Procent

    Oczywiscie, z niczym nie nalezy przesadzac, ale zamiast pisac if cos == null then throw w public method to juz raczej bede pisal Contract.Requires – z kilku zwgledow, jednym z nich jest to ze potem dokumentacja sie przejmowac nie bede musial, a druga, ze w intellisense dostane odpowiednie info na temat “zlego” wykorzystania :)

    To tak… by jeszcze dodac, np.: Contract.Requires mozemy napisac w postaci cr i dwa razy tab :) czyli sa snippets! :)

    i wszyscy teraz klaszcza :)

    @Arek

    Nie sadze zeby byly jakiekolwiek ograniczenia w kontekscie CF. zauwaz ze w wyniku masz zwykla klase. to co najwyzej moze byc to to iz CC nie zostanie przeniesione na CF a co by bylo juz idiotyzmem :) i trzeba to odrazu do nich na forum zglosic :) zreszta zaraz sie zapytam :) Zreszta mozesz sprawdzic czy Microsoft.Contracts.dll zadziala Ci juz teraz z CF :)

    Masz racje, do naszych metod tez mozemy to stosowac, byleby nie przesadzac, nie zawsze musimy dodawac walidacje parametru wejsciowego i wyjsciowego. Poprostu uzaleznienie sie od CC tez nie jest dobrym pomyslem :) nalezy to dozowac :)

    Ta analiza statyczna – to Cie informuje o mozliwych bledach, nie zawsze jeszcze dziala super poprawnie o czym swiadcza wpisy na forum, nie poprawi bledow, moze wskazac, ale musisz znia wspolpracowac ;)

    A co do wersji… zobacz ta z devlab moze da sie zaisntalowac, jezeli nie to masz akademicka wersje na research.microsoft.com/contracts ktora na pewno pojdzie ale nie mozesz na niej pisac komercyjnych rozwiazan :(

    Gutek

  4. Ograniczenia mogą być ze względu np. na sposób generowania kodu przez kompilator. Np. PostSharp nie działa do końca na mobilnych bo korzysta z klas, których nie ma w ramach .NET CF – chodzi o np. przestrzenie nazw System.Reflection.Emit do generowania kodu.

    Jeśli zaś chodzi o statyczną analizę kodu – cudów nie ma ;- ) To trochę tak jak z dodatkiem ReSharper, który podpowiada na bieżąco np. możliwość zajścia wyjątku NullReferenceExceptio, a co my z tym zrobimy zależy od nas.

  5. Gutek: Bardzo dobry tekst. Lubie czytac Twoje arty, bo tematy traktujesz calosciowo i zwracasz uwage na drobne szczegoly, ktore moga sprawiac na poczatku problemy. Swietne referencje, widac, ze troche pracowales przy tym tekscie :).
    Dzieki temu tekstowi przypomnialem sobie summit i przypomniales mi, ze tez mialem cos napisac ;)
    Pomysl koniecznie o pokazaniu tego tematu na spotkaniu grupy wg.net! Mozna przy okazji powtorzyc sesje o Spec# z zeszlego roku – pogadam o tym z Pawlem…

  6. Świetny tekst. Ja tylko w kwestii formalnej:
    W tekście jest: “w przypadku kiedy nasza metoda kożysta z parametru wyjściowego” zwróciłbym uwagę na “niestosowne” użycie wyrazu koŻysta :)

  7. @Michal

    dzieki :) a blad juz poprawiony ;)

    @mgrzeg

    dzieki :)

    nie wiem czy jest sens przypominac sesje o Spec#, raczej o nowych jezykach ktore wchodza w uzycie jak F#, IronRuby czy IronPython (nawet Groovy ma juz swoja wstepna implementacje!;))

    @Arek

    zamiast dyskutowac ;) mozesz zainstalowac wersje akademicka i sprawdzic, tak bedzie najlepiej IMO :)

    Gutek

  8. Powiem tyle:
    -artykuł rzetelny i zgrabnie napisany
    – ale to co zrobił Microsoft jest kiepskawe bo:
    — sposób opisu kontraktów jest prymitywny (wywołania jakichś dziwnych metod) i niekonsekwentny (raz wołasz metodę, raz dajesz atrybut), ogólnie brzydki
    — realizacja jakaś taka siermiężna. Po co wymyślili AOP? Żeby potem nie stosować tam gdzie akurat się najbardziej nadaje?
    — jakiś szalony pomysł z pisaniem implementacji do interfejsu tylko po to, żeby w tej implementacji zrobić kontrakty dla metod z interfejsu
    — generalnie Research się nie wysilił. To zwykły generator trywialnego kodu. Myślałem że coś bardziej sprytnego zrobią.

  9. @nightwatch

    Dzieki za komentarz.

    Co do Twoich uwag odnosnie implementacji tego rozwiazania przez MS:
    1) System jest dosc konsekwetny choc nie jakis cudowny. Rozumiem czemu czasami trzeba dac atrybut – tylko po to zebys nie musial za kazdym razem wolac tej samej metody. Zas jezeli chodzi o prymitywny: innym rozwiazaniem jest pisanie IFow lub przezucenie sie na SpecSharp. Robienie tego za pomoca atrybutow nie wchodzi w gre ze wzgledu na analize kodu, ktorej wykonanie nie bylo by wprosty sposob do zaimplemntowa – jezeli w ogole by sie dalo.
    2) Wiem, duzo ludzi jest za AOP, ja za to znam duze korporacje piszace zarowno w Javie i .NET ktore o AOP trzymaja sie jak najdalej. Sa to firmy ktore wymagaja by kod pozostal w nienaruszonym stanie, wprowadzenie AOP dalo by mozliwosc na wprowadzenie pewnych niechcianych funkcjonalnosci. I takich firm jest duzo. Ja osobiscie z checia bym wykorzystal momentami AOP, ale jakos tez nie jestem jego zwolenikiem. Zreszta chyba coraz mniej sie o tym slyszy… albo ja poprostu przestalem czytac te same blogi i witryny.
    3) Z tym sie zgodze… to jest chore i oni tez uwazaja ze tak jest… szukaja lepszego pomyslu
    4) Trywialny nie trywialny od roku widze kod publikowany przez ms na codeplex i za kazdym razem pisza oni “wlasna” implementacje kontrkatow za pomoca klas ale bez wpsarcia analizatora statycznego. Jezeli chodzi o same klasy to rzeczywiscie moze nie odkryli ameryki, ale analiza statyczna to jest to. Daje Ci to odrazu informacje co gdzie moze byc nie tak, ulatawia generowanie unit testow za pomoca PEX, a skoro mozna pisac jednolinijowy singleton to tez mozna napisac jednolinijkowa weryfikacje – jest to prostrze i szybsze niz pisanie wlasnego framework do obslugi parametrow wejsciowych czy tez pisanie IFow a w kolejnych wersjac bedzie dawalo wiecej informacji podczas pisania kodu – odrazu bedziesz mial informacje jaki wyjatk bedize wyzucony oraz jakie wartosci parametrow sa akceptowalne. Skroci sie Twoj czas pisania dokumentacji – kolejny plus :)

    Gutek

Comments are closed.