Posts tagged ‘design’

Increasing Utility of Utility Modules

2012-12-26 18:53

Admit it: all your projects have this one slightly odd part. It has many different names: “helper” classes, “common” functions, “auxiliary” code (if you’re that erudite), “shared” subroutines… Or simply a utility module, which is how I will call it from here.

This is the logic outside of application’s core. The non-essential building blocks which are nevertheless used throughout the whole project. Heap of scraps, squirreled to plug in the holes of your language, framework or libraries.

Those flea markets are notoriously difficult to keep in check. If left to their own devices, they gradually expand, swallowing more and more of incoming logic – code that would otherwise go into more specific, adequate places. This is where you can carefully observe the second law of programming dynamics: without directed influence, entropy can never decrease.

You cannot do away with utility modules, though. There will always be code that exhibits their two characteristic properties:

  • frequent usage throughout most of the project’s codebase
  • loose coupling with the rest of the project

But neither is binary: they are more like a continuous spectrum. And to make it even more difficult, they are also constantly in flux, effected by any change that happens in the code. To remain minimal (and thus manageable), utility modules require probably the most frequent, extensive and aggressive refactoring.

So how exactly do you deal with this necessary menace? I think it’s a matter of reacting quickly and decisively when one of these properties change. For the most part, the usage and (inter)dependencies of your utility package will dictate which one of these four steps you should take:

  • When certain entity (function, class) grows universal, up to a point when it’s used by more than one subpackage inside your app, it is usually time to put it into the utility module.
    Although somewhat obvious, this point is really important for one particular reason: preventing code duplication. Without a designated place for helper/common/etc. code, you will have different implementations of the very same things spread like a vermin across the whole codebase.
    And you really don’t want to have four distinct functions for turning a sequence [X, Y, Z] into a string "X, Y and Z", or something similar to that. This is way worse than even the biggest utility module you might end up dealing with.
  • Symmetrically, whenever an entity stops being used by more than one part of the program, you may roll it back into that sole part which still requires it.
    For most, this will be the trickiest part. I can tell you upfront that you shouldn’t actually do it every time. There are things basic enough that you don’t need at all to track usage of: the “join with ‘and'” example might be one such instance.
    But there are times when it’s more reasonable to put some utility stuff back into more dedicated package, because it simply belongs there. If you need to have that “and-join” localized, for example, you will likely find it more sensible to place its modified version into internationalization package. On the other hand, if you learn that it’s used only to format log messages which are never seen by the user, you can put it directly into logging module or just scrap altogether. (After all, programmers are not easily offended by poorly punctuated sentences).
  • When utility code evolves into subpackage on its own, extract it as such.
    When your internationalization needs require not only a localized “and-join” but also a way to match numeric values with noun plurals ("1 apple" vs "3 apples"; my native language is especially complex here), this can be the reason for creating a full-blown i18n package.
    It is quite natural process, for simple but useful snippets – ones that typically land in shared module – to grow more robust, functional and thus complex. At certain size threshold, it’s reasonable to promote them into next structural level: module or package.
  • Loosening up already loose ties may call for creating an external library.
    You probably don’t write code with the express intent of reusing it across different projects, regardless of whether you were convinced by Mythical Man Month that it costs three times as much to do so. But “accidents” happen, and you can sometimes find a non-trivial piece of utility code that appears to be floating, that doesn’t depend on anything else inside the parent project.
    Should you make such a discovery, by all means make it into a library. It doesn’t have to be real, external library – much less an open source one. Releasing a project into the wild is serious and time-consuming task, so I won’t ask you to do it every time (even though the world would benefit greatly if you did).
    Treat it as third-party code, though. Separate it into different root package, add a new build target for it, include it as a .jar or .egg rather than .java or .py files – and so on. This is a small investment that makes it much more likely for the code to increase its value threefold.
    Reducing the size of that pesky utility module will be just an added bonus.

So help you helper classes and utilize your utility modules for shared, common good :)

Tags: , ,
Author: Xion, posted under Computer Science & IT » Comments Off on Increasing Utility of Utility Modules

Niekoniecznie należy słuchać ekspertów

2011-06-20 22:45

