Podejście zwinne a budowa aplikacji, czyli jak to wszystko zaplanować?

Metody zwinne w wytwarzaniu oprogramowania stały się tak popularne i wszechobecne, że nie będzie nadużyciem, jeśli stwierdzę, że prawie wszystkie organizacje pracują zwinnie, „w agile-u”. Jednak gdy przyjrzeć się dokładniej, okazuje się, że sprawy nie mają się tak kolorowo. Ciągle można zauważyć tendencję do planowania wszystkiego z góry. Nic dziwnego, skoro przez lata organizacje pracowały w ten sposób; zmiana sposobu myślenia nie dzieje się ot tak, z dnia na dzień. Ta zmiana wymaga olbrzymiej pracy i determinacji na wszystkich poziomach. Bez względu na to, czy mówimy o developerach, testerach, architektach czy osobach z szeroko pojętego biznesu.

Co robić?

Dosyć często szukamy winy u innych – to niestety nie jest najproduktywniejszy sposób rozwiązywania problemów. Jeżeli „agile” nie wychodzi, to może warto zastanowić się, co JA robię nie tak, co JA mogę zmienić w swojej pracy. Będąc programistą, developerem, popełniłem wiele grzechów – albo inaczej: mogłem wiele rzeczy zrobić lepiej.

Architektura

Zacznijmy z przysłowiowej grubej rury. Nie raz i nie dwa razy zdarzyło mi się zastanawiać jak zaprojektować architekturę aplikacji tak aby była wspaniała, doskonała i spełniała wszystkie możliwe (w owym czasie) trendy. Nie robiłem tego ze względu na CDD (CV-Driven Development) – chodziło o to, aby uniknąć problemów, z jakimi spotkałem się wcześniej. Tym razem zrobię to lepiej, tym razem będzie doskonale – znasz to skądś?

Stworzenie doskonałej architektury, która będzie idealnie odpowiadała potrzebom naszego projektu nie jest trywialne. Najprostszy sposób to napisać aplikację, a potem napisać jej wersję drugą, trzecią i czwartą – każdą lepszą od poprzedniej. Niestety, mało kto ma komfort przepisywania jednej i tej samej aplikacji x-razy. Co zatem można zrobić zamiast tego (i zarazem lepiej)? Okazuje się, że lepiej sprawdza się ciągłe dostosowywanie architektury aplikacji do zmieniających się potrzeb klienta oraz nowych wymagań.

Wyobraźmy sobie, że naszym klientem jest biblioteka. Pierwsze wymaganie to prezentacja listy książek, które są na stanie. Czego tak naprawdę potrzebujemy? Lista oraz sposób dodawania elementów (książek) do listy. Ile czasu jest potrzebne, aby coś takiego napisać? Pewnie niezbyt dużo. Ale czy nie korci Cię, aby od razu dodać możliwość usuwania, modyfikowania, no i oczywiście generowanie kodów kreskowych, żeby wkleić je do książki i umożliwić szybkie skanowanie…. i na pewno wyobraźnia podpowiada Ci setkę innych rzeczy, które należy dodać.

A jeśli naprawdę nasza biblioteka nie potrzebuje nic więcej? Jeśli ma 1000 oddziałów? A może ma być tylko online? A może wszystko będzie działało tylko na jednym komputerze sprzed 15 lat? Czy naprawdę znasz odpowiedzi na wszystkie te pytania? Ja ich nie znam, ale nieraz dopowiadałem sobie czego jeszcze klient potrzebuje. Potem często się okazywało, że te wyimaginowane „potrzeby” były do wyrzucenia lub co gorsze zostawały w kodzie, ale trzeba je było utrzymywać.

Architektura jest jak kurs dla jachtu. Wyznacza pewien kierunek. Drivery architektoniczne wskazują cel, do którego chcemy dotrzeć. Nie oznacza to, że wypływając z portu mamy zaplanowane wszystkie zwroty, wszystkie ustawienia żagli i do tego horyzont czasowy. Te same zasady powinny przyświecać podczas budowania aplikacji. Reagujemy na bieżące zmiany, bieżące potrzeby, bieżące burze i zawirowania, aby posuwać się do przodu w wyznaczonym (mniej więcej) kierunku. Wypisz wymaluj czwarte zdanie z Agile Manifesto:

