Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Krótka opowieść programistyczna o Swingu-Kłamczuszku

Jak często spotykacie programistów, którzy na pytanie: „Jakie technologie lubisz najbardziej?” odpowiedzą: „Im starsze, tym lepsze, koleś!”? Pewnie nieczęsto. Starożytne budowle do dziś potrafią zachwycać inżynierów swoją trwałością i przemyślaną konstrukcją. Tymczasem „zaledwie” 10 lat w informatyce jest okresem, w którym futurystyczne wizje odchodzą do lamusa, a pomysły relatywnie świeżę uznawane są za przestarzałe.

Nie zawsze jednak możemy pracować w tym w czym byśmy chcieli, w szczególności jeśli brak doświadczenia nie pozwala nam na szybką ocenę przyszłej pracy (na samym dole się do tej myśli odnoszę – przeczytaj nim skomentujesz!). W efekcie przychodzi nam pracować w projekcie, który jeszcze długie lata będzie rozwijany, mimo że pod spodem mamy mały „skansen”.

Technologią, którą przyszło mi „pokochać” jest od jakiegoś czasu Swing, czyli biblioteka tworzenia GUI, z którą Java żyje jakieś 20 ostatnich lat. Inaczej mówiąc, Swing powstał w czasach w czasach nie mniej ciekawych niż obecne (choć chyba bardziej pionierskich). Z jednej strony był to okres premier pierwszych Windowsów z serii 3.x, pierwszej stabilnej wersji języka Java oraz początków WWW. Z drugiej strony był to czas prężnego rozwoju inżynierii oprogramowania, a w szczególności dzięki Bandzie Czterech spopularyzowany został wtedy termin Wzorzec projektowy. Wiele z pomysłów, które dziś są czymś oczywistym (w znaczeniu: „oczywiście, że są złe” lub „oczywiście, że są dobre”), wtedy dopiero przebijały się do mainstreamu.

r   e   k   l   a   m   a

M-WTF-C

Jeśli ktoś miał styczność z rozwojem interfejsów graficznych, prawdopodobnie korzystał z jednej z implementacji wzorca MVC. Architektci Swinga również o nim słyszeli. W artykule opisującym proces projektowy możemy jednak przeczytać:

The first Swing prototype followed a traditional MVC (…).
We quickly discovered that this split didn't work well in practical terms because the view and controller parts of a component required a tight coupling (…). So we collapsed these two entities into a single UI (user-interface) object…

Nie sposób sprawiedliwie ocenić tamtejsze decyzje (trzeba np. pamiętać, że Swing powstawał wiele lat przed wprowadzenie typów generycznych do Javy). Widać jednak, że technologia ta nie przystaje już do obecnych nam czasów, w szczególności, gdy powstaje coś większego niż hello-world-calkulator. Nie tylko coraz bardziej wonieje kod z pomieszanymi rolami, ale choćby samo wykańczanie rolki myszki od przewijania zbyt dużych klas powinno zapalać „żółte światło”.

:-|

?V?

Pozostaje traktować to jako swoisty znak czasów. Mam więc Swinga z jego szczególną wersją MVC. Zaczynamy projektować swój interfejs w zgodzie z jego architekturą. Nasza aplikacja posiada wyraźny podział na model i widok-kontroler.

