Jak przyjmować i jak zwracać kulturalnie – zastanawialiście się kiedyś na tym? Taki programistyczny savoir-vivre. Jak to zrobić, żeby mi (programiście) było wygodnie, a jednocześnie uszcześliwić przyszłego użytkownika API które tworzymy? Przecież to może być właśnie ja (ja piszący tego bloga)!
Sprzedam wam dwie proste reguły (na bank są inne o których nie wiem), które warto zapamiętać lub przynajmniej sie nad nimi zastanowić.
Przyjmowany parametr powinien być możliwie wysoko w drzewie dziedziczenia, możliwie najogólnieszy, z którego możemy skorzystać. Jak zawsze będę się wspierać na przejaskrawionych przykładach, zupełnie oderwanych od rzeczywistości:
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #a31515; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
1: int NotSoNiceSumFunction(List<int> arg)
2: {
3: int result = 0;
4: foreach (var i in arg)
5: {
6: result += i;
7: }
8: return result;
9: }
Oraz drugi przyklad:
1: int NiceSumFunction(IEnumerable<int> arg)
2: {
3: int result = 0;
4: foreach (var i in arg)
5: {
6: result += i;
7: }
8: return result;
9: }
Zasadniczo nie różnią się one od siebie poza typem arugumentu. Za pierwszym razem określamy go bardzo dokładnie: pracujemy tylko z listą intów. Podczas gdy w seksownym ciele funkcji, nie ma nigdzie zachowania, które mogłoby uzasadnić potrzebę używania listy. Za drugim razem, wymagamy tylko aby argument implementował interfejs IEnumerable.
Jaki jest zysk z takiego pozytywnego nastawienia?
1: void Test(string[] args)
2: {
3: List<int> input1 = new List<int>();
4: IList<int> input2 = new List<int>();
5: ICollection<int> input3 = new List<int>();
6: IEnumerable<int> input4 = new List<int>();
7: int[] input5 = new int[100];
8:
9: NotSoNiceSumFunction(input1); // :)
10: NotSoNiceSumFunction(input2); // :(
11: NotSoNiceSumFunction(input3); // :(
12: NotSoNiceSumFunction(input4); // :(
13: NotSoNiceSumFunction(input5); // :(
14:
15: NiceSumFunction(input1); // :)
16: NiceSumFunction(input2); // :)
17: NiceSumFunction(input3); // :)
18: NiceSumFunction(input4); // :)
19: NiceSumFunction(input5); // :)
20: }
Powyżej przykładowy kod z różnymi kontenerami dla intów. Które linijki kodu nie będą chciały banglać? Mój kompilator nie poradził sobie z linijkami 10,11,12,13. Czyli tak jak powiedzieliśmy podczas definiowania funkcji NotSoNiceSumFunction – działamy tylko i wyłącznie z listą intów. Podczas gdy NiceSumFunction poradził sobie z każdym rodzajem parametru. Chyba nie muszę pisać z kim będzie się lepiej współpracowało?
Teraz w drugą stronę, jest juz po wszystkim, wiatraczek wyje, procek gorący, mamy wynik. Jak go przekazać stronie zainteresowanej? Zwrócić jako ogólny typ, czy dokładnie określić co nam wyszło? Myśl, myśl, myśl.
Znowu kod dla przykładu:
1: List<int> GetAllItemsAsList()
2: {
3: List<int> items = new List<int>();
4: // magic goes here
5: return items;
6: }
Wersja mniej szczegółowa:
1: static IEnumerable<int> GetAllItemsAsEnum()
2: {
3: IEnumerable<int> items = new List<int>();
4: // magic goes here
5: return items;
6: }
Na którą wersję się decydujecie? Ja przyznam się szczerze, że do tej pory wynik zwracałem zawsze przez IEnumerable. Ale już tak nie robię.
Jak może wyglądać wykorzystanie metod ze strony klienta na przykładach:
1: void Test(string[] args)
2: {
3: List<int> result1 = null;
4: IList<int> result2 = null;
5: ICollection<int> result3 = null;
6: IEnumerable<int> result4 = null;
7: int[] result5 = null;
8:
9:
10: result1 = GetAllItemsAsList(); // :)
11: result2 = GetAllItemsAsList(); // :)
12: result3 = GetAllItemsAsList(); // :)
13: result4 = GetAllItemsAsList(); // :)
14: result5 = GetAllItemsAsList(); // :(
15:
16: result1 = GetAllItemsAsEnum(); // :(
17: result2 = GetAllItemsAsEnum(); // :(
18: result3 = GetAllItemsAsEnum(); // :(
19: result4 = GetAllItemsAsEnum(); // :)
20: result5 = GetAllItemsAsEnum(); // :(
21: }
W tym przypadku widać, że klient będzie bardziej szczęśliwy jeśli dokładnie określimy wynik naszej pracy, a decyzję o tym co z nim zrobić zostawimy końcowemu odbiorcy. W przypadku zwracania jako enum, klient będzie musiał przyjąć IEnumerable, a następnie samodzielnie przekształcić to, w razie potrzeby, w klasą bardziej wyspecjalizowaną.
Czyli co, bądźmy mili i proaktywni, przyjmujmy wszystko z czym możemy sobie poradzić. A zwracając wynik, zadbajmy o szczegółową informację o tym, czym jest wynik i jak z niego korzystać, klient sam podejmie najlepszą decyzję czy wykorzysta pełny potencjał naszej ciężkiej pracy, czy potrakutje nas po między mordziu.
Pozdrawiam z IE8 i czekam na riposty.
Fajne spostrzeżenie odnośnie typów zwracanych.
A ogólnie nie ma co chyba też się ograniczać do intów, mogę chcieć posumować sobie choćby floaty tym samym algorytmem.
Pozdr.
Jan K.
Nie ma się co ograniczać do kolekcji. Dzięki za odwiedziny.
Dobry post 🙂