Jak uniknąć kaskadowego refaktoryzacji?


52

Mam projekt. W tym projekcie chciałem przefaktoryzować go, aby dodać funkcję, i przebudowałem projekt, aby dodać funkcję.

Problem polega na tym, że kiedy skończyłem, okazało się, że muszę wprowadzić niewielką zmianę interfejsu, aby to uwzględnić. Więc dokonałem zmiany. I wtedy klasa konsumująca nie może zostać zaimplementowana z obecnym interfejsem pod względem nowego, więc potrzebuje również nowego interfejsu. Teraz minęły trzy miesiące i musiałem naprawić niezliczone, praktycznie niezwiązane ze sobą problemy, i patrzę na rozwiązywanie problemów, które zostały zaplanowane na rok od teraz lub po prostu wymienione jako nie naprawione z powodu trudności, zanim rzecz się skompiluje jeszcze raz.

Jak mogę uniknąć tego rodzaju refaktoryzacji kaskadowej w przyszłości? Czy to tylko symptom moich poprzednich zajęć, które zbyt mocno od siebie zależą?

Krótka edycja: w tym przypadku refaktorem była cecha, ponieważ refaktor zwiększył rozszerzalność określonego fragmentu kodu i zmniejszył pewne sprzężenie. Oznaczało to, że zewnętrzni programiści mogli zrobić więcej, co było funkcją, którą chciałem dostarczyć. Tak więc sam pierwotny refaktor nie powinien być zmianą funkcjonalną.

Większa edycja, którą obiecałem pięć dni temu:

Zanim zacząłem ten refaktor, miałem system, w którym miałem interfejs, ale w implementacji po prostu dynamic_castprzechodziłem przez wszystkie możliwe implementacje, które wysłałem. To oczywiście oznaczało, że nie można po prostu odziedziczyć po interfejsie, po drugie, a po drugie, nie byłoby możliwe, aby ktokolwiek bez dostępu do implementacji implementował ten interfejs. Zdecydowałem więc, że chcę rozwiązać ten problem i otworzyć interfejs do publicznego użytku, aby każdy mógł go wdrożyć, a wdrożenie interfejsu było wymaganiem całej umowy - oczywiście poprawa.

Kiedy znajdowałem i zabijałem ogniem wszystkie miejsca, które to zrobiłem, znalazłem jedno miejsce, które okazało się być szczególnym problemem. Zależało to od szczegółów implementacji wszystkich różnych klas pochodnych i zduplikowanych funkcji, które zostały już zaimplementowane, ale lepiej gdzie indziej. Zamiast tego mógł zostać zaimplementowany w postaci interfejsu publicznego i ponownie wykorzystać istniejącą implementację tej funkcjonalności. Odkryłem, że do poprawnego działania wymagał określonego kontekstu. Z grubsza mówiąc, wywołanie poprzedniej implementacji wyglądało trochę jak

for(auto&& a : as) {
     f(a);
}

Jednak, aby uzyskać ten kontekst, musiałem zmienić go na coś bardziej podobnego

std::vector<Context> contexts;
for(auto&& a : as)
    contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
    f(con);

Oznacza to, że dla wszystkich operacji, które kiedyś były częścią f, niektóre z nich muszą stać się częścią nowej funkcji, gktóra działa bez kontekstu, a niektóre z nich muszą być częścią części odroczonej f. Ale nie wszystkie metody fnazywają potrzebę lub chcą tego kontekstu - niektóre z nich potrzebują odrębnego kontekstu, który uzyskują osobnymi środkami. Więc dla wszystkiego, co fkończy się dzwonieniem (czyli, mówiąc z grubsza, prawie wszystko ), musiałem ustalić, jaki, jeśli w ogóle, potrzebny im kontekst, skąd go wziąć i jak podzielić je ze starego fna nowe fi nowe g.

I tak skończyłem tam, gdzie teraz jestem. Jedynym powodem, dla którego kontynuowałem, jest to, że i tak potrzebowałem tego refaktoryzacji z innych powodów.


67
Kiedy mówisz, że „przeprojektowałeś projekt, aby dodać funkcję”, co dokładnie masz na myśli? Refaktoryzacja z definicji nie zmienia zachowania programów, co powoduje, że to stwierdzenie jest mylące.
Jules

