Action i Func w C#
Pięć lat temu dopadł mnie zastój. Mój kod osiągnął już pewien poziom i ciężko było mi go dalej rozwijać. Oto, jak wykorzystałem aspekty programowania funkcyjnego, by kontynuować efektywne doskonalenie mojego kodu.
Mój kod był dość SOLID-ny, ale nadal istniało w nim wiele bardzo podobnych linijek, pomimo usilnej próby usunięcia duplikatów, gdzie tylko było to możliwe. Nie było to dokładne powielanie, ale powtarzające się wzorce w całym kodzie sprawiły, że utrzymywanie go sprawiało więcej problemów niż powinno.
Potem odkryłem, jak stosować Action
i Func
, aby jeszcze bardziej ulepszyć mój kod.
Spójrzmy na hipotetyczny przykład:
// Associates Degree
if (resume.HasAssociatesDegree)
{
try
{
resume.AssociatesDegreeScore = CalculateAssociatesDegreeScore();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Could not calculate associates degree score: {ex.Message}");
resume.AssociatesDegreeScore = 0;
}
}
else
{
resume.AssociatesDegreeScore = 0;
}
// Bachelors Degree
if (resume.HasBachelorsDegree)
{
try
{
resume.BachelorsDegreeScore = CalculateBachelorsDegreeScore();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Could not calculate bachelors degree score: {ex.Message}");
resume.BachelorsDegreeScore = 0;
}
}
else
{
resume.BachelorsDegreeScore = 0;
}
// Masters Degree
if (resume.HasMastersDegree)
{
try
{
resume.MastersDegreeScore = CalculateMastersDegreeScore();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Could not calculate masters degree score: {ex.Message}");
resume.MastersDegreeScore = 0;
}
}
else
{
resume.MastersDegreeScore = 0;
}
Chociaż jest to wymyślony przykład, ilustruje problem. W tym scenariuszu mamy powtarzalny wzorzec sprawdzania właściwości, a następnie wykonywania niestandardowej logiki, której nie należy wywoływać, jeśli właściwość była fałszywa, a następnie zapisywania wyniku w niestandardowej właściwości na obiekcie.
Jest to prosta procedura, ale duplikacja jest oczywista i wyciągnięcie metod jest utrudnione, ponieważ nie można wywołać CalculateX
, jeżeli CV nie ma odpowiedniego stopnia naukowego.
Dodatkowo załóżmy, że wystąpił błąd i konieczna była zmiana kodu. Musisz teraz dokonać tej samej zmiany w trzech miejscach. Jeśli przegapisz jedną, prawdopodobnie spowoduje to błędy. Dodatkowo podobieństwa kuszą Cię do tworzenia programów opartych na kopiowaniu i wklejaniu, co jest antywzorcem i potencjalnym źródłem błędów, jeśli nie wszystkie linie wymagające modyfikacji, zostały zmodyfikowane.
Nauczyłem się, że przekazywanie typów Action i Func pozwala uzyskać o wiele większą elastyczność metod, zapewniając im konfigurowalne zachowanie.
Action
to generyczna sygnatura metody, która nie zwraca wartości.
Na przykład sygnatura czegoś, co przyjmuje parametr boolean i liczbę całkowitą, wyglądałby tak: Action<bool, int> myAction;
Func jest bardzo podobny do Action
, z tą różnicą, że zwraca wartość. Typ zwróconej wartości jest określony przez ostatni argument. Na przykład Func<bool, int>
przyjmie wartość logiczną i zwróci liczbę całkowitą.
Tak więc możemy użyć Func
w naszym przykładzie, aby wyodrębnić metodę z pewnymi konfigurowalnymi zachowaniami:
private decimal EvaluateEducation(
bool hasAppropriateDegree,
string degreeName,
Func<decimal> calculateFunc)
{
if (hasAppropriateDegree)
{
try
{
// Invoke the function provided and return its result
return calculateFunc();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Could not calculate {degreeName} score: {ex.Message}");
}
}
return 0;
}
Poprawia to nasz kod w następujący sposób:
resume.AssociatesDegreeScore = EvaluateEducation(resume.HasAssociatesDegree,
"associates",
() => CalculateAssociatesDegreeScore());
resume.BachelorsDegreeScore = EvaluateEducation(resume.HasBachelorsDegree,
"bachelors",
() => CalculateBachelorsDegreeScore());
resume.MastersDegreeScore = EvaluateEducation(resume.HasMastersDegree,
"masters",
() => CalculateMastersDegreeScore());
Jest to o wiele prostsze, choć składnia jest nieco trudniejsza do odczytania. W trzecim parametrze tej metody deklarujemy wyrażenie lambda podobne do tych, których używamy do zapytań LINQ.
Jeśli musisz pracować z Func, który używa parametrów (powiedzmy, że masz sygnaturę Func<int, decimal>
, możesz zmienić logikę w ten sposób:
(myInt) => myInt + CalculateMastersDegreeScore();
Podsumowanie
Chociaż był to wymyślony przykład, mam nadzieję, że ilustruje on siłę przekazywania Func
i Action
do różnych metod. Jest to podstawa, na której zbudowane jest programowanie funkcyjne, ale w małych dawkach, może to być niezwykle pomocne również w programowaniu obiektowym.
Chociaż ta składnia sprawia, że kod jest nieco trudniejszy do odczytania, korzyści w utrzymywaniu są znaczące, a prawdopodobieństwo wprowadzania defektów związanych z duplikacją, jest znacznie niższe.
Oryginał tekstu w języku angielskim przeczytasz tutaj.