„Responding to change over following a plan”

Architektura architekturą, ale ktoś musi napisać kod

Są takie organizacje, gdzie funkcjonuje dział architektów, którzy tworzą najlepiej jak potrafią, najlepszą architekturę, która ma sprostać wszystkim wymaganiom. Jeżeli ten dział nie współpracuje z osobami tworzącymi kod to… no cóż, pojawia się brak zrozumienia oraz konflikty. Co więcej, tak powstała architektura nie jest architekturą a jedynie projektem. Architekturą jest to co zostało wykute w kodzie. Jeśli rozdzielimy te dwie rzeczy, jeśli architektura powstaje tylko na papierze, wówczas pozostaje projektem lub marzeniem. Jeśli zaś kod powstaje bez architektury, niejako samorzutnie, zazwyczaj przypomina spaghetti. Ani jedno, ani drugie nie jest czymś czego oczekują klienci. Klienci chcą:

„Działające oprogramowanie od szczegółowej dokumentacji”

Aby tego dokonać architekci i programiści muszą współpracować. Potrzebujemy w zespole osoby, która raz za jakiś czas usiądzie i zastanowi się, czy obecna architektura jeszcze spełnia bieżące wymagania, czy już pora na refaktoryzację. I zwróć uwagę: refaktoryzację a nie rewolucję.

Refaktoryzacja

Dlaczego refaktoryzacja jest taka ważna? Wiadomym jest, że klienci będą mieli nowe potrzeby i nowe wymagania. Gdy tylko nasze oprogramowanie rozwiąże ich problemy, zaraz pojawią się nowe potrzeby. Wraz z nowymi potrzebami może okazać się, że należy trochę przebudować nasz kod. Jeśli robimy to regularnie, jeśli regularnie myślimy o refaktoryzacji, wówczas poprawiamy w takiej sytuacji tylko to, co poprawione być musi, zamiast przebudowywać całą aplikację. Jeżeli nasz kod bardziej przypomina pudełko klocków Lego (low coupling) niż talerz spaghetti (hight coupling) to jesteśmy w stanie przemodelować środek aplikacji bez wielomiesięcznej rewolucji.

Eee… tam, to tylko teoria!

No nie, to nie jest tylko teoria, to jest długoterminowy plan i codzienna solidna praca nad kodem. Rozmawiałem kiedyś z człowiekiem, który zjadł zęby na Event Sourcingu i to z niemałymi sukcesami. Wiesz jaki był jego przepis?

  1. Tworzymy aplikację CRUD. Tak zwykły CRUD – „nothing fancy”. Dlaczego tak? Bo tworząc CRUD-a jesteśmy w stanie bardzo szybko dostarczyć klientowi podstawową funkcjonalność, to po pierwsze. A po drugie poznajemy potrzeby i domenę.
  2. Po kilku tygodniach/miesiącach zaczynamy publikować eventy. Dlaczego tak? Bo po tych kilku tygodniach/miesiącach znamy już domenę na tyle dobrze, że jesteśmy w stanie sensownie zidentyfikować zdarzenia domenowe. Co ciekawe, na początku tylko publikujemy eventy – nie konsumujemy ich. Taka zmiana dla klienta jest praktycznie niewidoczna.
  3. Po dalszych kilku tygodniach/miesiącach zaczynamy konsumować eventy. Dopiero tutaj z wprowadzonych wcześniej eventów pojawia się (dodatkowa i duża) wartość dla klienta. Wartość dodatkowa, bo już na samym początku dostarczamy wartość zwykłym CRUD-em. Teraz jednak pojawia się wielkie WOW! Można dostarczyć klientowi informacje o danych wstecz. Dlaczego dopiero teraz? Bo z każdą linijką kodu, poznajemy coraz więcej szczegółów i zawiłości problemów klienta, a im więcej wiemy, tym trafniejsze decyzje możemy podejmować.

Pamiętasz jeszcze to co pisałem o bibliotece? Jaką najlepiej użyć bazę danych? Na studiach pewnie była relacyjna – ale która? Masz wybraną jedną, jedyną słuszną? Jak się zmieni ten wybór, jeśli odkryjemy, że mamy obsługiwać 50% bibliotek na świecie? Jak się zmieni wybór, jeśli odkryjemy, że te 50% bibliotek to specjalistyczne wewnętrzne biblioteki w szpitalach? W końcu jak się zmieni wybór, jeśli okaże się, że ze względu na RODO, dane nie mogą wypłynąć poza szpital? Im więcej wiemy, tym lepsze decyzje możemy podejmować. Na początku projektu nie mamy takiego komfortu jak na końcu.