Nieodłączną częścią realizacji projektu programistycznego jest wybór używanych technologii. Dotyczy to zarówno samego początku, jak i późniejszych etapów, gdy czasami trzeba zdecydować się np. na skorzystanie z jakiejś biblioteki X lub jej zamiennika Y. Takie dylematy mają i koderzy-amatorzy (co często uzewnętrzniają na przeróżnych forach ;]), i firmy tworzące oprogramowanie komercyjnie. Decyzję trzeba jednak w końcu podjąć; czy są jakieś uniwersalne kryteria, którymi można się kierować?
Inżynieria oprogramowania każe zwykle zdać się na poprzednie doświadczenia lub wiedzę ekspertów. Ta druga opcja jest często preferowana wobec braku podstaw (tj. wspomnianych doświadczeń) do skorzystania z pierwszej. Wydaje się to rozsądne. W końcu osoba, która bardzo dobrze zna dane rozwiązanie, powinna mieć też odpowiednie pojęcie o tym, czy aplikuje się ono do konkretnego problemu.

A jednak mam co do tego całkiem poważne wątpliwości. Potrafiłbym znaleźć kilka sensownych argumentów na to, że osoba biegła w stosowaniu danej technologii nie musi być wcale dobrym doradcą. W grę mogą bowiem wchodzić silne czynniki zaburzające solidność osądu eksperta. Przychodzą mi na myśl zwłaszcza dwa:

  • Osoba świetnie znająca jakieś rozwiązanie jest z pewnością taką, która zastosowała je w co najmniej kilku projektach. Kto wie, być może nawet zakończyły się one sukcesem ;) Niezależnie od ich wyniku, zdobyte przy okazji doświadczenie prawdopodobnie nauczyło naszego eksperta, jak powinien radzić sobie z przeróżnymi kruczkami, niejasnościami, niedoróbkami, brakami czy wręcz jawnymi bugami występującymi w danej platformie czy bibliotece. Jeśli wielokrotnie i z powodzeniem dawał sobie z nimi radę, jest wielce prawdopodobne, iż będzie (nawet nieświadomie) umniejszał ich znaczenie podczas “obiektywnej” oceny całego rozwiązania. Po prostu z perspektywy czasu nie będą się one wydawały aż tak dolegliwe, jakie mogą być w rzeczywistości dla osoby słabiej obeznanej z daną technologią.
  • Nasz ekspert od rozwiązania X na pewno poświęcił sporo czasu i mocy obliczeniowej swoich neuronów na dokładnie zapoznanie się z jego szczegółami. Wysiłek ten sprawił przy okazji, że w naturalny sposób zaczął on wyżej cenić X-a, ponieważ w ten sposób potwierdzał w swoich oczach słuszność decyzji, aby rozwijać swoją znajomość tego zagadnienia. Dzięki tej (znów: zwykle nieświadomej) praktyce, nasz ekspert zapobiegł powstaniu dysonansu poznawczego (cognitive dissonance) lub przynajmniej znacząco go zredukował. Efektem ubocznym tego zjawiska jest jednak zaburzone postrzeganie wartości X-a jako rozwiązania, związane głównie z tzw. błędem potwierdzania (confirmation bias) lub zaprzeczania (disconfirmation bias). Krótko mówiąc, nasz ekspert generalnie przywiązuje większą wagę do pozytywnych ocen X-a, a mniejszą do negatywnych. Niezbyt dobrze wróży to jego obiektywności.

Jak wynika z powyższych argumentów, wypowiedziane przez eksperta zdanie na temat przydatności jakiegoś rozwiązania niekoniecznie musi być obiektywne i wartościowe. Oczywiście nie twierdzę, że największe pole do popisu przy podejmowaniu decyzji powinni mieć wobec tego początkujący, bo to z kolei zakrawa na przegięcie w drugą stronę :) Sądzę jednak, że najbardziej kompetentnymi i obiektywnymi osobami byłyby takie, które wprawdzie stosowały dane rozwiązanie w praktyce, ale nie są w nim całkiem biegłe. A już zupełnie dobrze byłoby wówczas, jeśli mają one też pewne pojęcie o porównywalnych, alternatywnych rozwiązaniach.

Regulacja głośności jako porażka funkcjonalna

2011-02-21 19:36