5
@Jules: Ściśle mówiąc, funkcja ta pozwoliła innym programistom na dodanie określonego rodzaju rozszerzenia, więc funkcja była refaktorem, dzięki czemu struktura klas była bardziej otwarta.
DeadMG,

5
Myślałem, że jest to omówione w każdej książce i artykule, który mówi o refaktoryzacji? Kontrola źródła przychodzi na ratunek; jeśli stwierdzisz, że aby wykonać krok A, musisz najpierw wykonać krok B, a następnie złomować A i najpierw wykonać B.
rwong

4
@DeadMG: to jest książka, którą pierwotnie chciałem zacytować w moim pierwszym komentarzu: „Gra„ pick-up stick ”jest dobrą metaforą metody Mikado. Eliminujesz„ dług techniczny ”- problemy ze starszymi programami wbudowane w prawie każde oprogramowanie system - postępując zgodnie z zestawem łatwych do wdrożenia reguł. Ostrożnie wyodrębniasz każdą powiązaną zależność, aż ujawnisz główny problem, bez zwijania projektu. ”
rwong

2
Czy możesz wyjaśnić, o którym języku programowania mówimy? Po przeczytaniu wszystkich twoich komentarzy doszedłem do wniosku, że robisz to ręcznie zamiast korzystać z IDE, aby ci pomóc. Chciałbym zatem wiedzieć, czy mogę udzielić praktycznej porady.
pakujący

Odpowiedzi:


69

Ostatnim razem, gdy próbowałem rozpocząć refaktoryzację z nieprzewidzianymi konsekwencjami, i nie mogłem ustabilizować kompilacji i / lub testów po jednym dniu , poddałem się i przywróciłem bazę kodu do punktu przed refaktoryzacją.

Następnie zacząłem analizować, co poszło nie tak, i opracowałem lepszy plan, jak wykonać refaktoryzację w mniejszych krokach. Więc moja rada dotycząca unikania refaktoryzacji kaskadowej jest po prostu: wiedz, kiedy przestać , nie pozwól, aby sprawy wymknęły się spod kontroli!

Czasami musisz ugryźć kulę i odrzucić cały dzień pracy - zdecydowanie łatwiej niż wyrzucić trzy miesiące pracy. Dzień, w którym tracisz, nie jest całkowicie próżny, przynajmniej nauczyłeś się, jak nie podchodzić do problemu. I z mojego doświadczenia wynika, że zawsze można zrobić mniejsze kroki w refaktoryzacji.

Uwaga dodatkowa : wydaje się, że jesteś w sytuacji, w której musisz zdecydować, czy chcesz poświęcić pełne trzy miesiące pracy i zacząć od nowa z nowym (i mam nadzieję, że bardziej udanym) planem refaktoryzacji. Mogę sobie wyobrazić, że nie jest to łatwa decyzja, ale zadaj sobie pytanie, jak wysokie jest ryzyko, że potrzebujesz kolejnych trzech miesięcy nie tylko w celu ustabilizowania kompilacji, ale także w celu usunięcia wszystkich nieprzewidzianych błędów, które prawdopodobnie wprowadziłeś podczas przepisywania, które zrobiłeś w ciągu ostatnich trzech miesięcy ? Napisałem „przepisz”, ponieważ myślę, że tak właśnie zrobiłeś, a nie „refaktoryzacja”. Nie jest wykluczone, że możesz szybciej rozwiązać obecny problem, wracając do ostatniej wersji, w której projekt się kompiluje, i zacznij od prawdziwego refaktoryzacji (w przeciwieństwie do „przepisywania”) ponownie.


53

Czy to tylko symptom moich poprzednich zajęć, które zbyt mocno od siebie zależą?

Pewnie. Jedna zmiana powodująca mnóstwo innych zmian jest właściwie definicją sprzężenia.

Jak uniknąć kaskadowych reaktorów?

W najgorszym rodzaju baz kodowych jedna zmiana będzie się kaskadować, ostatecznie powodując zmianę (prawie) wszystkiego. Częścią każdego refaktora, w którym występuje powszechne połączenie, jest izolowanie części, nad którą pracujesz. Konieczne jest dokonanie refaktoryzacji nie tylko w miejscu, w którym nowa funkcja dotyka tego kodu, ale w miejscu, gdzie wszystko inne dotyka tego kodu.