Ale jeśli dobrze zbierzemy wszystkie wymagania na początku…

No to będzie łatwiej, tylko czy to jest w ogóle możliwe? Będąc młodym developerem, miałem nadzieję, że tak. Wyciągnę wnioski z poprzednich projektów, przetrawię to, przeanalizuję i teraz będzie lepiej. Jednak problemy zasadniczo są dwa.

  1. Klient nie powie nam o wszystkim, bo pewne rzeczy będą dla niego oczywiste i w jego odczuciu nie będą wymagały dyskusji. Klasyczny przykład to systemy do wystawiania faktur z lat około ‘90. W sumie temat do „obgonienia” w Excelu. Jak zapytamy klienta czy jest taka faktura to powie nam, że dokument obejmuje: odbiorcę, wystawcę, numer, datę, pozycje… tutaj suma, tam mnożenie. Żadna technologia kosmiczna. Ale jak wejdziemy w szczegóły to okaże się, że mamy proformy, korekty, faktury zagraniczne, zagraniczne wewnątrzwspólnotowe, z odwrotnym obciążeniem i jeszcze pewnie wiele innych typów. Klient o wielu pewnie nam nie powie, bo dla niego to może być oczywiste.
  2. Jeśli dostarczymy klientowi dobry, działający software – taki, który spełnia jego potrzeby i rozwiązuje jego problemy – to pewnie wkrótce zaczną się pojawiać nowe potrzeby. Prosta sprawa: jeśli już zbudowaliśmy ten system do faktur, to może by tak zrobić mailing na koniec roku do naszych kontrahentów? Jak mamy mailing, to może jakiś system bonusów dla naszych najwierniejszych klientów? Wraz z jedzeniem apetyt rośnie więc i wymagań będzie przybywać.

„Bądźcie gotowi na zmiany wymagań nawet na późnym etapie jego rozwoju. Procesy zwinne wykorzystują zmiany dla zapewnienia klientowi konkurencyjności.”

Źródło: „Założenia Manifestu Programowania Zwinnego”

Ale nam się wszystko wali

Już wiemy, że mamy stały i nieunikniony strumień wymagań. Wiemy, że nie zaprojektujemy „über-architektury”, która wszystko zniesie – tylko będziemy musieli ciągle (continuously) refaktoryzować i dostosowywać się do zmian. Jak w ogóle zapanować nad tym „chaosem”? Odpowiedź znajdziemy na drugiej stronie Agile Manifesto – w 12 zasadach wspierających rozwój oprogramowania.

„Ciągłe skupienie na technicznej doskonałości i dobrym projektowaniu zwiększa zwinność.”

Tworząc linijkę kodu tworzymy ją najlepiej jak potrafimy. Pisząc kawałek kodu z przysłowiowego „buta” pamiętać należy, że to my sami będziemy ten kod utrzymywać, więc pewnie i sami się potkniemy o niego. Nie wierzysz? Spróbuj w domu codziennie zjeść trzy banany, a skórki rzucaj na podłogę. Jak myślisz, po jakim czasie przejedziesz się na jednej z nich?

„Najlepsze rozwiązania architektoniczne, wymagania i projekty pochodzą od samoorganizujących się zespołów.”

Słowo klucz to ZESPÓŁ!!! Nie w pojedynkę, nie każdy osobno na swoim piętrze, ale razem, zespołowo. Łącząc różne kompetencje jesteśmy wstanie budować lepsze produkty, które lepiej odpowiadają na potrzeby naszych klientów.

„W regularnych odstępach czasu zespół analizuje możliwości poprawy swojej wydajności, a następnie dostraja i dostosowuje swoje działania do wyciągniętych wniosków.”