Fajnie jest używać oprogramowania, które da się łatwo dopasować do swoich upodobań – zwłaszcza, jeśli da się to zrobić łatwo, szybko i intuicyjnie, bez konieczności czytania długich manuali. Niestety, konstruowanie interfejsu udostępniającego szeroką funkcjonalność oraz wcześniej wspomniane cechy nie jest łatwe. Dla mnie dobrym przykładem tego, co może pójść źle podczas próby uszczęśliwienia użytkownika mnogością opcją i elastycznością konfiguracji jest banalny na pierwszy rzut oka mechanizm: regulacja głośności dźwięku w komputerach PC.

Problem z nim związany polega – mówiąc ogólnie – na rozbiciu zagadnienia na mniejsze fragmenty (dzięki czemu w teorii uzyskujemy większą kontrolę nad każdym z nich) bez zastanowienia się nad niepożądanymi efektami wzajemnych relacji między tymi fragmentami. Regulacja głośności w komputerach PC występuje bowiem obecnie nie w jednym, a w kilku miejscach. W skrajnych przypadkach może być ich więcej, niż da się policzyć na palcach jednej dłoni, gdy w grę wchodzi:

  • sprzętowe pokrętło głośności, będące częścią słuchawek, zestawu głośników lub kontrolek na obudowie laptopa
  • globalne, systemowe ustawienie głośności dźwięku (w Windows dostępne po kliknięciu na ikonkę w zasobniku)
  • globalne ustawienie głośności regulowane w panelu kontrolnym sterownika karty dźwiękowej
  • systemowy poziom dźwięku ustawiany dla każdego z głośników (centralnego, subwoofera, itd.)
  • systemowy poziom dźwięku ustawiany dla poszczególnych aplikacji lub komponentów systemowych
  • właściwy dla aplikacji (np. gier) poziom głośności ustawiany w samym programie

Ałć. Sporo tego, prawda? W celu usprawiedliwienia tego bałaganu można argumentować, że za prawie każdy z elementów tej listy odpowiada ktoś, jako że znajdują się one w rożnych warstwach abstrakcji. Tak też w teorii powinniśmy je traktować i nawet czerpać korzyści z tego faktu, na przykład poprzez przyciszenie efektów dźwiękowych w grze na rzecz muzyki z działającego w tle odtwarzacza.
Teoria zaś, jak wiemy, niczym nie różni się od praktyki – ale tylko w teorii. W praktyce możemy mieć zupełnie inny przypadek użycia, kiedy na przykład próbujemy rozmowy przez komunikator i stwierdzamy, że nie słyszymy drugiej strony wystarczająco głośno. Będąc rozsądnym użytkownikiem (optymistyczne założenie ;]) udamy się wpierw do ustawień programu i tam wyregulujemy głośność. Ale jeśli to nie pomoże, zapewne w drugim kroku pokręcimy odpowiednim pokrętłem lub przesuniemy globalny systemowy suwak. W konsekwencji następny chord.wav czy inny dźwięk będący częścią UI systemu może nam dostarczyć, mówiąc oględnie, zaskakująco intensywnych wrażeń słuchowych ;)

To jest właśnie przykład niepożądanej interakcji pomiędzy zachodzącymi na siebie fragmentami zagadnienia. Ale w przypadku regulacji głośności nawet pożądane interakcje nie są oczywiste. Czy łatwo jest bowiem określić, w jaki sposób X% nadrzędnego i Y% podrzędnego poziomu dźwięku przełoży się na to, co ostatecznie usłyszymy w głośnikach? Wymaga to chwili zastanowienia, a przecież mówimy tu o czynności, którą powinno się wykonywać automatycznie, bezwiednie i niemal zupełnie nieświadomie! Nie spodziewam się też, aby statystyczny użytkownik miał jakiekolwiek pojęcie o istotnym tutaj prawie Webera-Fechnera, które dodatkowo wpływa na faktycznie słyszaną intensywność wynikowego dźwięku.

Z tych wszystkich narzekań wyłania się wniosek, że regulacja głośności w PC-tach to zagadnienie o sztucznie zawyżonym poziomie komplikacji. Mogłoby ono być znacznie prostsze, gdyby nie obciążono go balastem pozornej konfigurowalności. Jako dobry przykład może służyć analogiczny mechanizm w telefonach, opierający się na dokładnie jednej sprzętowej kontrolce poziomu dźwięku (np. przyciskach) i braku zależności między poszczególnymi ustawieniami (np. multimediów, dzwonka czy rozmowy).