BUM! Budzi mnie wgniatająca się w policzek klawiatura. Orientujecie się, że to był tylko sen. Z ekranu spogląda na mnie jakaś programistyczna chimera :-( .

Z jakiegoś powodu w Swingu są komponentu, które z całego MVC oferują tylko środkowe V (trochę jak środkowy palec prawej dłoni, hmmm).

Polubiłeś M-VC? No to szkoda :->

Przykład dla zainteresowanych: JLabel (zresztą chyba jeden z częściej wykorzystywanych komponentów). Na pierwszy rzut oka obiekt niezmienny, czyli raz utworzony, pozostaje taki na zawsze. Ale czy na pewno? Nic go nie chroni przed skutkami ubocznymi, np. możemy zrobić tak:


// etykieta udająca, że jest tylko widokiem
JLabel justSimpleLabel = new JLabel("Nie robię nic ciekawego");
justSimpleLabel.setText("Okej, jednak robie...");

Zatem nie jest to zwykły widok. A mimo to nie ma modelu, którym można by manipulować…

:-/

M vs. V

MVC jeszcze raz. Idea jest prosta: trzy luźno powiązane elementy, mające w jak najlepszy sposób spełniać swoje role. A Swing? Powinno być podobnie: dwa niezależne elementy, (mimo ograniczeń) mające w jak najlepszy sposób spełniać swoje role. W szczególności oznacza to, że widok-kontroler najlepiej odzwierciedla wizualną stronę i relację z modelem, sam model zaś najlepiej nadaje się zarządzania danymi. Przy okazji w Swingu dla „uproszczenia” widok-kontroler oferuje również bezpośredni dostęp do zawartości modelu. No to sprawdźmy to…

Jednym z częściej używanych komponentów jest pole tekstowe. Tutaj JTextField. Jest też model, tu nazwany Document. Oferuje on mniej więcej to, czego można oczekiwać (a może nawet trochę więcej). Sprawdźmy więc jak to działa najbardziej trywialnym przykładzie, w którym manipulujemy zawartością pola tekstowego:


// metody widoku, czyli zbędny dodatek

// pobranie tekstu z widoku
String content = myTextField.getText();

// przekazanie tekstu do widoku
String newContent = "nowa zawartość";
myTextField.setText(newContent);


// vs.


// metody modelu, czyli najważniejszy dla nas fragment

// pobranie tekstu z modelu
int length = document.getLength();
String contentTheHardWay;
try {
    contentTheHardWay = document.getText(0, length);
} catch (BadLocationException badLocationException) {
    new OhWhyCheckedException("I'm sorry... So sorry...", badLocationException);
}

// przekazanie tekstu do modelu
String newContentTheHardWay = "nowa zawartość";
try {
    document.remove(0, document.getLength());
    document.insertString(0, newContentTheHardWay, null);
} catch (BadLocationException badLocationException) {
    new OhWhyCheckedException("I'm sorry... So sorry...", badLocationException);
}

Jak widać, w Swingu model zawsze ma priorytet jeśli chodzi o wygodę, klarowność, bezpieczeństwo. W przykładzie z JTextField widok nie pozwala określić zbyt wielu parametrów wejściowych… Co jest jak najbardziej ZŁE. Co gorsza, jeśli w metodzie widoku coś pójdzie nie tak, jak wtedy żyć? Tylko używając modelu, możemy uprościć sobie pracę jawnie przekazując parametry (i to, że będziemy to ZAWSZE robić tak samo, byłoby tylko niegrzeczną wymówką od nierobienia tego w ten sposób!). Eh, spójrzcie jeszcze na te FATALNE nazwy metod w widoku! Najważniejszy jest jednak kontrolowany wyjątek. Dzięki niemu żaden śmierdzący i leniwy programista nie zignoruje krytycznych wyjątków naszego pola tekstowego. WIN WIN WIN…

:-[

M vs. V jeszcze raz

Idea jest prosta: trzy luźno powiązane elementy… I ten nieszczęsny JTextField.

JTextField ma krewniaka. JFormattedTextField. Idea jest świetna: wystarczy ustawić odpowiedni format akceptowalnej zawartości i reszta robi się sama. Tylko liczby? Tylko kwoty pieniężne? Tylko konta bankowe? Proste, a nawet bardzo proste! Przestałem tego używać po zmarnowaniu całego dnia na szukaniu błędu…

Zaczęło się od tego, że dostałem prośbę, by przy wprowadzaniu liczb kropka '.' zamieniana była na przecinek ','. Prosta prośba, którą jednak można zrealizować na wiele różnych sposobów. Ja wziąłem Document i dopiąłem do niego DocumentFilter (mimo swej nazwy można nim zarówno zubażać zbiory wprowadzanych znaków, jak i wzbogacać). Na zwykłym polu tekstowym mój „filtr” po wprowadzeniu kropki zmieniał ją na przecinek. Niestety w przypadku JFormattedTextField efektu nie było żadnego…

Prosta idea MVC mówiąca o luźno powiązanych elementach coś oznacza. Oznacza, że komponenty mają na siebie tylko taki wpływ, jaki muszą. Co to jednak znaczy „muszą”? JFormattedTextField w hierarchii Swinga jest widokiem-kontrolerem, a dokument modelem. Ale czy na pewno? No cóż… JFormattedTextField jest raczej widoko-kontrolero-modelem, który w swej schizofrenii dalej pozwala na wtłoczenie Documentu. Dodanie filtra do tego Documentu jest jednak bezcelowe, gdyż za chwilę nasz swingowy paczworek podmieni go na swój własny… Ręka do góry ilu z was potrafiłoby się tego domyślić w rozsądnym czasie?

:-(

Jestę komponentę… JComponentę!!!

Porzućmy wreszcie to umęczone MVC. Lubię idee czystego kodu. Czemu więc nie przyjrzeć się pod ich kątem Swingowi?

Lubię np. przewidywalność kodu. Weźmy znane i lubiane (mhm, ^_^ ) figury. Figury dają się narysować. Kwadrat też jest figurą! I jak wezmę kwadrat, to próba narysowania go też skończy się rysunkiem! Nie melodią, nie dostawcą pizzy czekającym na gotówkę, ale rysunkiem! W Swingu jest tak saaaa… W Swingu bywa różnie.

Co będzie naszym swingowym odpowiednikiem figury? Będzie nim JComponent. JLabel to JComponent.

… i JCheckBox to też JComponent.

… i JButton to też JComponent.

… i JTable to też JComponent.

I generalnie rzecz biorąc jest spoko. Jak chcę żeby mi mój komponent się przerysował, to nie interesuje mnie jaką konkretnie jest implementacją:


List<JComponent> components = createList();
components.add(new JTable());
components.add(new JButton());
// itd...
foreach (JComponent component : components {
    component.repaint();
}

Ma sens? Ma.

To teraz załóżmy, że programuję arkusz kalkulacyjny w Swingu. Najważniejszą funkcją arkuszy kalkulacyjnych jest oczywiście kolorowanie komórek. W Swingu możemy sobie wziąć tabelkę i pokazać jej w jaki sposób należy renderować poszczególne komórki, żeby miały nasze ulubione kolory. Służy do tego TableCellRenderer, który dla programisty jest prostym interfejsem. Całość wygląda dość fajnie – ktoś użył odpowiedniego rozwiązania w odpowiednim miejscu (konkretnie wzorca Strategia ).

Co jeśli wolimy jednak by Swing rysował komórki po swojemu? JTable zacznie z domyślnie ustawionym DefaultTableCellRenderer. Tylko czym on właściwie jest? Patrzymy na hierarchię dziedziczenia i… JComponent??? A pamiętacie jak pisałem, że lubię przewidywalność kodu? No to:


void invalidate()
// Overridden for performance reasons.

boolean isOpaque()
// Overridden for performance reasons.

void repaint()
//Overridden for performance reasons.

// itd...

Komponent, który ma robić za konkretną „strategię”? A potem komuś i tak nie podobają się jego metody i wycina ich logikę? To po co w ogóle dziedziczyć po JComponent? Gdzie jest sens tego?

Jestę JComponentę… i niczym więcej?

Wiemy już czym są nasze komponenty. Dużo gorzej się robi, gdy się dowiadujemy, czym one nie są… Ale od początku. Mamy sobie w okienku nagłówek i kilka komponentów, w których można coś poprzestawiać. Chcemy też, żeby w wersji pirackiej za karę nie dało klikać się w te komponenty:


JLabel headerLabel = new JLabel("Nagłówek");
JTable table = new JTable();
JButton button = new JButton();
JCheckBox checkBox = new JCheckBox();

void disableIfIllegal() {
    if (!isLegal()) {
        table.setEnabled(false);
        button.setEnabled(false);
        checkBoxsetEnabled(false);
    }
}

„Małpia” robota, co nie? Widać, że wszystkie nasze komponenty mają metodę setEnabled() . Wszyscy znamy podstawową zasadę DRY oraz wierzymy w umiejętności twórców Javy. Wystarczy zatem sprawdzić, który z interfejsów implementowanych przez wszystkie nasze komponenty odpowiada za tę metodę i dzięki polimorfizmowi postanawiamy zrobić coś na kształt:


void disableIfIllegal() {
if (!isLegal()) {
    foreach (SomethingLikeEnableable enableable : getComponents()) {
        enableable.setEnabled(false);
    }
}

Do pełni szczęścia brakuje nam jeszcze tylko nazwy interfejsu. Sprawdzamy więc… I co? Okazuje się, że nie ma żadnego Enableable. Każde setEnabled jest po prostu jakąś metodą i tylko przypadkiem nazywają się one tak samo. A nasze sprytne foreach? Może gdyby w Javie było duck typing. A jeśli komuś urodziło się sto komponentów w okienku?


component1.setEnabled(false);
component2.setEnabled(false);
component3.setEnabled(false);
// ... ^_^
component100.setEnabled(false);

:'-(

A kiedy siła argumentu nie starcza, pojawia się argument siły…

W chwilach słabości zdarza mi się czasem wpadać na głupie pomysły. Jest np. 15:00 i brakuje mi poczucia konstruktywnie spędzonego czasu w pracy, ponieważ od 9:00 jedyne co udało mi się napisać, to 15 prób rozwiązania problemu, którego natury nie rozumiem ja, ani żaden z kolegów, ani żaden z użytkowników StackOverflow, ani żadnego innego pomocnego forum… Czasem zaczynam się w takich sytuacjach zastanawiać, czy może po prostu błąd nie leży gdzieś po stronie używanego narzędzia.

I kiedy kiedyś nie mogłem zrozumieć natury błędu związanego z używaniem przeze mnie JTable, w akcie desperacji, po prostu postanowiłem sprawdzić bebechy JTable (wchodzicie tam na własne ryzyko). Jest tu według mnie kilka problemów.

Po pierwsze jak duży musi być pojedynczy plik z kodem, by był za duży? Dla mnie za duży jest wtedy, kiedy źle się go czyta. Słusznie? Nie wiem. To może zobaczmy co inni o tym sądzą, np. tu.

Po drugie, nawet gdybyśmy się umówili, że sam rozmiar pliku nie jest żadnym wyznacznikiem, tu według mnie mamy przykład klasy, w której dzieje się po prostu zbyt wiele. Pomijając wspominaną wcześniej przypadłość Swinga polegającą na tym, że mimo dostępu do modelu, widok-kontroler i tak „dubluje” jego metody, mamy tu zarówno wysokopoziomowe operacje związane np. z dostosowaniem powierzchowności, jak i dość niskopoziomową manipulację na poziomie wyliczaniu liczby pikseli.

Skutek dla mnie jest taki, że rozwiązania mojego problemu do dziś nie znam, gdyż otwarcie kodu JTable było dla mnie jak uderzenie obuchem w głowę. Uznałem, że e nie mam tam czego szukać i pozostało mi żyć z poczuciem, że pokonano mnie argumentem siły…

;'-( ' '

(A gdyby kogoś to interesowało, mój problem polega na tym, że gdy chciałem by kolejne komórki tej samej kolumny tabeli otrzymywały losowe kolory, to zarówno korzystając z domyślnie generowanego renderera komórek, jak i tworząc własny, który rozszerzał DefaultTableCellRenderer i tak zawsze wszystkie dostawały kolor ten sam – pierwszy spośród wszystkich wylosowanych. Magia polega na tym, że jeśli renderer został stworzony przy użyciu metody JTable#getDefaultRenderer(Class<?> columnClass) wszystko zaczynało magicznie działać tak, jak tego chciałem.)

Wciąż rozwijana biblioteka

Pomimo wszystkich cool story, z którymi się tutaj z wami podzieliłem mógłbym ten wpis zakończyć jakimś pozytywnym akcentem, np. opowieścią, że z nadzieją patrzę w przyszłość wiedząc i widząc, że Swing jest wciąż kochany, rozwijany i powszechnie używany. Wolę jednak po prostu sprawiedliwie tę bibliotekę ocenić.

Miłość. Z „miłością” do Swinga bywa różnie. Pod wieloma względami podoba mi się on bardziej niż np. Androidowe API (mimo, że jest to znacznie nowocześniejsze narzędzie, nie miałem poczucia, by twórcy podstawowych komponentów androidowych słyszeli kiedykolwiek o jakiejkolwiek wersji MVC, czystym kodzie i podobnych rzeczach). Nie być najgorszym w swojej klasie, to jednak zdecydowanie za mało na jakieś cieplejsze uczucia.

Rozwój. Rozwój Swinga, owszem, do niedawna był jeszcze widoczny (ostatnia jakaś istotniejsza zmiana to chyba czasy Javy 7). Sęk w tym, że mamy do czynienia z czasem przeszłym. Ponadto czasem wręcz chciałoby się krzyknąć CZEMU TAK PÓŹNO?!. Prosty przykład to „generyki”. Ogólnie w Javie zawitały już w wersji 5. A w Swingu? Ktoś się obudził i nagle stwierdził, że świetnym pomysłem byłoby również w tej bibliotece skorzystać z takich dobrodziejstw. Stało się tak dwa wydania później… w bardzo ograniczonej formie.

Innym przykładem niech będą propozycje nowości z czasów Javy 6. Programiści Swinga wpadli na kilka mniej-lub-bardziej fajnych pomysłów i wrzucili je do osobnej biblioteki SwingX. Miały one stać się standardem w następnej wersji Javy. Udało się? Niestety tylko część propozycji została przyjęta. Reszta żyje dalej w SwingX, który obecnie nie posiada nawet strony domowej…

Powszechne użycie? Nie muszę chyba pisać, że w kontekście powyższych wniosków nie sposób traktować powszechnego użytku tej biblioteki jako coś pozytywnego, prawda?

PS

Mimo takiej, a nie innej mojej opinii o Swingu, traktuję doświadczenie nabyte w pracy z tą biblioteką jako coś bardzo cennego. Uważam, że jeśli nie dostajemy narzędzi, które lubimy, warto spróbować polubić, te które dostaliśmy. Zmienić można podejście lub pracę, bo narzędzia zmieniają się najwolniej. Niech to będzie moja pozytywna myśl na koniec tego wpisu :-) . 

programowanie

Komentarze