Zwykle oznacza to, że niektóre adaptery pomagają staremu kodowi pracować z czymś, co wygląda i działa jak stary kod, ale korzysta z nowej implementacji / interfejsu. W końcu jeśli wszystko, co robisz, to zmieniasz interfejs / implementację, ale zostawiasz połączenie, nic nie zyskujesz. To szminka na świni.


33
+1 Im bardziej potrzebne jest refaktoryzacja, tym szerzej będzie ona osiągać. Taka jest natura tej rzeczy.
Paul Draper,

4
Jeśli jednak naprawdę dokonujesz refaktoryzacji , inny kod nie powinien od razu zajmować się zmianami. (Oczywiście, że ostatecznie będziesz chciał wyczyścić inne części ... ale nie powinno to być natychmiast wymagane). Zmiana, która „kaskaduje” w pozostałej części aplikacji, jest większa niż refaktoryzacja - w tym momencie jest to w zasadzie przeprojektowanie lub przepisanie.
cHao

+1 Adapter to dokładnie sposób na wyizolowanie kodu, który chcesz zmienić jako pierwszy.
winkbrace,

17

Wygląda na to, że refaktoryzacja była zbyt ambitna. Refaktoryzacja powinna być wykonywana małymi krokami, z których każdy może zostać ukończony w (powiedzmy) 30 minut - lub, w najgorszym przypadku, co najwyżej dziennie - i pozostawia projekt do zbudowania, a wszystkie testy wciąż trwają.

Jeśli minimalizujesz każdą indywidualną zmianę, naprawdę nie powinno być możliwe, aby refaktoryzacja zepsuła twoją kompilację na długi czas. Najgorszym przypadkiem jest prawdopodobnie zmiana parametrów na metodę w powszechnie używanym interfejsie, np. W celu dodania nowego parametru. Ale wynikające z tego zmiany są mechaniczne: dodanie (i zignorowanie) parametru w każdej implementacji oraz dodanie wartości domyślnej w każdym wywołaniu. Nawet jeśli istnieją setki referencji, wykonanie takiego refaktoryzacji nie powinno zająć nawet jednego dnia.


4
Nie rozumiem, jak taka sytuacja może się pojawić. Dla każdego uzasadnionego refaktoryzacji interfejsu metody musi istnieć łatwy do ustalenia nowy zestaw parametrów, który można przekazać, co spowoduje, że zachowanie wywołania będzie takie samo jak przed zmianą.
Jules

3
Nigdy nie byłem w sytuacji, w której chciałem przeprowadzić takie refaktoryzację, ale muszę powiedzieć, że brzmi to dla mnie dość nietypowo. Czy mówisz, że usunąłeś funkcjonalność z interfejsu? Jeśli tak, to gdzie to poszło? W inny interfejs? Lub gdzie indziej?
Jules

5
Następnie sposobem na to jest usunięcie wszystkich zastosowań funkcji do usunięcia przed refaktoryzacją w celu jej usunięcia, a nie później. Pozwala to zachować budowanie kodu podczas pracy nad nim.
Jules

11
@DeadMG: to brzmi dziwnie: usuwasz jedną funkcję, która nie jest już potrzebna, jak mówisz. Ale z drugiej strony piszesz „projekt staje się całkowicie niefunkcjonalny” - brzmi to tak naprawdę, że funkcja jest absolutnie potrzebna. Proszę o wyjaśnienie.
Doc Brown

26
@DeadMG W takich przypadkach normalnie należy opracować nową funkcję, dodać testy, aby upewnić się, że działa, przenieść istniejący kod do korzystania z nowego interfejsu, a następnie usunąć (obecnie) zbędną starą funkcję. W ten sposób nie powinno być punktu, w którym wszystko się psuje.
sapi

12

Jak mogę uniknąć tego rodzaju kaskadowego refaktora w przyszłości?

Myślenie życzeniowe

Celem jest doskonały projekt i wdrożenie OO nowej funkcji. Celem jest także unikanie refaktoryzacji.

Zacznij od zera i zaprojektuj nową funkcję, która jest tym, czego sobie życzysz. Nie spiesz się, aby zrobić to dobrze.