Tags: , ,
Author: Xion, posted under Computer Science & IT, Thoughts » 9 comments

DOD organizuje dane

2011-01-08 18:56

Moi koledzy-częściowo-po-fachu, czyli programiści silników gier, wymyślili niedawno magiczny trzyliterowy akronim DOD – skrót od Data-Oriented Design, czyli projektowanie oparte o dane. Oczywiście określenie ‘niedawno’ jest względnym i pewnie wielu z nich orzekło by, że DOD jest z nimi już całkiem długo. Każdy mem potrzebuje jednak czasu na rozprzestrzenienie się, a w przypadku tego fala tweetów na jego temat dotarła do mnie dopiero niedawno. Niedługo potem rzecz wydała mi się cokolwiek podejrzana.

Podstawowe pytanie brzmi rzecz jasna: o co w tym właściwie chodzi?… Ponieważ mówimy o programowaniu gier, to odpowiedź jest jasna: jeśli nie wiadomo o co chodzi, to chodzi o wydajność. W zaawansowanych grach czasu rzeczywistego mamy do czynienia z ogromną ilością danych, na których trzeba wykonać wiele, często skomplikowanych operacji, a wszystko to jeszcze musi być zrobione dostatecznie szybko, aby możliwe było pokazanie na ekranie kolejnej klatki bez widocznych przycięć. Dlatego też już dawno zauważono, że kodowanie “blisko sprzętu” się opłaca, bo pozwala maksymalnie wykorzystać jego możliwości.
To oczywiście nakłada na kod pewne wymagania oraz stwarza konieczność zwrócenia uwagi na rzeczy, którymi “normalnie” nie ma potrzeby się zajmować. Ładnym przykładem jest chociażby zarządzanie pamięcią. W wielu językach jest ono albo kompletnie pomijalne (garbage collector), albo sprowadza się do dbania o to, aby każdy zaalokowany blok był w końcu zwolniony. Gdy jednak stawiamy na wydajność, powinniśmy też zainteresować się szybkością samej operacji alokacji oraz takim rozmieszczeniem przydzielanych bloków, aby komunikacja na linii procesor-pamięć odbywała się z jak najmniejszą liczbą zgrzytów.
Ten i wiele podobnych szczegółów platformy sprzętowej powodują, że pisanie efektywnego kodu w silnikach gier to często dość literalne postępowanie według zasady Do It Yourself, połączone z ignorowaniem części feature‘ów wysokopoziomowych języków programowania, o których wiadomo, że negatywnie odbijają się na wydajności. Cóż, życie; nie ma w tym nic zaskakującego. Myślę, że każdy co bardziej zaawansowany programista zdążył zdać sobie sprawę z tego, że wszelkie koderskie udogodnienia związane z podniesieniem poziomu abstrakcji mają swój koszt liczony w dodatkowych cyklach procesora (i nie tylko). Rezygnacja z nich jest więc dobrym posunięciem, jeśli chcemy te “stracone” cykle odzyskać.
Robiąc to, będziemy mieli ciastko, ale już nie będziemy mogli go zjeść – a to oczywiście nie jest przyjemne. I po części zapewne stąd wzięło się pojęcie DOD, które nie odnosi się do niczego w gruncie rzeczy nowego, ale pozwala łatwiej odnosić się do tego rodzaju koderskich praktyk poprzez nadanie im nazwy. A przy okazji – jak mi się wydaje – w jakiś nie do końca wytłumaczalny sposób redukuje dysonans poznawczy programistów silników gier, którzy świadomie muszą pozbawiać się możliwości przestrzegania “jedynie słusznych” zasad pisania kodu.

