TenCoNieJestWDomu napisał(a)
- Jak PORZĄDNIE zaprojektować aplikacje napisaną z podejściem projektowym? Zazwyczaj starałem się wypisać na kartce potrzebne mi funkcje i poukładać je w jakieś sensowne klasy ale czy to dobre podejście?
To dobry sposób. Ja go kiedyś używałem częściej. Teraz, na co dzień, bardzo rzadko. Może dlatego, że doświadczenie mi wzrosło bardziej niż stopień skomplikowania projektów, które robię.
Stosuję za to rutynowo inne narzędzie, którego i Ty się musisz w końcu nauczyć, jeśli chcesz być dobrym programistą. Refaktoryzacja. To po prostu zmiana struktury istniejącego kodu. Brzmi prosto, ale trzeba trochę nauki i doświadczenia, by robić to sprawnie. Tj. małymi kroczkami, nie psując niczego.
Poprzez refaktoryzację możemy rozumieć np. głupią zmianę nazwy metody lub zmiennej. Albo wydzielenie metody -- gdy masz metodę, która jest za długa, i chcesz ją podzielić na kilka mniejszych. Równie często korzystam z wydzielenia klasy (raczej obiektu, bo na co dzień piszę w języku bez dziedziczenia klasycznego).
Na co dzień pracuję więc tak:
- Zastanawiam się nad rozwiązaniem. Myślę, jakie obiekty mogę wydzielić.
- Tworzę sobie zalążki tych obiektów (w Twoim przypadku: definicje klas).
- Piszę kolejne metody.
Czasami zaczynam od definicji kilku pustych metod naraz, ale często nie muszę tego robić, bo mam to w głowie. Mam we łbie zestaw narzędzi: wzorców projektowych, standardowych zachowań i typów obiektów. Np. "ten obiekt to tak naprawdę będzie mapa" albo "tamten obiekt będzie zarządzał flagami" lub "ten obiekt będzie koordynatorem", "proxy" czy jakimś kontenerem. W głowie od razu widzę część metod, jakich będę potrzebował.
- Załóżmy, że podczas pisania kodu widzę, że coś jest nie tak. Np. (pisząc w terminach klasycznego dziedziczenia) że połowa metod używa połowy własności obiektu i zarówno te metody, jak i własności, mają pewien człon w nazwie lub są logicznie mocno powiązane. Zapala mi się lampka mówiąca, że klasa cierpi na za niską kohezję, tj. nie jest wystarczająco integralna. I wydzielam klasę z tej połowy związanych ze sobą pól i metod. Wydzielona klasa charakteryzuje się wysoką kohezją, tj. wszystko jest w niej ze sobą ściśle związane. Jeśli dodatkowo jest mała i zajmuje się jedną rzeczą na jednym poziomie abstrakcji, to dla mnie informacja, że dokonałem dobrego podziału.
Nie próbuję więc od razu rozpisać pełnej struktury klas i trzymać się jej superdokładnie. Refaktoryzacja pozwala mi na dokonanie zmian gdy już dojdę do rozwiązania jakiegoś problemu i zobaczę, że pierwotny wybór nie był optymalny. Teraz mam lepsze zrozumienie problemu i mogę dokonać lepszego wyboru.
Ważne jest jednak, by te rzeczy, o których piszę, nie były dla nas pretekstem i usprawiedliwieniem do lenistwa. W porównaniu do mniej doświadczonych programistów, ja jestem naprawdę skrupulatny i konsekwentny jeśli chodzi o refaktoryzację i utrzymywanie jakości kodu. I trochę już się naprojektowałem, więc łatwiej mi ad hoc wymyślać przyzwoite struktury w miarę prostych obiektów.
Wiem też na ogół, kiedy powinienem się zatrzymać i przypisać planowaniu znacznie większy priorytet, niż standardowo.
Np. gdy planuję globalną architekturę aplikacji, których się ma trzymać N programistów w X różnych projektach. Wtedy trzeba przemyśleć mnóstwo rzeczy, wiele konsekwencji. Na firmowym wiki powstają wtedy ścisłe dokumenty -- najpierw propozycje, potem standardy. Na forum zakładane są wątki, a na odpowiednim etapie organizowane są też spotkania i brainstormingi. Architekturę sprawdza się też, w miarę możliwości, za robiąc jakiś fragment projektu od A do Z, wszystkie warstwy (tzw. metoda pocisków smugowych, wspomniana w książce "Pragmatyczny programista") lub za pomocą projektu pilotażowego.
Zdarza się, że pół godziny czy więcej spędzam na wymyślaniu nazwy dosłownie paru modułów na krzyż.
Niedawno miałem taką sytuację. Moduł zawierał:
- Obiekt/przestrzeń nazw, która miała bardzo ogólną odpowiedzialność. Ciężko było nadać jej wystarczająco konkretną nazwę. W tamtym momencie nazwę miała akurat konkretną ale, jak się okazało, zupełnie mylącą.
- Kilka obiektów o związanych z pewną funkcjonalnością. Wszystkie miały ten sam przedrostek i -- w sumie -- długie, niewygodne nazwy.
- Jeszcze trochę rzeczy, które wydawały się nazwane OK.
Nad samą nazwą z punktu 1. myślałem z pół godziny i jeszcze skonsultowałem się z dwoma innymi programistami. Jednym, który nie odnalazł się w tym module i zgłosił mi, że go nie ogarnia (bo to ja stworzyłem tę złą hierarchię -- taaak, wpadki zdarzają mi się tak jak każdemu) i jeszcze z drugim b. dobrym programistą. Ostatecznie nazwę wymyśliliśmy wspólnie z tym drugim, każdy z nas jedno słowo (lol). W punkcie 2 przeprowadziłem odrobinkę żmudną, ale prostą refaktoryzację, zmieniając nazwy obiektów i plików i przemieszczając pliki do osobnego folderu, przez co mogłem usunąć z nazw przedrostek.
Również niedawno zdarzyło mi się, że kumpel poprosił mnie o radę i godzinę rozkminialiśmy wyjątkowo upierdliwy pod względem IO problem. Rysowaliśmy na kartce diagramy, próbowaliśmy podpasować problem pod znane nam wzorce, analizowaliśmy konsekwencję proponowanych po kolei rozwiązań. A co się stanie, jak ktoś będzie chciał dodać jeszcze jeden sposób na X? Czy damy zapewnić silną typizację oraz utrzymać zasadę otwarty-zamknięty?
TenCoNieJestWDomu napisał(a)
- Czy jeżeli pozostało mi kilka funkcji, które nie pasują do żadnej sensownej klasy warto z nich zrobić dodatkową klasę, a funkcje zadeklarować jako static ?
Można, choć to oczywiście ciężko już nazwać programowaniem OO. Nieraz ciężko tego uniknąć.
Ponownie jednak: nie usprawiedliwiajmy się. Najpierw porządnie się zastanówmy, czy nie lepiej zrobić z tego jakąś klasę. Zawsze możesz opisać tu na forum sytuację i podać nazwy funkcji i problemy, jakie mają rozwiązywać. Może wymyślimy dla nich jakąś klasę.
Jako przykład mogę podać ciasteczka na stronach www. Nie wiem, czy siedzisz w webdevelopmencie. Ciasteczka to po prostu informacje zapisywane w przeglądarce: pary nazwa/wartość, które zostają zapisane na dysku i które można odczytać pomiędzy jedną wizytą użytkownika a drugą.
Przeważnie, ciasteczka obsługuje się funkcjami statycznymi. Chodzi mi akurat o język JavaScript, który nie ma dziedziczenia klasycznego, ale będę używał terminologii klasycznej. Przeglądarka udostępnia "średnio" wygodne API analogiczne do funkcji globalnych:
void setCookie(String name, String value);
String getCookie(String name);
Twórcy bibliotek dodają do tego trochę bajerów (które pominę) i porządkują ciasteczka, tworząc moduł/klasę i definiując metody klasyczne:
class Cookies {
static void setCookie(String name, String value);
static String getCookie(String name);
}
I używa się tego tak:
Cookies.setCookie("moje_ciasteczko", "wartosc ciasteczka");
println(Cookies.getCookie("moje_ciasteczko"));
Ale przecież można to napisać obiektowo -- w większości modułów, nad którymi pracowałem, takie API było dużo wygodniejsze:
class Cookie {
Cookie(String name); // konstruktor
void set(String value);
String get();
}
Można go było używać tak:
Cookie myCookie = new Cookie("moje_ciasteczko");
myCookie.set("wartosc");
println(myCookie.get());
Zysk z podejścia obiektowego widać lepiej w prawdziwej aplikacji. Tu to trochę uprościłem, ale ciasteczka, oprócz nazwy, mają też inne ustawienia: ścieżkę, można też sobie zdefiniować domyślną wartość (zwróci ją get()
gdy dane ciasteczko nie jest ustawione) itp. W podejściu obiektowym, gdy używamy ciasteczka, to ścieżkę podajemy -- tak samo jak nazwę -- tylko raz: przy wywołaniu konstruktora. W podejściu z metodami klasycznymi nazwę i ścieżkę podajemy przy każdym setCookie()
i getCookie()
.