Zauważ jednak, że kluczem jest tutaj „dodaj funkcję”. Nowe rzeczy pozwalają nam w dużej mierze ignorować obecną strukturę bazy kodu. Nasz projekt myślenia życzeniowego jest niezależny. Ale potrzebujemy jeszcze dwóch rzeczy:

  • Refaktoryzuj tylko tyle, aby wykonać niezbędny szew do wstrzyknięcia / wdrożenia kodu nowej funkcji.
    • Odporność na refaktoryzację nie powinna napędzać nowego projektu.
  • Napisz klasę do klienta z interfejsem API, dzięki któremu nowa funkcja i istniejący kodek będą się wzajemnie ignorować.
    • Transliteruje, aby uzyskać obiekty, dane i wyniki tam iz powrotem. Zasada najmniejszej wiedzy niech będzie przeklęta. Nie zrobimy nic gorszego niż to, co już robi istniejący kod.

Heurystyka, wyciągnięte wnioski itp.

Refaktoryzacja była tak prosta, jak dodanie domyślnego parametru do istniejącego wywołania metody; lub pojedyncze wywołanie metody klasy statycznej.

Metody rozszerzenia istniejących klas mogą pomóc utrzymać jakość nowego projektu przy absolutnie minimalnym ryzyku.

„Struktura” jest wszystkim. Struktura jest realizacją zasady jednolitej odpowiedzialności; konstrukcja ułatwiająca funkcjonalność. Kod pozostanie krótki i prosty aż do hierarchii klas. Czas na nowy projekt jest nadrabiany podczas testów, przeróbek i unikania włamań przez starą dżunglę kodu.

Zajęcia polegające na pobożnym życzeniu koncentrują się na zadaniu. Ogólnie rzecz biorąc, zapomnij o rozszerzeniu istniejącej klasy - po prostu ponownie wywołujesz kaskadę refaktorów i musisz radzić sobie z narzutem „cięższej” klasy.

Usuń wszelkie pozostałości tej nowej funkcjonalności z istniejącego kodu. Tutaj pełna i dobrze zamknięta funkcjonalność nowej funkcji jest ważniejsza niż unikanie refaktoryzacji.


9

Z (cudownej) książki Working Effective with Legacy Code Michaela Feathersa :

Kiedy przełamujesz zależności w starszym kodzie, często musisz nieco zawiesić swoje poczucie estetyki. Niektóre zależności załamują się; inne wyglądają mniej niż idealnie z punktu widzenia projektowania. Są jak punkty nacięcia w chirurgii: po pracy może pozostać blizna w kodzie, ale wszystko pod nią może się poprawić.

Jeśli później uda ci się zakryć kod w punkcie, w którym przerwałeś zależności, możesz także wyleczyć tę bliznę.


6

Wygląda na to, że (zwłaszcza z dyskusji w komentarzach) wprowadziłeś własne zasady, które oznaczają, że ta „drobna” zmiana to tyle samo pracy, co całkowite przepisanie oprogramowania.

Rozwiązaniem musi być „nie rób tego” . Tak dzieje się w prawdziwych projektach. Wiele starych interfejsów API ma w wyniku tego brzydkie interfejsy lub porzucone (zawsze zerowe) parametry lub funkcje o nazwie DoThisThing2 (), które działają tak samo jak DoThisThing () z całkowicie inną listą parametrów. Inne popularne sztuczki to ukrywanie informacji w globach lub oznaczanie wskaźników w celu przemycenia ich przez dużą część frameworka. (Na przykład mam projekt, w którym połowa buforów audio zawiera tylko 4-bajtową wartość magiczną, ponieważ było to o wiele łatwiejsze niż zmiana sposobu, w jaki biblioteka wywoływała swoje kodeki audio).

Trudno udzielać konkretnych porad bez określonego kodu.


3

Zautomatyzowane testy. Nie musisz być fanatykiem TDD, ani nie potrzebujesz 100% zasięgu, ale zautomatyzowane testy pozwalają na pewne zmiany. Ponadto wygląda na to, że masz projekt z bardzo wysokim sprzężeniem; powinieneś przeczytać o zasadach SOLID, które zostały opracowane specjalnie w celu rozwiązania tego rodzaju problemów w projektowaniu oprogramowania.

