Ekstremalna przeróbka ASP.NET - Część 6: Podział obowiązków

Udostępnij na: Facebook

Kyle Baley (kyle@baley.org | http://kyle.baley.org)
Donald Belcham (donald.belcham@gmail.com | http://igloocoder.com)
James Kovacs (jkovacs@post.harvard.edu | http://www.jameskovacs.com)

Kod dostępny do pobrania z witryny MSDN Code Gallery

Witam z powrotem w szóstym odcinku serii Ekstremalna przeróbka ASP.NET. W poprzednich artykułach skupialiśmy się na technologiach związanych z przeglądarką po stronie klienta: XHTML, CSS, JavaScript, jQuery i jQuery UI. Kilka następnych artykułów w tej serii będzie się skupiać na kodzie po stronie serwera. Jak możemy zmodyfikować nasz kod C#, aby poprawić możliwości utrzymywania i rozszerzania naszej bazy kodu oraz jej elastyczność. Ten artykuł, część 6, skupi się w szczególności na podziale obowiązków.

Powinny być rozdzielone

Podział obowiązków jest pojęciem, które zastosowane do tworzenia oprogramowania zajmuje się utrzymywaniem dystansu pomiędzy rozłącznymi aspektami naszego kodu. Może się to wydawać skomplikowane, ale wszyscy mieliśmy z tym do czynienia w przeszłości, nawet jeśli o tym nie wiedzieliśmy. Każdy prawdopodobnie słyszał, że nie powinniśmy mieć kodu dostępu do danych na naszych stronach WWW. To jest właśnie podział obowiązków. Strona WWW nie powinna bezpośrednio wiedzieć, jak radzić sobie z problemami bazodanowymi takimi, jak łańcuchy połączeń, obiekty poleceń, itd.

Dlaczego to takie ważne? Podział obowiązków jest jednym z pojęć, które pozwalają nam pracować nad określonym aspektem aplikacji, bez znacznego wpływu na inne części tej aplikacji. Pomyślmy o tym w ten sposób: Czy nie powinienem być w stanie pracować nad silnikiem swojego samochodu bez konieczności zajmowania się jego kołami? W naszym oprogramowaniu nie powinniśmy zajmować się infrastrukturą bazodanową, gdy pracujemy w plikach aspx.cs (lub aspx.vb).

Jeśli nie będziemy zwracać uwagi na podział obowiązków, to zaczniemy dostrzegać pewne wspólne problemy podczas pracy nad swoimi bazami kodu. Wszyscy byliśmy w sytuacji, gdy próbując zmieniać coś, co wygląda na drobiazg, musieliśmy w końcu zagłębić się w wewnętrzną infrastrukturę naszej aplikacji. Biorąc pierwszy z brzegu przykład powiedzmy, że chcielibyśmy zmienić sposób określania, co użytkownik ma prawo robić na stronie. W aplikacji ScrewTurn uprawnienia są określane przez wywołanie klasy singleton AuthChecker. (Naprawianie problemów związanych z wzorcem singleton omówimy w przyszłym artykule. Na razie skupimy się na podziale obowiązków.) Aby określić, czy użytkownik ma na przykład uprawnienia do pobierania załącznika, wykonujemy następujące wywołanie:

AuthChecker.Instance.CheckActionForPage(currentPage, 
Actions.ForPages.DownloadAttachments,                                                                    
currentUsername, currentGroups)

Jeśli przyjrzymy się temu wierszowi kodu w oderwaniu od kontekstu, to na pierwszy rzut oka nic nie wydaje się stanowić problemu. Wyobraźmy sobie jednak, że to wywołanie istnieje na wielu, lub wszystkich stronach WWW w aplikacji. Zobaczymy wtedy, że zmiana sposobu określania możliwości pobierania załączników przez użytkownika będzie wymagać od nas zmodyfikowania każdej z tych stron WWW. To znaczy w efekcie jedna zmiana wymusi konieczność modyfikowania niezwiązanego z nią kodu.

Mamy tu do czynienia z jeszcze jednym problemem. Rozważmy ilość szczegółów związanych z autoryzacją, które istnieją w tej aplikacji. Dobrym przykładem jest metoda Page_Load w pliku Default.aspx.cs.  W metodzie Page_Load istnieją wywołania sprawdzające informacje o zabezpieczeniach dotyczące danego użytkownika, w tym:

  • Czy użytkownik może przeglądać stronę?
  • Czy użytkownik może pobierać załączniki ze strony?
  • Czy użytkownik może ustawiać uprawnienia?
  • Czy użytkownik może administrować stroną?
  • Czy użytkownik może przeglądać dyskusje na stronie?
  • Czy użytkownik może brać udział w dyskusjach na stronie?
  • Czy użytkownik może zarządzać dyskusjami na stronie?
  • Czy użytkownik może edytować stronę?

To bardzo dużo informacji autoryzacyjnych, o których musi wiedzieć metoda Page_Load. Oczywiście metoda Page_Load musi pobierać i wykorzystywać te informacje. Ale nasze pytanie brzmi: Czy musi wiedzieć jakpobierać wszystkie te informacje? Co równie ważne, czy musi to być wykonywane dla każdego zadania, które może być realizowane, z osobna? Pytając w inny sposób, czy Page_Load musi dokładnie rozumieć, w jaki sposób obsługiwane są zabezpieczenia? Nie, nie musi. Czy może po prostu poprosić jakąś inną klasę o dostarczenie tych wszystkich informacji o zabezpieczeniach? Tak, może! Metoda Page_Load jedynie musi uzyskać zakres uprawnień, a nie powinna znać, ani interesować się mechanizmami, dzięki którym się to dokonuje.

Ostrzeżenie: Przed nami utrzymywanie kodu

Ostatecznym celem przy stosowaniu rozdzielania obowiązków jest łatwość utrzymywania aplikacji. Ponieważ jest to istniejąca aplikacja, ScrewTurn Wiki prawdopodobnie ma obszary, nad którymi jest ciężko pracować, ponieważ klasy wykonują zbyt wiele i są wrażliwe na zmiany. Najmniejsza zmiana w jednej klasie może mieć wpływ na inne miejsca w kodzie. Dzieląc obowiązki możemy rozbić kod na pojedyncze elementy tak, aby przeglądając jedną klasę nie ulegać rozproszeniu przez dodatkowy kod, który nie należy do jej obowiązków.

Jest to dobra odskocznia do jednego ze sposobów, dzięki którym możemy zacząć rozdzielać obowiązki w naszych klasach. Jak wiele rzeczy w naszym rzemiośle, niełatwo jest zauważyć, kiedy rozdzielenie obowiązków zostało naruszone. Jest tylko jedna rzecz, o którą możemy zapytać, gdy patrzymy na dany fragment kodu:

                Czy ten kod powinien być w tej klasie?

Albo mówiąc inaczej:

                Czy jest to coś, za co ta klasa bezpośrednio odpowiada?

Kwestia odpowiedzialności jest ważna. Gdy zaczynamy myśleć o rozdzielaniu obowiązków i o odpowiedzialności klas, to kierujemy się ku zasadzie pojedynczej odpowiedzialności:

                Klasa powinna mieć tylko jeden powód do zmiany. – Robert C. Martin w pracy Agile Software Development: Principles, Patterns, and Practices

Wcześniej, gdy omawialiśmy zawartość metody Page_Load w pliku Default.aspx.cs, zaproponowaliśmy zadanie pytania „Czy musi ona wiedzieć, jak pobierać wszystkie te poszczególne informacje?” W naszym przypadku odpowiedź powinna brzmieć nie. Metoda Page_Load powinna tylko wiedzieć, że musi pobrać informacje autoryzacyjne, a nie musi znać dokładnych szczegółów, jak te informacje są pobierane.  Rozdzielenie tych dwóch potrzeb leży u podstaw podziału obowiązków.

Jest jeszcze jeden dobry powód, aby spróbować rozdzielić obowiązki w swojej aplikacji. W poprzednim artykule wspominaliśmy o testowaniu interfejsu użytkownika przy pomocy WatiN. Jeśli ktoś próbował skonfigurować testowanie interfejsu użytkownika, to szybko odkrył, że tworzenie tej siatki zabezpieczającej może stać się kłopotliwe. Testy są powolne i często trzeba je przerabiać w miarę dokonywania zmian w interfejsie użytkownika. Przenosząc kod poza strony ASPX do oddzielnych klas otwieramy arenę testową i jesteśmy w stanie pisać prawdziwe testy jednostkowe, które testują tylko pojedynczą klasę w izolacji, bez konieczności uruchamiania wystąpienia przeglądarki, serwera WWW i bazy danych. Te testy są szybsze, mniej wrażliwe i mogą nas doprowadzić do miejsca, gdzie kod na stronach ASPX jest ograniczony do interakcji pomiędzy różnymi gadżetami interfejsu użytkownika.

Od zasad do praktyki

Dość gadania. Zastosujmy to w bazie kodu aplikacji ScrewTurn Wiki. Po pierwsze zaczniemy od pliku z kodem dla strony Default.aspx. Aby ułatwić sobie sprawy, zaczniemy od góry i przyjrzymy się zawartości metody Page_Load. Jest to zapracowana metoda, co nie jest rzadkie w istniejących aplikacjach. W samej tej metodzie można dostrzec kilka zakresów odpowiedzialności:

  • Przekierowanie strony w razie potrzeby
  • Ustalenie praw użytkownika dla tej strony
  • Konfiguracja różnych gadżetów interfejsu użytkownika
  • Wyświetlanie zawartości żądanej strony wiki

 

Patrząc na ten kod, pierwszą rzeczą, która przyciągnęła naszą uwagę była powtarzalna natura kodu autoryzacyjnego. Jak widać w kodzie na Rysunku 1, jest dużo wywołań pobierających pojedyncze wartości uprawnień.

Rysunek 1: Kod metody Page_Load dla strony Default.aspx.

string currentUsername = SessionFacade.GetCurrentUsername();
string[] currentGroups = SessionFacade.GetCurrentGroupNames();
 
bool canView = AuthChecker.Instance.CheckActionForPage
(currentPage, Actions.ForPages.ReadPage, currentUsername, currentGroups);
bool canEdit = false;
bool canEditWithApproval = false;
Pages.CanEditPage
(currentPage, currentUsername, currentGroups, out canEdit, out canEditWithApproval);
if(canEditWithApproval && canEdit) canEditWithApproval = false;
bool canDownloadAttachments = AuthChecker.Instance.CheckActionForPage
(currentPage, Actions.ForPages.DownloadAttachments, currentUsername, currentGroups);
bool canSetPerms = AuthChecker.Instance.CheckActionForGlobals
(Actions.ForGlobals.ManagePermissions, currentUsername, currentGroups);
bool canAdmin = AuthChecker.Instance.CheckActionForPage
(currentPage, Actions.ForPages.ManagePage, currentUsername, currentGroups);
bool canViewDiscussion = AuthChecker.Instance.CheckActionForPage
(currentPage, Actions.ForPages.ReadDiscussion, currentUsername, currentGroups);
bool canPostDiscussion = AuthChecker.Instance.CheckActionForPage
(currentPage, Actions.ForPages.PostDiscussion, currentUsername, currentGroups);
bool canManageDiscussion = AuthChecker.Instance.CheckActionForPage
(currentPage, Actions.ForPages.ManageDiscussion, currentUsername, currentGroups);

Z pewnością nie możemy teraz popatrzeć na ten kod i powiedzieć po prostu „Pozbądźmy się tego wszystkiego”, ponieważ jest nam on potrzebny do prawidłowego funkcjonowania strony. Ale to, w jaki sposób pobierane są właściwości, czy to z klasy AuthChecker, Pages, czy nawet przez bezpośrednie kodowanie, jest to odpowiedzialność jakiejś innej klasy, a nie tej. Page_Load potrzebuje tylko uzyskać ostateczne wartości.

Ponieważ postanowiliśmy, że Page_Load nie musi już mieć dokładnej wiedzy na temat tych szczegółów, możemy je wyodrębnić. Naszym pierwszym krokiem jest utworzenie klasy, która jest odpowiedzialna wyłącznie za zapewnianie informacji autoryzacyjnych dla pliku Default.aspx.cs. Nazwiemy tę klasę AuthorizationServices. Zamiast zagłębiać się od razu w zawartość klasy AuthorizationServices, przyjrzyjmy się, jak chcemy z niej korzystać w metodzie Page_Load w pliku Default.aspx.cs. To co chcemy zrobić, to wyeliminowanie wszystkich pojedynczych wywołań do klasy AuthChecker i zamiana ich na pojedyncze wywołanie, które zwraca wszystkie te same informacje autoryzacyjne, ale w skonsolidowanej postaci. To znaczy, szukamy czegoś takiego, jak:

AuthorizationServices authorizationServices = new AuthorizationServices();
CurrentCapabilitiesStatus currentUser = authorizationServices.RetrieveCurrentStatusFor(CurrentPage);

To proste wywołanie AuthorizationServices wystarcza nam do określenia, jakie poziomy uprawnień są dostępne dla bieżącego użytkownika. Wyniki tego wywołania metody są zwracane w obiekcie CurrentCapabilitiesStatus. Obiekt ten jest dość prosty i ma jedynie właściwości zawierające różne wartości autoryzacyjne.

Teraz, gdy wiemy, jak chcemy, aby wyglądał kod, możemy przejść do tworzenia tych klas i ich wypełniania. Ponieważ naszym zadaniem jest usunięcie szczegółowych informacji związanych z zadaniami autoryzacyjnymi z metody Page_Load, zacznijmy od tego. W tym przypadku najprostszym działaniem jest wycięcie i wklejenie istniejących wywołań AuthChecker z kodu metody Page_Load do metody RetrieveCurrentStatusFor. Aby kod AuthChecker się kompilował i działał, będziemy musieli przenieść też nieco innego kodu. Będzie to między innymi:

string currentUsername = SessionFacade.GetCurrentUsername();
string[] currentGroups = SessionFacade.GetCurrentGroupNames();
 
var canEdit = false;
var canEditWithApproval = false;
Pages.CanEditPage(currentPage, currentUsername, currentGroups, out canEdit, out canEditWithApproval);

Teraz, gdy mamy cały ten kod w metodzie RetrieveCurrentStatusFor, ostatnim krokiem jest utworzenie i wypełnienie danymi wynikowego obiektu typu CurrentCapabilitiesStatus. Jak wspominaliśmy wcześniej, jest to prosta klasa, która zawiera w sobie tylko właściwości. Można było zauważyć we wcześniejszym fragmencie kodu, że przypisywaliśmy wystąpienie tego obiektu do zmiennej o nazwie „currentUser”. Wtedy mogło się to wydawać dziwne, ale kiedy połączymy to z nazwami właściwości, to zobaczymy, że wynikowy kod w Page_Load będzie lepiej przedstawiał swoje zamiary. Ponieważ nie wiemy, jakie jest zamierzone użycie właściwości, gdy jesteśmy w klasie CurrentCapabilitiesStatus, wróćmy do metody Page_Load i przyjrzyjmy się, jak zamierzamy z nich w niej skorzystać.

Istnieje możliwość, jeśli przenieśliśmy kod z Page_Load do AuthorizationServices.RetrieveCurrentStatusFor, że w metodzie Page_Load będzie teraz kod, którego nie da się skompilować. Powinno się to ograniczać do zmiennych, które były w tej metodzie, ale teraz są przenoszone do klasy CurrentCapabilitiesStatus jako właściwości. To są te punkty użycia, których szukamy. Pierwszym, który zobaczymy jest instrukcja „if”, która sprawdza, czy bieżący użytkownik nie może przeglądać obecnej strony. Zmieńmy to wykorzystując teraz zmienną „currentUser”.

if(!currentUser.CanView) {
    // ciało metody pozostaje bez zmian
}

Tutaj można zobaczyć, w jaki sposób użycie nazwy currentUser zaczyna odgrywać rolę. Podczas przeglądania tego kodu powinno być jasne, co dokładnie robimy w tej metodzie. Postanowiliśmy nazwać właściwość „CanView”, ponieważ przede wszystkim mamy do czynienia z pozytywnymi pytaniami podczas korzystania z właściwości CurrentCapabilitiesStatus. Gdybyśmy mieli prześledzić pozostałe wykorzystywane informacje autoryzacyjne, utworzylibyśmy właściwości CanDownloadAttachments, CanSetPerms, CanAdmin, CanViewDiscussion, CanEditDiscussion, CanManageDiscussion, CanEdit i CanEditWithApproval, wszystkie zdefiniowane w obiekcie CurrentCapabilitiesStatus.

Gdy już dodamy te właściwości do obiektu CurrentCapabilitiesStatus i zastosujemy je w metodzie Page_Load w odpowiednich miejscach, to pozostanie nam jeszcze jeden problem do rozwiązania. Podczas przenoszenia kodu autoryzacyjnego z Page_Load do metody RetrieveCurrentStatusFor w AuthorizationServices, nie utworzyliśmy i nie zapełniliśmy obiektu CurrentCapabilitiesStatus. Dokonanie tego przypisania wartości do właściwości jest ostatnim krokiem w tej przeróbce.

Podsumujmy, co zrobiliśmy. Usunęliśmy kod z metody Page_Load korzystając z opcji wytnij i wklej oraz utworzyliśmy dwie nowe klasy. Nie wydaje się to wiele, jeśli chodzi o refaktoring. Ale to dlatego, że jeszcze nie skończyliśmy tego zadania. Aby uznać pracę za zakończoną, musimy teraz przejść do każdej strony, która wykorzystywała AuthChecker i dokonać tych samych zmian, co tutaj. To będzie sporo pracy, ale użyta przez nas metoda do oddzielania kodu specyficznego dla autoryzacji może być stosowana wobec innych plików z kodem. Spójrzmy jeszcze raz na refaktoring, ale teraz popatrzmy, jak zautomatyzowane narzędzia do refaktoringu mogą nam pomóc w dokonywaniu zmian.

Całościowy obraz

Gdy już wszystkie użycia AuthChecker zostaną usunięte, a w ich miejsce użyta zostanie klasa AuthorizationServices, to praca z bazą kodu będzie znacznie prostsza. Rozważmy teraz, co się wiąże ze zmianą sposobu pobierania uprawnień. Zamiast konieczności znajdowania i zmieniania wielu miejsc, w których pierwotnie używane było wywołanie AuthChecker (według moich szacunków 130), możemy pracować tylko w klasie AuthorizationServices. Ten jeden punkt do zmian pokazuje, że rozdzielamy obowiązki w swojej bazie kodu.

Kilka rzeczy wydarzyło się od czasu, gdy po raz pierwszy przyjrzeliśmy się metodzie Page_Load z pliku Default.aspx.cs. Po pierwsze byliśmy w stanie jasno zdefiniować obszar w kodzie związany z autoryzacją. Gdy pojawi się cokolwiek związanego z określaniem autoryzacji użytkownika, to wiemy, że zajmie się tym klasa AuthorizationServices. Nie będziemy już stale cierpieć z powodu wpływu modyfikacji lub naprawy funkcjonalności związanej z tym problemem na inne miejsca w kodzie.

Po drugie ograniczyliśmy liczbę wierszy w metodzie Page_Load, nie ograniczając przy tym możliwości odszyfrowywania przez programistów naszych zamiarów w kodzie dzięki zastosowaniu zrozumiałych nazw zmiennych i właściwości. Jest to dość ważne przy tego rodzaju refaktoringu. Nigdy nie chcemy pozostawiać kodu mniej zrozumiałego niż go zastaliśmy (a biorąc pod uwagę, jak szybko komentarze stają się przestarzałe, pozostawianie ich po sobie nie jest optymalnym podejściem pomocnym w zrozumieniu kodu). Zamiast tego myślmy o tym, jak będziemy korzystać ze zmiennych, właściwości i metod w kodzie i nazywajmy je tak, aby łatwo można było odczytać ich zamiary.

Podsumowanie

Ostatnim osiągnięciem jest to, że zaczęliśmy trend ograniczania powtórzeń w naszej bazie kodu. Jest to efekt uboczny rozdzielania obowiązków, ale implementując izolowaną klasę, której jedyną odpowiedzialność stanowi autoryzowanie użytkownika dla danej strony, pozwalamy sobie wyeliminować powtarzalny kod, który też się tym zajmuje. Proste wyszukiwanie wystąpień „AuthChecker” w całym rozwiązaniu pokazuje, że wiele lokacji może skorzystać z użycia nowej klasy AuthorizationServices.

Zajęcie się problemem autoryzacji jest tylko jednym z wielu zadań związanych z refaktoringiem, które są dostępne w metodzie Page_Load w pliku Default.aspx.cs. Do innych należą przekierowywanie adresów URL, budowanie adresów URL, Content.GetPageContent i Settings. Jeśli przejrzymy kod dołączony do tego artykułu, to możemy zobaczyć, jak wydzieliliśmy te problemy z metody Page_Load tak, żeby miała ona tylko jeden powód do zmiany: to w jaki sposób wyświetlamy zawartość strony.

James Kovacs jest niezależnym architektem, programistą, szkoleniowcem i złotą rączką, mieszkającym w Calgary w stanie Alberta, specjalizującym się w programowaniu zwinnym przy użyciu .NET Framework. Ma tytuł Microsoft MVP for Solutions Architecture i uzyskał stopień magistra na Harvard University. Można się z nim skontaktować pod adresem jkovacs@post.harvard.edu lub www.jameskovacs.com.

Donald Belcham jest starszym programistą, niezależnym kontrahentem i ekspertem od programowania zwinnego, który jest zdecydowanym poplecznikiem fundamentalnych wzorców i praktyk zorientowanych obiektowo. Jest współautorem książki „Brownfield Application Development in .NET” (Manning Press, 2008), a można się z nim skontaktować pod adresemdonald.belcham@igloocoder.com lub www.igloocoder.com.

Kyle Baley jest starszym programistą mającym ponad 10 lat doświadczenia w różnych firmach. Jest współautorem książki „Brownfield Application Development in .NET”, co kieruje jego zainteresowania na silne podstawy zorientowane obiektowo oraz bycie praktycznym i elastycznym we wszelkich projektach. Można się z nim skontaktować pod adresem http://kyle.baley.org lub kyle@baley.org.