Jak dotąd wszystko jest w gruncie rzeczy bardzo ładne i sensowne, i bez problemu zgadzam się z postulatami Data-Oriented Design tam, gdzie się one aplikują. Zgadzam się nawet z tą domniemaną ukrytą motywacją, zwłaszcza że sam nieraz narzekałem na owe “jedynie słuszne” rady. Za to nijak nie mogę pojąć, dlaczego następnym krokiem – po wynalezieniu pojęcia DOD – był mniej lub bardziej frontalny atak na programowanie obiektowe, określane nieco bardziej znanym (ale naturalnie również trzyliterowym) akronimem OOP.
Nie, nie chodzi o to, że programowanie obiektowe jest doskonałe – bo nie jest, nie było, nigdy nie będzie i nawet nie aspiruje do miana finalnego rozwiązania dla dowolnego problemu (już nie wspominając o tym, że takowe po prostu nie istnieją). Rzecz w tym, że zwolennicy DOD (DOD-a? :]) w nieprzemyślany sposób wybrali sobie przeciwnika, nie zauważając, że jest on paradygmatem zupełnie innego rodzaju niż ich własny. A to przecież takie proste:

  • OOP sugeruje, że naturalną architekturą dla systemów jest ta oparta o obiekty, będące mniej lub bardziej abstrakcyjnymi bytami, połączonymi relacjami i wchodzącymi ze sobą w interakcje
  • DOD wskazuje na to, że głównym zadaniem kodu jest transformacja danych w inne dane i że optymalizacja procesu przetwarzania danych jest priorytetem przy tworzeniu wewnętrznej struktury programu

Widać to, prawda?… Miedzy powyższymi dwoma podejściami nie tylko nie ma sprzeczności. One są od siebie po prostu niezależne, co oznacza również, że mogą występować razem w jednym programie.


“Mapa nie jest terytorium.”

Jeśli Data-Oriented Design koniecznie potrzebuje jakiegoś przeciwnika, to są nim raczej inne xOD-y, których jest już przynajmniej kilka, chociaż wiele nie zostało jeszcze nawet nazwanych. (Dobry przykład to projektowanie oparte o user experience, czyli wrażenie użytkownika, gdzie priorytetem jest m.in. responsywność, nie będąca wcale synonimem wydajności). To, co piewcy DOD zdają się krytykować w swoich publikacjach, to jakieś “projektowanie oparte o eleganckie abstrakcje”, czyli pisanie kodu, który jest sztuką dla sztuki: ładnie wygląda (w założeniu), ściśle trzyma się założeń używanego paradygmatu przy jednoczesnym eksploatowaniu wszelkich jego “zdobyczy” (czyli np. wzorców projektowych). I chociaż bywają w swoich wysiłkach niezwykle twórczy (w prezentacjach z tego tematu spotkałem nawet cytaty z Baudrillarda), to nie zmienia to faktu, że kopią leżącego (czy raczej biją martwego konia, jakby to powiedzieli Amerykanie ;-)). Bo jeśli ktoś naprawdę posuwa się do takich absurdów jak czteropoziomowa hierarchia dziedziczenia obiektów gry, to znaczy że ma znacznie poważniejsze problemy niż okazjonalny cache miss :)

Kiedy kod jest przejrzysty

2010-12-21 22:44

Pisanie kodu to czasami niewdzięczne zajęcie, ale tylko z rzadka. Nieprzyjemnych wrażeń o wiele częściej, jak sądzę, dostarcza czytanie kodu. Jest tak po części pewnie dlatego, że statystycznie programista przeznacza na to zajęcie znacznie więcej czasu (wystarczy policzyć chociażby debugowanie). Innym powodem może być też to, że dokładne zdefiniowanie takiego określenia jak przejrzysty kod okazuje się zaskakująco trudno, bo kategoria ta wydaje się w dużym stopniu subiektywna.

Weźmy na przykład komentarze. Potoczna koderska mądrość mówi nam, że łatwy do zrozumienia kod zapewne posiada dużo komentarzy, bo w końcu jak inaczej opisać zawiłości konstrukcji językowych i bibliotecznych, które się w nim znajdują?… Może to i przekonująca logika, ale ja mam lepszą. Można by przecież liczbę tych zawiłości minimalizować jeśli nie eliminować zupełnie. Kod przejrzysty powinien dokumentować się w zasadzie sam. Wyjątki istnieją, oczywiście, bo zawsze znajdą się jakieś kruczki, haki i inne triki, które można opatrzyć jedynie napisem // Magic. Do not touch. (o ile nie chcemy opisywać ich działania w elaboracie na piętnaście linii). Jeśli jednak większość kodu wygląda “magicznie”, to jego autora należy czym prędzej przeszkolić w bardziej mugolskich technikach programowania :)