Poleciłbym te książki.

  • Skutecznie współpracuje ze starszym kodem , piórami
  • Refaktoryzacja , Fowler
  • Rosnące oprogramowanie obiektowe, prowadzone przez testy , Freeman i Pryce
  • Clean Code , Martin

3
Twoje pytanie brzmi: „Jak uniknąć tego [niepowodzenia] w przyszłości?” Odpowiedź brzmi: nawet jeśli obecnie „masz” CI i testy, nie stosujesz ich poprawnie. Nie miałem błędu kompilacji, który trwał dłużej niż dziesięć minut od lat, ponieważ kompilację postrzegam jako „pierwszy test jednostkowy”, a gdy jest zepsuty, naprawiam go, ponieważ muszę widzieć, jak testy przechodzą jako Pracuję dalej nad kodem.
asthasr

6
Jeśli refaktoryzuję mocno używany interfejs, dodaję podkładkę. Ta podkładka obsługuje domyślne, więc starsze połączenia nadal działają. Pracuję nad interfejsem za podkładką dystansową, a potem, kiedy skończę z tym, zaczynam zmieniać klasy, aby ponownie korzystać z interfejsu zamiast podkładki.
asthasr

5
Kontynuacja refaktoryzacji pomimo niepowodzenia kompilacji przypomina martwe obliczanie . To technika nawigacyjna w ostateczności . W refaktoryzacji możliwe jest, że kierunek refaktoryzacji jest po prostu niewłaściwy, a już widziałeś ten wyraźny znak (moment, w którym przestaje się kompilować, tj. Latanie bez wskaźników prędkości), ale zdecydowałeś się kontynuować. W końcu samolot spada z radaru. Na szczęście do refaktoryzacji nie potrzebujemy czarnej skrzynki ani śledczych: zawsze możemy „przywrócić ostatni znany dobry stan”.
rwong

4
@DeadMG: napisałeś „W moim przypadku poprzednie wywołania po prostu nie mają już sensu”, ale w twoim pytaniu „ niewielka zmiana interfejsu, aby to uwzględnić”. Szczerze mówiąc, tylko jedno z tych dwóch zdań może być prawdziwe. Z opisu problemu wydaje się całkiem jasne, że zmiana interfejsu na pewno nie była niewielka . Naprawdę powinieneś naprawdę się zastanowić, jak sprawić, by zmiana była bardziej zgodna z poprzednimi wersjami. Z mojego doświadczenia wynika, że ​​zawsze jest to możliwe, ale najpierw musisz wymyślić dobry plan.
Doc Brown

3
@DeadMG W takim przypadku uważam, że to, co robisz, nie może być rozsądnie nazwane refaktoryzacją, którego podstawowym celem jest zastosowanie zmian projektowych jako serii bardzo prostych kroków.
Jules

3

Czy to tylko symptom moich poprzednich zajęć, które zbyt mocno od siebie zależą?

Najprawdopodobniej tak. Chociaż podobne efekty można uzyskać za pomocą ładnej i czystej bazy kodu, gdy wymagania zmienią się wystarczająco

Jak mogę uniknąć tego rodzaju refaktoryzacji kaskadowej w przyszłości?

Obawiam się, że oprócz przestania pracować nad starszym kodem. Ale możesz użyć metody, która pozwala uniknąć efektu braku działającej bazy kodu przez kilka dni, tygodni lub nawet miesięcy.

Ta metoda nosi nazwę „Metoda Mikado” i działa w następujący sposób:

  1. zapisz cel, który chcesz osiągnąć, na kartce papieru

  2. dokonaj najprostszej zmiany, która zaprowadzi cię w tym kierunku.

  3. sprawdź, czy działa przy użyciu kompilatora i zestawu testów. Jeśli tak, przejdź do kroku 7. W przeciwnym razie przejdź do kroku 4.

  4. na papierze zwróć uwagę na rzeczy, które należy zmienić, aby obecna zmiana zadziałała. Rysuj strzały, z bieżącego zadania, do nowych.

  5. Cofnij zmiany To jest ważny krok. Jest to sprzeczne z intuicją i na początku boli fizycznie, ale skoro właśnie wypróbowałeś prostą rzecz, wcale nie jest tak źle.

  6. wybierz jedno z zadań, które nie ma błędów wychodzących (brak znanych zależności) i wróć do 2.

  7. zatwierdzić zmianę, przekreślić zadanie na papierze, wybrać zadanie, które nie zawiera błędów wychodzących (brak znanych zależności) i powrócić do 2.

