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).