Trochę odrębnie od przygodnych komentarzy traktuję wstawki mające służyć na dokumentację do poszczególnych elementów kodu. Tworzenie jej w pełnej formie, z opisem każdego pola, metody i parametru jest nie tylko czasochłonne, ale i w dużej części niezbyt sensowne. Z drugiej jednak strony brak chociaż słowa wzmianki nt. tego, co dana klasa czy metoda w zasadzie robi to zbrodnia popełniona na nieszczęśnikach, którzy potem muszą przedzierać się przez taką terra incognita. Można obyć się bez wiedzy, jak dany element działa, ale informacja o tym, do czego służy lub co ma w założeniu robić jest po prostu niezbędna.

Komentarze, według mnie, to jednak w ostatecznym rozrachunku sprawa drugorzędna. Znacznie ważniejsza – być może nawet pierwszorzędna – zdaje mi się bowiem odpowiednia organizacja na każdym poziomie projektu. Zaczyna się ona od rozmieszczenia funkcjonalności w klasach, modułach i pakietach, a kończy na układzie samych deklaracji w plikach. Ogólna zasada jest tu bardzo prosta: chodzi o to, by elementy podobne i odpowiedzialne za zbliżone zadania były fizycznie blisko siebie. Regułę tę można łamać w przypadkach uzasadnionych i chęć podziału kodu na warstwy (np. logiki i interfejsu) jest na pewno jednym z nich. Często nie da się tego jednak powiedzieć np. o grupowaniu ze sobą w klasie metod o tej samej widoczności zamiast podobnej i/lub zależnej funkcjonalności

Wreszcie, o przejrzystości kodu decydują też wrażenia wzrokowe otrzymywane w czasie jego oglądania. Wpatrywanie się w długie, gęsto zapełnione znakami wiersze o znikomej ilości pustych przestrzeni jest zajęciem męczącym i dlatego powinniśmy oszczędzać go sobie i innym. Stosujmy więc białe znaki, i to rozrzutnie. Spacje, podziały wiersza oraz puste wiersze (a czasami i wcięcia, jeśli język na to pozwala) używane w przemyślany sposób poprawiają wizualny odbiór listingów i jednocześnie poprawiają wspomnianą wcześniej organizację – tym razem na poziomie mikro.

Tags: , ,
Author: Xion, posted under Programming, Thoughts » 2 comments

Długie ciągi odwołań

2008-08-11 22:12

Chociaż obiekty powinny być jak najbardziej autonomiczne i możliwie niezależne od innych, to bardzo często zdarza się sytuacja, gdy trzeba “zrobić coś” używając czegoś “powszechnie dostępnego”. W praktyce chodzi też o to, by obiekty nie miały kilometrowej długości konstruktorów, do których trzeba by przekazywać wszystko, z czego ewentualnie będą one chciały kiedyś skorzystać.
Dlatego też stosuje się raczej inne rozwiązania, pozwalające w razie potrzeby ‘skoczyć’ w inne miejsce całej sieci powiązań między obiektami, jaka zwykle występuje w programie.

Sieć ta ma zresztą często formę drzewka, wychodzącego od wspólnego korzenia (jakiegoś ‘superobiektu’, reprezentującego całą aplikację), zawierającego w sobie wszystkie obiekty niższych rzędów. Logiczne jest więc zapewnienie odpowiednich metod, pozwalających na dostęp do nich. Wówczas można zawsze rozpocząć od samej góry i, schodząc coraz niżej, dojść w końcu do interesującego nas obiektu.
Z drugiej strony można też rozpoczynać wędrówkę zawsze od obiektu, w którym aktualnie jesteśmy, i w razie potrzeby poruszać się też w górę. Aby było to możliwe, należy jednak dla każdego obiektu jasno określić jego obiekt nadrzędny (parent) i dać możliwość dostępu do niego (np. poprzez przekazanie w konstruktorze odpowiedniego wskaźnika).

Niestety, obie te metody mogą w niektórych sytuacjach zaowocować długimi ciągami wywołań w rodzaju:

  1. ParentObject()->ParentObject()->SubObject3()->Objects(56)->SubObject1()

