W obronie kohezji logicznej

W swej znakomitej książce „Code Complete” Steven McConnell rozważa między innymi pojęcie kohezji.  W dużym skrócie – kohezja jest miarą „spoistości” danego modułu, klasy lub funkcji. Im większa kohezja – tym mocniej dana funkcja (klasa, moduł) zorientowana jest na realizację jednego, konkretnego zadania. Perfekcyjnie kohezyjną funkcją jest funkcja sin(), spotykana w wielu językach i licząca sinus z danej wartości. Służy ona do liczenia sinusa i nie ma żadnych efektów ubocznych ani dodatkowego działania. Podobną kohezją cechują się funkcje zwane geterami lub seterami – ich zadaniem jest zwrócić, lub ustawić odpowiednią wartość zapewniając odpowiedni poziom bezpieczeństwa.

W naturze spotka się wiele rodzajów kohezji – od najsilniejszej, funkcjonalnej, gdzie działanie danej funkcji jest ściśle podporządkowane temu, co ma ona uczynić, aż po kohezję przypadkową, którego to terminu nie trzeba chyba wyjaśniać. Istnieje wiele kohezji dobrych (np. kohezja sekwencyjna, występująca wtedy, gdy ważna jest kolejność operacji, które w innej kolejności nie mają sensu), jak i złych. Jedną ze złych kohezji jest kohezja logiczna.

Podstawową ideą kohezji logicznej jest tworzenie funkcji, które wykonują różne zadania, w zależności od przesłanej do nich flagi. Wykonywane funkcje są średnio ze sobą związane, ot najważniejsza jest owa sterująca flaga. McConnell uważa taki rodzaj kohezji za generalnie nieakceptowany.

Pomimo, że generalnie zgadzam się z McConnellem, to jednak dochodzę do wniosku, że myli się on w tej sprawie. Z jednej strony mamy bowiem czystość kodu, z drugiej zaś jego użyteczność. Przypomina mi to sytuację, gdy miałem w funkcji obsługi zdarzenia kod, który w pewnym momencie prosił użytkownika o potwierdzenie. Jakiś czas później musiałem zautomatyzować tą formatkę zapewniając funkcję, która wykona to samo co wspomniana obsługa zdarzenia, klikając „Tak” na zadawane w niej pytanie. Rozwiązaniem problemu był refaktoring kodu obsługującego zdarzenie do nowej funkcji, która otrzymała dodatkowy parametr mówiący czy pokazywać potwierdzenie, czy domyślnie zakładać kliknięcie „Tak”. Funkcja nazywała się InternalSelectItem i jej wywołanie występowało zarówno w obsłudze zdarzenia OnSelectItem (parametr mówiący o potwierdzeniu miał wartość TRUE), oraz w obsłudze automatycznej, gdzie z kolei parametr miał wartość FALSE (dłuższą dyskusję można znaleźć na stackoverlow.com).

Z drugiej strony niedawno w swej pracy stanąłem przed zadaniem dodania kilku funkcjonalności na istniejącej formatce. Formatka prezentowała różne rodzaje bytów („itemów”) które mogły być klasyfikowane na różne sposoby. Istniała cała masa możliwych zmian klasyfikacji dostępnych z menu kontekstowego. Użytkownik zaznaczał sobie dane, które chciał zmienić, wybierał z menu kontekstowego co chce zrobić, a system realizował jego życzenie.

Oczywiście zanim dochodziło do wykonania  operacji, użytkownik musiał potwierdzić chęć wykonania, później należało sprawdzić, czy zaznaczono prawidłowe itemy, potem wykonywano operację, a na końcu odświeżano listę. Całość realizowana jest przez poniższy kod (tu w pseudopascalu):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
procedure TMyForm.PerformOperationOnSelected (APerformingQuery: TSomeQuery; AQuestionText: string; ALimitOperationToAssignedOnly, ALimitOperationToIgnoredOnly:boolean);
var
  li:integer;
  lItemId: integer;
  lMustRefresh:boolean;
begin
  if Grid.SelectedRecordCount = 0 then Exit ;
  if MessageDlg(AQuestionText, mtConfirmation, [mbYes, mbNo], 0) = mrNo then Exit;
  lMustRefresh:=false;
  { Iterating selected records }
  for li := 0 to Grid.SelectedRecordCount - 1 do
  begin
    { If not a valid item, use continue to jump to next iteration}
    if NOT Grid.SelectedRecords[ li ].IsValidItem then Continue ;
    if (ALimitOperationToAssignedOnly) then
    begin
      {Use continue to jump to next iteration if unassigned or igonred}
      if (NULL = Grid.SelectedRecords[ li ].Values[ cAssignmentId ]) then Continue;
      if (cIgnored = Grid.SelectedRecords[ li ].Values[cAssignmentId]) then Continue;
    end;
    if (ALimitOperationToIgnoredOnly) then
    begin
      {Use continue to jump to next iteration if assigned or unassigned – ie limit to ignored}
      if (NULL = Grid.SelectedRecords[ li ].Values[ cAssignmentId ]) then Continue;
      if (0 < Grid.SelectedRecords[ li ].Values[cAssignmentId]) then Continue;
    end;
    lItemId:= Grid.SelectedRecords[ li ].Values[ cItemId ] ;
    APerformingQuery.ParamByName(cParamName).AsInteger:= lItemId;
    APerformingQuery.Exeute();
    lMustRefresh:=true;
  end ;
  if (lMustRefresh) then acRefresh.Execute();
end;

Cóż tu się dzieje? Ano najpierw mamy pytanie do użytkownika, następnie sprawdzenie które Itemy są w ogóle właściwe do przetwarzania (może się okazać, że część wpisów to jedynie wpisy grupujące). Dopiero później w pętli dokonywane jest sprawdzenie każdego itemu na okoliczność pasowania do flagi sterującej i jeśli w wyniku sprawdzenia nie zostaniemy skierowani na następną iterację, dokonywana jest właściwa operacja za pomocą obiektu APerformingQuery przekazywanego jako parametr. Procedura dodatkowo sprawdza, czy faktycznie dokonała jakiś operacji, i wywołuje obiekt odświeżający GUI.Jest to klasyczny przykład kohezji logicznej: nie dość, że flagami sterującymi znacząco zmieniamy zbiór itemów wchodzących w zakres działania procedury, to jeszcze sam sens działania – obiekt wykonujący działanie również jest przekazywany jako parametr! Jedyne, w czym się procedura owa broni, to tekst komunikatu dla użytkownika, przekazany jako parametr – to może mieć sens choćby ze względu na możliwość zmiany języka.

Pozostaje zadać sobie pytanie, czy kod ten jest zły? Cóż, niewielka separacja logiki biznesowej od logiki interfejsu może jednak razić. Jednak biorąc pod uwagę, iż faktyczna logika biznesowa ukryta jest w obiekcie wykonującym rzeczywiste działanie, sprawa przestaje wyglądać tak brzydko.

Śmierdzą trochę flagi sterujące, ale… czyż bez nich nie byłoby konieczne implementowanie kilku różnych procedur, do obsługi każdego elementu interfejsu użytkownika z osobna? Kohezja logiczna,  mimo że generalnie niezbyt akceptowalna, potrafi czasem pomóc nam, zmniejszając ilość kodu obsługującego interfejs użytkownika i drastycznie zmniejszając ilość punktów styku logiki interfejsu i logiki biznesowej.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.