20 października 2010
Odsłon: 2490
Design by Contract (TM) to technika programowania defensywnego, postulująca jawne specyfikowanie interfejsów komponentów,
np. poprzez deklarowanie warunków, które muszą spełniać argumenty metod (to w zasadzie najczęstszy przypadek). Zwykle w projektach realizujemy
to wymaganie albo poprzez zwykłe if-y rzucające ArgumentException, albo tworząc statyczne klasy pomocnicze, takie jak Check czy
Require. Od pewnego czasu dostępne jest narzędzie Code Contracts opracowane przez Microsoft Research i to z niego
zdecydowałem się skorzystać w projekcie EuroManager.
Code Contracts składa się z kilku powiązanych ze sobą elementów, m.in.:
- Biblioteki klas, umożliwiających definiowanie kontraktów - obecnie jest ona integralną częścią .NET Framework 4, zatem
możemy pisać kod wykorzystujący Code Contracts bez obecności pozostałych elementów, bez pobierania czy kupowania czegokolwiek.
- Narzędzia do weryfikacji w czasie wykonywania, które wstrzykuje kontrolę kontraktów do assemblies przy kompilacji w trybie Debug
i pozwala wyłapać przypadki ich naruszenia. Skorzystanie z narzędzia wymaga pobrania Code Contracts Standard Edition
ze strony DevLabs.
- Narzędzia do weryfikacji statycznej, które potrafi wskazać niektóre naruszenia kontraktów nawet bez uruchamiania aplikacji
(efektowne, trzeba to przyznać), ale wymaga CC w wersji Premium (i VS też w wersji Premium).
Po instalacji Code Contracts Standard Edition na karcie projektu pojawi się dodatkowa pozycja, która umożliwia m.in. konfigurację
weryfikacji kontraktów (bez czego tak naprawdę deklarowanie kontraktów nie ma większego sensu).
Najprostszym, i zarazem najczęstszym sposobem wykorzystania Code Contracts w projekcie EuroManager jest wspomniana na początku
kontrola argumentów metod. Poniżej parę przykładów:
public ChaseBallState(IPlayer player)
{
Contract.Requires(player != null);
this.player = player;
}
public static float Time(float speed, float distance)
{
Contract.Requires(speed.IsRational() && speed > 0);
Contract.Requires(distance.IsRational() && distance >= 0);
return distance / speed;
}
Możliwości mamy tak naprawdę dużo więcej, możemy np. deklarować predykaty dotyczące wartości zwracanych, ale nie chcę tutaj
wchodzić w szczegóły - w sieci można znaleźć wiele dobrych tutoriali. Wystarczy rzucić okiem na klasę Contract:
Klasa System.Diagnostics.Contracts.Contract
Prosta rzecz, a dzięki niej niskim kosztem zyskujemy dużo lepiej wyspecyfikowane metody. Ale to nie wszystko - Code Contracts
oferuje też bardziej zaawansowane możliwości, jako przykład podam coś, co uważam za szczególnie przydatne: możliwość definiowania
kontraktów dla interfejsów. Zobaczmy to na przykładzie prościutkiego interfejsu ITimeAware:
[ContractClass(typeof(TimeAwareContract))]
public interface ITimeAware
{
void Update(float elapsedTime);
}
Interfejs udekorowany został atrybutem ContractClass, który wskazuje klasę stanowiącą jego kontrakt.
Ta klasa z kolei jest abstrakcyjną implementacją interfejsu, udekorowaną komplementarnym atrybutem ContractClassFor.
W implementacjach swoich metod zawiera wywołania metod klasy Contract.
[ContractClassFor(typeof(ITimeAware))]
public abstract class TimeAwareContract : ITimeAware
{
public void Update(float elapsedTime)
{
Contract.Requires(elapsedTime.IsRational() && elapsedTime > 0);
}
}
Muszę też wspomnieć tutaj o pewnej wadzie przyjętego podejścia. Klasa stanowiąca kontrakt nie może dziedziczyć z innej klasy,
co powoduje, że w przypadku interfejsów rozszerzających inne interfejsy musimy powielać wszystkie definicje metod. Niby nic
szczególnego, ale jest to nieco irytujące, choć też nie szukałem zbyt głęboko sposobów na obejście tego zachowania.
Pełną wersję kodu źródłowego można znaleźć na stronie projektu:
http://euromanager.codeplex.com.