Regularna samokontrola to podstawa, tak powie Ci każdy lekarz. Nie inaczej sprawy się mają z budowaniem produktów. Na co dzień zakaszemy rękawy i tworzymy kod, testujemy, wdrażamy, naprawiamy, piszemy SQL-e i JSON-y, docker-file i YAML-e, praca wre. Ale regularnie powinniśmy się zatrzymać i sprawdzić, czy zmierzamy w dobrym kierunku. Czy ten wspaniały framework, który przywieźliśmy z konferencji nas spowalnia czy przyspiesza. Raz w tygodniu zatrzymaj i zastanów się, czy nie biegasz tak szybko z taczkami, że nie masz czasu ich załadować. A może nie taczki, tylko wiadro będzie lepsze? Refleksja nad tym co i jak robimy jest najlepszą praktyką, jaką można sobie wyobrazić.

Jak zarządzać dużą ilością testów automatycznych?

testy automatyczne

Testy automatyczne – jeszcze kilka lat temu coś bardzo egzotycznego i mało spotykanego – bo przecież można zatrudnić skończoną ilość studentów do przeklikiwania. Chyba wielu słyszało ten idiotyczny tekst. Rynek jednak powoli się zmienia. Obecnie okazuje się, że przysłowiowych klikaczy nie ma już tak wielu. Zresztą, zatrudnianie klikaczy do testowania aplikacji od A do Z jest skrajnie nieefektywne kosztowo. Taka osoba może klikać 8h dziennie, jednak po kilku iteracjach klikania zacznie być coraz mniej efektywna, przyjdzie zarówno zmęczenie jak i znużenie. Efektywność w wyłapywaniu błędów może znacząco spaść.

Testy automatyczne

Testy automatyczne to naturalny krok w rozwoju organizacji. Dosyć szybko można zauważyć, że niektóre testy można zautomatyzować. Ma to tę zaletę, że odciążamy człowieka, który teraz może bardziej skupić się na pracy testera, czyli na opracowywaniu scenariuszy testowych, a nie na bezmyślnym klikaniu wg. ustalonego scenariusza. Może zająć się czymś bardziej kreatywnym. Super…

…ale

…ale nie ma róży bez kolców. Test automatyczny działa bardzo zero-jedynkowo, albo przechodzi albo nie. Jeśli testy automatyczne są napisane prawidłowo – czyli nie są to tzw. flaky tests, które raz przechodzą a raz nie – to powinny zacząć nie przechodzić w momencie pojawienia się błędu w aplikacji lub w przypadku zmiany logiki aplikacji.

Jeśli pojawił się błąd w aplikacji – należy poprawić aplikację, jeśli natomiast zmieniła się logika aplikacji to trzeba dostosować test automatyczny do nowych wymagań. Jest jednak jeszcze jedna zmiana, która nie jest ani błędem ani zmianą w logice aplikacji, a też może spowodować, że nasze testy automatyczne przestaną działać.

Testy automatyczne a zmiana interfejsu użytkownika

No właśnie, zmiana w interfejsie użytkownika. Trochę inne kolorki, trochę inny układ przycisków, drobne poprawki UI/UX i już nasze testy leżą.

Z każdym jednym testem automatycznym, który dodajemy do naszego rozwiązania dodajemy kolejny element, który trzeba utrzymywać. Przy 2-5-10 testach automatycznych to nie jest wiele pracy, ale co jeśli tych testów będzie 100-1000? Wtedy zaczyna to być znaczący koszt. Czasem pojawia się pomysł, aby zaprzestać używać testów automatycznych, bo to kosztuje zbyt dużo. Decyzja taka jednak jest jak wylanie dziecka z kąpielą. Rozwiązuje problem, ale może niekoniecznie w sposób pożądany. Albo dosadniej, na ból głowy lepiej wziąć lekarstwo niż zaaplikować gilotynę. Niby oba sposoby działają, ale ten pierwszy jest jednak bardziej preferowany.

Ok, to jak poradzić sobie z dużą ilością testów automatycznych? Tak, aby się dało to utrzymywać przy rozsądnym koszcie? Rozwiązaniem jest Page Object Pattern. Wzorzec bardzo prosty w swoim założeniu i genialnie efektywny, jeśli stosowany poprawnie. Jeśli chcesz wiedzieć więcej na temat tego wzorca, to zapraszam Cię na zupełnie DARMOWY webinar na temat Page Object Pattern.

Jest tylko jeden haczyk. Trzeba się spieszyć z rejestracją, bo czasu nie zostało wiele.

REJESTRACJA

BBQ4IT czyli najlepsza konferencja na Podbeskidziu – Call For Papers