Według mnie jest to jednak żadna wada. Podobne potworki świadczą tak naprawdę, że struktura programu jest nieprzemyślana i niezgodna z potrzebami. Nic zatem dziwnego, że korzystanie z niej jest trudne i niewygodne. W tym przypadku można wręcz powiedzieć, że język programowania wykazuje się pewną inteligencją, jednoznacznie wskazując nam, że coś robimy źle ;-)

Tags:
Author: Xion, posted under Programming » 11 comments

Wiosenne porządki #3 – Miękki refaktoring

2008-04-10 17:24

Kiedy piszemy jakieś klasy, wydaje nam się, że dokładnie przemyśleliśmy ich interfejs, który jest intuicyjny, łatwy w użytkowaniu, wydajny, rozszerzalny, i tak dalej. Musi tak być, skoro sami go napisaliśmy, prawda? ;-) Cóż, życie niestety aż nazbyt często weryfikuje to jakże naiwne założenie. A wtedy pozostaje nam przyjrzeć się, co zaprojektowaliśmy nie dość dobrze i próbować to zmienić.
Jest tylko jedno “ale”: do chwili, gdy zdecydujemy się na zmiany w interfejsie, nasza klasa może być już wykorzystywana w innych fragmentach projektu lub zgoła nawet w innych projektach. To może dotyczyć i takich, których nie jesteśmy autorami – jeżeli tylko zdecydowaliśmy się udostępnić naszą twórczość szerszej publiczności. A wtedy sprawa staje się co najmniej problematyczna, bo każda nieprzewidziana modyfikacja może spowodować kaskadę kolejnych zmian, jakie trzeba będzie poczynić w odpowiedzi na nią.

Jeśli naturalnie bardzo tego chcemy możemy je wszystkie przeprowadzić. Wówczas przynajmniej nasz interfejs będzie znów elegancki – przynajmniej do czasu, gdy stwierdzimy, że znów już taki nie jest ;] Jest to jak najbardziej możliwe, ale pewnie nie trzeba wspominać, jak pracochłonna może być taka operacja.
Spójrzmy raczej na sposób, w jaki radzą sobie z tym problemem twórcy szeroko wykorzystywanych bibliotek programistycznych różnego rodzaju. To, co jest ich cechą wspólną w kolejnych wersjach, to zachowywana zawsze kompatybilność wstecz. Jest ona osiągana przez modyfikacje polegające wyłącznie na dodawaniu elementów interfejsu bez zmiany już istniejących. Pewnie najbardziej znanym przejawem takiej praktyki jest istnienie funkcji z końcówką Ex w Windows API, które robią trochę więcej niż ich “zwykłe” odpowiedniki i mają nieco zmienioną listę parametrów.

To oczywiście nie jedyna droga nieinwazyjnego poprawiania interfejsu. Takich sposobów jest co najmniej kilka, jak chociażby:

  • Wprowadzanie całkiem nowych klas rozszerzających funkcjonalność już istniejących. Można w tym celu wykorzystać agregację (obiekt nowej klasy zawiera obiekt starej), dziedziczenie prywatne czy nawet publiczne. Ważne jest, aby nowa klasa na tyle różniła się od oryginalnej, by jej istnienie było uzasadnione i nie powodowało dylematów pod tytułem “Której klasy mam użyć?…”.
  • Uzupełnienie istniejących klas o nowe metody. Dzięki temu, że nie zmieniamy żadnego z już istniejących składników klasy, stary interfejs nadal będzie dostępny.
  • Dodanie do metod wersji przeciążonych i/lub parametrów domyślnych. Jest to odpowiednik wspomnianego przyrostka Ex. Zauważmy, że po takiej operacji możemy zmienić implementację starych metod tak, aby wewnętrznie korzystały one z nowych, rozszerzonych wersji. Póki nie zmieni się sposób ich wywoływania, kod pozostanie kompatybilny wstecz.

Można się zastanawiać, czy taki “miękki refaktoring” nie jest odkładaniem na później tego, co i tak należy wykonać? Zapewne tak: przecież każdy kod można zawsze napisać lepiej, czyli od nowa :) Trzeba jednak odpowiedzieć sobie na pytanie, czy korzyści z tego będą większe niż włożony wysiłek. Jeżeli nie kodujemy jedynie dla samej przyjemności kodowania, to nie ma cudów: odpowiedź najczęściej będzie negatywna.

Tags:
Author: Xion, posted under Programming » 7 comments
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.