W ten sposób będziesz mieć działającą bazę kodu w krótkich odstępach czasu. Gdzie możesz także scalić zmiany z resztą zespołu. I masz wizualną reprezentację tego, co wiesz, że nadal musisz zrobić, to pomaga zdecydować, czy chcesz kontynuować przedsięwzięcie, czy też powinieneś go zatrzymać.


2

Refaktoryzacja jest uporządkowaną dyscypliną, różniącą się od czyszczenia kodu według własnego uznania. Przed rozpoczęciem musisz napisać testy jednostkowe, a każdy krok powinien składać się z konkretnej transformacji, o której wiesz, że nie powinna wprowadzać żadnych zmian w funkcjonalności. Testy jednostkowe powinny przejść po każdej zmianie.

Oczywiście podczas procesu refaktoryzacji naturalnie odkryjesz zmiany, które należy zastosować, które mogą spowodować uszkodzenie. W takim przypadku postaraj się zaimplementować podkładkę kompatybilności dla starego interfejsu korzystającego z nowej struktury. Teoretycznie system powinien nadal działać jak poprzednio, a testy jednostkowe powinny przejść pomyślnie. Można oznaczyć podkładkę zgodności jako przestarzały interfejs i wyczyścić ją w odpowiednim czasie.


2

... Przeprojektowałem projekt, aby dodać funkcję.

Jak powiedział @Jules, Refaktoryzacja i dodawanie funkcji to dwie bardzo różne rzeczy.

  • Refaktoryzacja polega na zmianie struktury programu bez zmiany jego zachowania.
  • Z drugiej strony dodanie funkcji zwiększa jej zachowanie.

... ale czasami trzeba zmienić wewnętrzne działanie, aby dodać swoje rzeczy, ale wolę nazwać to modyfikacją niż refaktoryzacją.

Musiałem wprowadzić niewielką zmianę interfejsu, aby to uwzględnić

Tam rzeczy się psują. Interfejsy są rozumiane jako granice izolujące implementację od sposobu jej użycia. Gdy tylko dotkniesz interfejsów, wszystko po obu stronach (zaimplementowanie go lub użycie) będzie musiało zostać zmienione. To może się rozprzestrzeniać tak daleko, jak się tego doświadczyło.

wtedy klasa konsumująca nie może zostać zaimplementowana z obecnym interfejsem pod względem nowej, więc potrzebuje również nowego interfejsu.

To, że jeden interfejs wymaga zmiany, brzmi dobrze ... że rozprzestrzenia się na inny oznacza, że ​​zmiany rozprzestrzeniają się jeszcze bardziej. Wygląda na to, że jakaś forma danych / danych wymaga spłynięcia w dół łańcucha. Czy tak jest w przypadku?


Twoje przemówienie jest bardzo abstrakcyjne, więc trudno to rozgryźć. Przykład byłby bardzo pomocny. Zwykle interfejsy powinny być dość stabilne i niezależne od siebie, umożliwiając modyfikację części systemu bez szkody dla reszty ... dzięki interfejsom.

... w rzeczywistości najlepszym sposobem uniknięcia kaskadowych modyfikacji kodu są właśnie dobre interfejsy. ;)


-1

Myślę, że zwykle nie możesz, chyba że chcesz zachować rzeczy takimi, jakie są. Jednak w sytuacjach takich jak Twoja myślę, że lepiej jest poinformować zespół i poinformować go, dlaczego należy przeprowadzić pewne refaktoryzacje, aby kontynuować zdrowszy rozwój. Nie chciałbym po prostu sam naprawiać. Rozmawiałbym o tym podczas spotkań Scruma (zakładając, że je macie) i systematycznie podchodziłem do niego z innymi programistami.


1
wydaje się, że nie oferuje to nic istotnego w porównaniu z punktami przedstawionymi i wyjaśnionymi w poprzednich 9 odpowiedziach
komnata

@gnat: Może nie, ale uprościło odpowiedzi.
Tarik
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.