BBQ4IT jako pomysł pojawił się dwa lata temu. Od pomysłu do czynu i tak powstała konferencja (więcej o BBQ4.IT możesz przeczytać tutaj). W tym roku odbędzie się kolejna edycja tej zacnej inicjatywy. Prace nad nią już trwają. W związku z tym mamy dla Ciebie propozycję. Jeśli chcesz współtworzyć tą konferencję z nami to zgłoś swojego talka na BBQ4IT już dzisiaj. Scenę będziesz dzielić z dwiema gwiazdami polskiej sceny IT, które już potwierdziły swój udział… (ale na razie ciiiisza, nie zdradzamy kto to) więc, żeby potem nie było płaczu, że mogłam/mogłem wystąpić na scenie obok ….. i ……. 🙂 siadaj do tematu i wysyłaj na:

http://bbq4.it/c4p

Raspberry pi zero w +WiFi + ssh czyli jak ustawić nie mając odpowiedniej przejściówki

Uzupełniając vloga nr 16 – gdzie mówiłem o WiFi w Raspberry Pi zero W pora napisać jak skonfigurować WiFi i ssh jeśli nie mamy przejściówki MikroUSB <-> gniazdo USB. Zamiast kupować, szukać wystarczy:

Uruchamiamy SSH:

na karcie SD założyć plik o nazwie ssh i dowolnej treści. Możemy z poziomu dowolnego systemu operacyjnego to zrobić wkładając kartę SD do czytnika i zakładając plik. Treść nie ma zupełnie znaczenia. Znaczenie ma nazwa tego pliku. Przy starcie raspberry pi sprawdzi czy plik ssh istnieje i jeśli istnieje to się odpowiednio skonfiguruje. Prosta sprawa.

Uruchamiamy WiFi:

Z WiFi sprawa jest trochę bardziej skomplikowana. Musimy sięgnąć do bebechów na karcie. Dokładniej do pliku /etc/wpa_supplicant/wpa_supplicant.conf. Jeśli masz linuxa lub maca sprawa jest prosta. W przypadku Windowsa jest trudniej ponieważ Windows natywnie nie chce czytać systemu plików Linuxa, co więcej nawet nie widzi głównej części karty gdzie znajdują się wszystkie dane. Sprawa jednak jest prosta. Użyjmy linuxa. Najszybiej będzie zainstalować po prostu maszynę wirtualną – jeśli nie wiesz co to jest maszyna wirtualna to na początek najbardziej bezbolesny będzie Virtual Box. W Virtual Boxie zakładamy nową maszynę i jako nośnik CD wrzucamy ISO choćby pobrane ze strony Ubuntu (oczywiście może to być dowolny linux ale ubuntu wiem, że zadziała z Virtual Boxem i jest sporo materiałów o ubuntu gdyby chcieć wejść bardziej w świat linuksów). Po tej operacji będziemy mieli linuxa pod ręką.

Pozostaje jedynie udostępnić kartę SD maszynie (Menu-> Urządzenia -> USB -> Twój czytnik kart SD) i odpowiednio ustawić plik /etc/wpa_supplicant/wpa_supplicant.conf a odpowiednio to bardzo prosto:

[javascript]
network={
ssid="nazwa_wifi"
psk="haslo_wifi"
}
[/javascript]

Tylko tyle i aż tyle. Po uruchomieniu maliny z tak zmienioną kartą malinka połączy się do naszego WiFi

Uruchomiło się i co dalej

Jeśli malinka się uruchomiła to pozostaje ją znaleźć w swojej sieci. Najłatwiej będzie zaglądnąć do routera i do klientów DHCP. Mając PI malinki lecimy już z górki:

ssh 192.168.x.x -l pi 

Podać hasło (defaultowe jest bodajże raspberry) i jesteśmy w środku. Stąd mamy klika możliwości. Albo uruchamiamy vnc (jak we vlogu) albo ustawiamy statyczne IP, żeby nie trzeba było szukać maliny po sieci albo aktualizyjemy albo robimy cokolwiek innego chcemy od naszej malinki 🙂

Unknown error: cannot get automation extension

Jakiś czas temu znalazłem taki błąd w logach z uruchomienia paczki testów selenium.

Unknown error: cannot get automation extension

A w konsoli jedynie:

