Code Review: Złe użycie kontraktów w interfejsach

O kontraktach już kiedyś pisałem tutaj. Dziś z kolei zaprezentuję bardzo złą praktyką, czasami niestety jeszcze spotykaną tzn.:

interface IDataFilter
{
   IEnumerable<int> Filter(IEnumerable<int> data);
}
class CustomFilter:IDataFilter
{
   public IEnumerable<int> Filter(IEnumerable<int> data)
   {
       Contract.Requires(data!=null);
       // jakas logika

       return new int[0];
   }
}

Może wydawać się, że metoda jest dobrze zaimplementowana i korzysta z programowania defensywnego. Niestety (albo właściwie na szczęście), po skompilowaniu dostaniemy następujący warning:

Warning    2    CodeContracts: Method 'ConsoleApplication4.CustomFilter.Filter(System.Collections.Generic.IEnumerable`1<System.Int32>)' implements interface method 'ConsoleApplication4.IDataFilter.Filter(System.Collections.Generic.IEnumerable`1<System.Int32>)', thus cannot add Requires.

Dlaczego? Powyższy kod łamie zasadę Liskov. W skrócie, nie powinniśmy zmieniać zasady działania funkcji bazowej. Użytkownik korzystając z z interfejsu lub klasy bazowej nie powinien wnikać w szczegóły implementacyjne. Innymi słowy, jeśli programista wywoła następujący kod:

IDataFilter filter=...

filter.Filter(null);

Powyższa konstrukcja powinna być ZAWSZE dozwolona albo niedozwolona. Jeśli jedna implementacja akceptuje NULL a druga nie, wtedy łamiemy zasadę Liskov. Interfejs zawsze powinien definiować kontrakt (bo właściwie nim jest) i z tego względu prawidłowa implementacja może wyglądać następująco:

[ContractClass(typeof(DataFilterContract))]
interface IDataFilter
{
   IEnumerable<int> Filter(IEnumerable<int> data);
}
[ContractClassFor(typeof(IDataFilter))]
class DataFilterContract:IDataFilter
{
   public IEnumerable<int> Filter(IEnumerable<int> data)
   {
       Contract.Requires(data != null);

       return default(IEnumerable<int>);
   }
}

class CustomFilter:IDataFilter
{
   public IEnumerable<int> Filter(IEnumerable<int> data)
   {
       // jakas logika

       return new int[0];
   }
}

Przede wszystkim usunęliśmy kontrakt z CustomFilter i przenieśliśmy go do osobnej klasy. Następnie za pomocą atrybutów doczepiliśmy go do IDataFilter. Przy takiej implementacji to interfejs jest odpowiedzialny za definiowanie dopuszczalnych parametrów a wszyscy co implementują go, muszą odpowiednio dane obsłużyć. Implementacja interfejsu to nie tylko pisanie kodu metod ale również przestrzeganie warunków wejściowych i wyjściowych – to również jest częścią interfejsu (kontraktu).