Po krótkich poszukiwaniach okazało się, że linijka która powodowała to zachowanie to:

[csharp]
webDriver.Manage().Window.Maximize();
[/csharp]

No i teraz mamy dwie opcjie, pierwsza to najbardziej oczywista – aktualizacja ChromeDriver-a z wersji 2.24 (na której to się pokazało) do najnowszej – aktualnie 2.29. Albo jeśli z jakiś powodów nie możemy/nie chcemy zaktualizowac ChromeDrivera to można inaczej zmaksymalizować okno:

[csharp]

var options = new ChromeOptions();
options.AddArgument("start-maximized");
_webDriver = new ChromeDriver(Configuration.WebDriversPath, options);

&nbsp;

[/csharp]

Więc jak trafisz taki błąd, to może właśnie masz tutaj rozwiązanie 🙂 Powodzenia

Moje narzędzia do obsługi repozytorium kodu

Serii o podstawach ciąg dalszy. W ostatnim wpisie pisałem o najważniejszym artefakcie czyli o historii produktu zawartej w repozytorium kodu. Teraz pora zobaczyć na narzędzia. Bez odpowiednich* narzędzi praca jest po prostu mozolna i nieefektywna. Ponieważ aktualnie moim jedynym repozytorium kodu jakiego używam to git (zarówno w pracy jak i podczas przeprowadzanych szkoleń) to przedstawione poniżej narzędzia służą do pracy z gitem właśnie ale pewnie część z nich będzie pracować z innymi repozytoriami – sprawdź jeśli jeszcze nie używasz gita.

Konsola jako podstawowy klient do repozytorium kodu

Cmder to jedna z najlepszych konsoli na Windowsa czyli Conemu ale ubrana w customizacje dzięki czemu jest ładna i mocarna. W kontekście obsługi repozytorium kodu… posiada wbudowanego klienta git. Pobierasz cmder-a, rozpakowywujesz i tyle, masz i konsolę i gita. Nic więcej nie trzeba instalować, kombinować.

 

cmder jako najlepsze podstawowe narzędzie do obsługi gita

 

Plus jest taki, ze na bieżąco widzimy na jakiej gałęzi jesteśmy oraz czy mamy jakieś zmiany (wówczas napis master na rycinie powyżej byłby czerwony). Mamy obsługę powershella, posh gita i wielu wielu więcej ale dla mnie osobiście jedna z najlepszych rzeczy to gl czyli przegląd historii – z kolorkami i w ogóle.

przeglądanie historii w gicie

 

IDE musi rozumieć kontekst repozytorium kodu

Nie wyobrażam sobie pracy w IDE bez obsługi repozytorium kodu. Na szczęście wszystkie 3 narzędzia (Visual Studio, Visual Studio Code, InteliJ – Webstorm, Rider, PyCharm),  których używam mają to wbudowane. Ponieważ większość czasu spędzam w Visual Studio to lista moich priorytetów:

  1. chcę widzieć w jakim stanie są pliki,
  2. chcę mieć podgląd zmian od ostatniego commitu pod ręką,
  3. chcę mieć annotate/blame pod ręką,
  4. chcę mieć pod ręką możliwość powrotu do działającego kodu.

 

Super proste i super wygodne przeglądanie historii

Tak jak wcześniej pisałem historia to jedna z najważniejszych artefaktów produkcji oprogramowania. Zatem jak ją łatwo przeglądać? Niby wszystkie narzędzia coś tam mają ale dla mnie jest jedno super świetne narzędzie, które po pierwsze robi to co ma robić świetnie a po drugie jest pod ręką – w moim IDE z którego nie chcę wychodzić jak nie ma takiej potrzeby.

 

To narzędzie to CodeLineage i jak dla mnie to jest mistrz w przeglądaniu historii. Łatwo mogę wybrać dwa dowolne punkty historii i oglądać co się zmieniło. Szybko, wygodnie i w punkt.

Podsumowanie (TL;DR;)

Trzy dla mnie podstawowe narzędzia to Cmder, Visual Studio i CodeLineage. Rzeczy, których nie potrafię zrobić z gitem z poziomu konsoli robię z poziomu SourceTree.

 

*) dla każdego odpowiednie narzędzie oznacza coś innego (ale wiadomo, że pewne narzędzia są jedyne słuszne 😉 )