Jak daleko mogę przesunąć refaktoryzację bez zmiany zachowania zewnętrznego?

27

Według Martina Fowlera refaktoryzacja kodu jest (wyróżnienie moje):

Refaktoryzacja to zdyscyplinowana technika restrukturyzacji istniejącego kodu, zmieniająca jego wewnętrzną strukturę bez zmiany zewnętrznych zachowań . Jego sercem jest seria drobnych zachowań zachowujących transformacje. Każda transformacja (zwana „refaktoryzacją”) niewiele robi, ale sekwencja transformacji może spowodować znaczną restrukturyzację. Ponieważ każde refaktoryzacja jest niewielka, prawdopodobieństwo niepowodzenia jest mniejsze. System jest również w pełni sprawny po każdym małym refaktoryzacji, co zmniejsza ryzyko poważnego uszkodzenia systemu podczas restrukturyzacji.

Co to jest „zachowanie zewnętrzne” w tym kontekście? Na przykład, jeśli zastosuję refaktoryzację metody przenoszenia i przeniosę jakąś metodę do innej klasy, wygląda to tak, jakbym zmienił zachowanie zewnętrzne, prawda?

Chciałbym więc dowiedzieć się, w którym momencie zmiana przestaje być refaktorem i staje się czymś więcej. Termin „refaktoryzacja” może być niewłaściwie używany w przypadku większych zmian: czy jest na to inne słowo?

Aktualizacja. Wiele ciekawych odpowiedzi na temat interfejsu, ale czy przeniesienie metody refaktoryzacji nie zmieni interfejsu?

SiberianGuy
źródło
Jeśli istniejące zachowanie jest do bani lub jest niekompletne, zmień je, usuń / przepisz. Być może nie będziesz wtedy ponownie faktoryzować, ale kogo obchodzi, jak się nazywa, jeśli system poprawi się (prawda?) W wyniku tego.
Job
2
Twój przełożony może się przejmować, jeśli otrzymałeś pozwolenie na refaktoryzację i dokonałeś przepisania.
JeffO
2
Granicą refaktoryzacji są testy jednostkowe. Jeśli masz narysowaną przez nich specyfikację, cokolwiek zmienisz, co nie przerywa testów, to refaktoryzacja?
George Silva,
1
Podobne pytanie na SO stackoverflow.com/questions/1025844/…
sylvanaar
Jeśli chcesz dowiedzieć się więcej, ten temat jest również aktywną dziedziną badań. Istnieje wiele publikacji naukowych na ten temat, np. Informatik.uni-trier.de/~ley/db/indices/a-tree/s/…
Skarab

Odpowiedzi:

25

„Zewnętrzne” w tym kontekście oznacza „obserwowalne dla użytkowników”. Użytkownicy mogą być ludźmi w przypadku aplikacji lub innych programów w przypadku publicznego API.

Jeśli więc przeniesiesz metodę M z klasy A do klasy B, a obie klasy znajdują się głęboko w aplikacji, a żaden użytkownik nie zauważy żadnej zmiany w zachowaniu aplikacji z powodu tej zmiany, możesz słusznie nazwać to refaktoryzacją.

Jeśli, OTOH, jakiś inny podsystem / komponent wyższego poziomu zmieni swoje zachowanie lub ulegnie awarii z powodu zmiany, jest to rzeczywiście (zwykle) obserwowalne dla użytkowników (lub przynajmniej dla sysadminów sprawdzających logi). Lub jeśli twoje klasy były częścią publicznego API, może istnieć kod innej firmy, który zależy od tego, czy M jest częścią klasy A, a nie B. Więc żaden z tych przypadków nie jest refaktoryzujący w ścisłym tego słowa znaczeniu.

istnieje tendencja do wywoływania przeróbek kodu jako refaktoryzacji, co, jak sądzę, jest nieprawidłowe.

Rzeczywiście, jest to smutna, ale oczekiwana konsekwencja modnego refaktoryzacji. Deweloperzy od dawna wykonują przeróbki kodu w sposób ad hoc iz pewnością łatwiej jest nauczyć się nowego modnego hasła niż analizować i zmieniać zakorzenione nawyki.

Jakie jest zatem właściwe słowo dla przeróbek, które zmieniają zachowanie zewnętrzne?

Nazwałbym to przeprojektowaniem .

Aktualizacja

Wiele ciekawych odpowiedzi na temat interfejsu, ale czy przeniesienie metody refaktoryzacji nie zmieni interfejsu?

Czego? Konkretne klasy, tak. Ale czy te klasy są w jakikolwiek sposób bezpośrednio widoczne dla świata zewnętrznego? Jeśli nie - ponieważ są one w twoim programie i nie są częścią zewnętrznego interfejsu (API / GUI) programu - żadna dokonana tam zmiana nie jest obserwowana przez podmioty zewnętrzne (chyba że zmiana coś oczywiście psuje).

Wydaje mi się, że istnieje głębsze pytanie: czy konkretna klasa istnieje jako samodzielna jednostka? W większości przypadków odpowiedź brzmi „ nie” : klasa istnieje tylko jako część większego komponentu, ekosystemu klas i obiektów, bez których nie można utworzyć instancji i / lub jest bezużyteczna. Ekosystem ten obejmuje nie tylko jego (bezpośrednie / pośrednie) zależności, ale także inne klasy / obiekty, które od niego zależą. Wynika to z faktu, że bez tych klas wyższego poziomu odpowiedzialność związana z naszą klasą może być bez znaczenia / bezużyteczna dla użytkowników systemu.

Np. W naszym projekcie dotyczącym wynajmu samochodów jest Chargeklasa. Ta klasa sama w sobie nie jest użyteczna dla użytkowników systemu, ponieważ agenci wynajmu stacji i klienci nie mogą wiele zrobić z indywidualną opłatą: zajmują się umowami najmu jako całością (które obejmują wiele różnych rodzajów opłat) . Użytkownicy są najbardziej zainteresowani sumą tych opłat, które ostatecznie mają zapłacić; agent jest zainteresowany różnymi opcjami umowy, długością wynajmu, grupą pojazdów, pakietem ubezpieczenia, dodatkowymi przedmiotami itp. itd., które (na podstawie wyrafinowanych reguł biznesowych) regulują obecne opłaty i sposób obliczania płatności końcowej z tych. Przedstawiciele krajów / analitycy biznesowi dbają o szczegółowe reguły biznesowe, ich synergię i efekty (na dochody firmy itp.).

Niedawno zreorganizowałem tę klasę, zmieniając nazwę większości jej pól i metod (zgodnie ze standardową konwencją nazewnictwa Java, która została całkowicie zaniedbana przez naszych poprzedników). Planuję również dalsze refaktoryzacje, aby zastąpić Stringi charpola bardziej odpowiednie enumi booleantypy. Wszystko to z pewnością zmieni interfejs klasy, ale (jeśli wykonam swoją pracę poprawnie) żadne z nich nie będzie widoczne dla użytkowników naszej aplikacji. Żadnemu z nich nie zależy na tym, w jaki sposób reprezentowane są poszczególne ładunki, mimo że na pewno znają pojęcie ładunku. Mógłbym wybrać jako przykład sto innych klas nie reprezentujących żadnego pojęcia domeny, więc będąc nawet koncepcyjnie niewidocznym dla użytkowników końcowych, ale pomyślałem, że bardziej interesujące jest wybranie przykładu, w którym przynajmniej widoczność jest na poziomie koncepcji. Ładnie pokazuje to, że interfejsy klasowe są jedynie reprezentacjami pojęć domenowych (w najlepszym wypadku), a nie rzeczywistością *. Reprezentację można zmienić bez wpływu na koncepcję. Użytkownicy mają tylko i rozumieją tę koncepcję; naszym zadaniem jest mapowanie koncepcji i reprezentacji.

* I można łatwo dodać, że model domeny, który reprezentuje nasza klasa, sam w sobie jest jedynie przybliżoną reprezentacją jakiejś „prawdziwej rzeczy” ...

Péter Török
źródło
3
nitpicking, powiedziałbym, że „zmiana projektu” to nie przeprojektowanie. przeprojektowanie brzmi zbyt poważnie.
user606723,
4
ciekawe - w swoim przykładem klasy A jest „istniejący korpus kodu”, a jeśli metoda M jest publiczna następnie zewnętrzne zachowanie A jest zmieniane więc można chyba powiedzieć, że klasa A jest zmodernizowany, a cały system jest refactored..
saus
Lubię obserwowalne dla użytkowników. Dlatego nie powiedziałbym, że przerwanie testów jednostkowych jest znakiem, ale raczej testami integracyjnymi byłyby znakiem.
Andy Wiesendanger,
8

Zewnętrzne oznacza po prostu interfejs w jego prawdziwym znaczeniu językowym. Rozważ krowę w tym przykładzie. Tak długo, jak karmisz warzywa i dostajesz mleko jako wartość zwrotną, nie obchodzi cię, jak działają narządy wewnętrzne. Teraz, jeśli Bóg zmieni krowy narządów wewnętrznych, tak że jego krew stanie się niebieska, o ile punkt wejścia i punkt wyjścia (usta i mleko) nie zmienią się, można to uznać za refaktoryzację.

Saeed Neamati
źródło
1
Nie sądzę, że to dobra odpowiedź. Refaktoryzacja może oznaczać zmiany interfejsu API. Ale nie powinno to wpływać na użytkownika końcowego ani na sposób odczytu / generowania plików wejściowych / plików.
rds
3

Dla mnie refaktoryzacja była najbardziej produktywna / wygodna, gdy granice zostały ustalone przez testy i / lub formalną specyfikację.

  • Granice te są wystarczająco sztywne, aby czuć się bezpiecznie wiedząc, że jeśli od czasu do czasu przekroczę, zostanie to wykryte na tyle szybko, że nie będę musiał cofać wielu zmian, aby odzyskać. Z drugiej strony dają one wystarczającą swobodę w ulepszaniu kodu bez obawy o zmianę nieistotnego zachowania.

  • Szczególnie podoba mi się to, że tego rodzaju granice są adaptacyjne, że tak powiem. Mam na myśli: 1) Wprowadzam zmiany i sprawdzam, czy są zgodne ze specyfikacją / testami. Następnie 2) jest przekazywany do kontroli jakości lub testów użytkownika - zwróć uwagę, że nadal może się nie powieść, ponieważ brakuje czegoś w specyfikacjach / testach. OK, jeśli 3a) testy zakończą się pomyślnie, jestem skończony. W przeciwnym razie, jeśli 3b) testowanie się nie powiedzie, to 4) wycofam zmianę i 5) dodaję testy lub wyjaśnij specyfikację, aby następnym razem ten błąd się nie powtórzył. Należy pamiętać, że bez względu na to, czy testowanie przepustki lub nie, ja zdobyć coś - albo kodu / testy / specyfikacji zostanie poprawiona - moje wysiłki nie zamieni się w ogólnej ilości odpadów.

Jeśli chodzi o granice innego rodzaju - jak dotąd nie miałem szczęścia.

„Obserwowalne dla użytkowników” to bezpieczny zakład, jeśli ktoś ma dyscyplinę, którą należy stosować - co w jakiś sposób zawsze wymagało dużego wysiłku w analizie istniejących / tworzenia nowych testów - może zbyt dużego wysiłku. Inną rzeczą, której nie lubię w tym podejściu, jest to, że ślepe jego stosowanie może okazać się zbyt restrykcyjne. - Ta zmiana jest zabroniona, ponieważ wraz z nią ładowanie danych zajmie 3 sekundy zamiast 2. - Uhm, a co powiesz na sprawdzenie u użytkowników / ekspertów UX, czy jest to istotne, czy nie? - Nie ma mowy, żadna zmiana w obserwowalnym zachowaniu jest zabroniona, kropka. Bezpieczny? obstawiasz! produktywny? nie całkiem.

Innym, którego próbowałem, jest zachowanie logiki kodu (sposób, w jaki to rozumiem podczas czytania). Z wyjątkiem najbardziej elementarnych (i zazwyczaj niezbyt owocnych) zmian, ta zawsze była puszką robaków ... a może powinienem powiedzieć puszkę błędów? Mam na myśli błędy regresji. Po prostu zbyt łatwo jest złamać coś ważnego podczas pracy z kodem spaghetti.

komar
źródło
3

Refaktoryzacja książka jest dość silny w swojej wiadomości, które można wykonywać tylko refactoring gdy masz zasięg testów jednostkowych .

Można więc powiedzieć, że dopóki nie łamie się żadnych testów jednostkowych, refaktoryzuje się . Kiedy przełamujesz testy, nie dokonujesz już refaktoryzacji.

Ale: co powiesz na takie proste refaktoryzacje, takie jak zmiana nazw klas lub członków? Czy oni nie łamią testów?

Tak, robią to i za każdym razem musisz rozważyć, czy przerwa jest znacząca. Jeśli twój SUT jest publicznym API / SDK, wówczas zmiana nazwy jest rzeczywiście przełomową zmianą. Jeśli nie, to prawdopodobnie OK.

Pamiętaj jednak, że często testy nie są przerywane, ponieważ zmieniłeś zachowanie, na którym ci zależy, ale dlatego, że są to testy kruche .

Mark Seemann
źródło
3

Najlepszym sposobem zdefiniowania „zachowania zewnętrznego” w tym kontekście mogą być „przypadki testowe”.

Jeśli zrefaktoryzujesz kod i nadal będzie on przekazywał przypadki testowe (zdefiniowane przed refaktoryzacją), wówczas refaktoryzacja nie zmieniła zachowania zewnętrznego. Jeśli jeden lub więcej przypadków testowych nie, to nie zmienił zachowanie zewnętrznego.

Tak przynajmniej rozumiem różne książki opublikowane na ten temat (np. Fowler).

Cesar Providenti
źródło
2

Granicą byłaby linia, która mówi między tymi, którzy opracowują, utrzymują, wspierają projekt i tymi, którzy są jego użytkownikami innymi niż osoby wspierające, opiekunów, programistów. Zatem w świecie zewnętrznym zachowanie wygląda tak samo, podczas gdy wewnętrzne struktury stojące za tym zachowaniem uległy zmianie.

Dlatego przenoszenie funkcji między klasami powinno być OK, o ile nie są to te, które widzą użytkownicy.

Tak długo, jak przerobienie kodu nie zmienia zachowań zewnętrznych, nie dodaje nowych funkcji ani nie usuwa istniejących, myślę, że można nazwać przerobienie refaktoryzacją.

vpit3833
źródło
Nie zgadzać się. Jeśli zamienisz cały kod dostępu do danych na nHibernate, nie zmieni to zachowania zewnętrznego, ale nie będzie zgodne z „zdyscyplinowanymi technikami” Fowlera. Byłoby to przeprojektowanie, a nazwanie go refaktoryzacją ukrywa związany z tym czynnik ryzyka.
pdr
1

Z całym szacunkiem musimy pamiętać, że użytkownicy klasy nie są użytkownikami końcowymi aplikacji zbudowanych z tą klasą, ale raczej klasami, które są implementowane poprzez wykorzystanie - albo wywołanie, albo dziedziczenie - z klasy, która jest refaktoryzowana .

Kiedy mówisz, że „zachowanie zewnętrzne nie powinno się zmieniać”, masz na myśli, że jeśli chodzi o użytkowników, klasa zachowuje się dokładnie tak, jak wcześniej. Może się zdarzyć, że pierwotna (nierefaktoryzowana) implementacja była jedną klasą, a nowa (refaktoryzowana) implementacja ma jedną lub więcej superklas, na których zbudowana jest klasa, ale użytkownicy nigdy nie widzą wnętrza (implementacji) zobacz tylko interfejs.

Więc jeśli klasa ma metodę o nazwie „doSomethingAmazing”, nie ma znaczenia dla użytkownika, czy jest ona implementowana przez klasę, do której się odnoszą, lub przez nadklasę, na której zbudowana jest ta klasa. Jedyne, co liczy się dla użytkownika, to to, że nowy (doładowany) „doSomethingAmazing” ma ten sam wynik, co do starego (bezrefrakcjonowanego) „doSomethingAmazing”.

Jednak to, co nazywa się refaktoryzacją w wielu przypadkach, nie jest prawdziwym refaktoryzowaniem, ale być może ponownym wdrożeniem, które ma na celu ułatwienie modyfikacji kodu i dodanie nowej funkcji. Tak więc w tym późniejszym przypadku (pseudo) refaktoryzacji nowy (refaktoryzowany) kod faktycznie robi coś innego, a może coś więcej niż stary.

Zeke Hansell
źródło
Co jeśli formularz okna wyświetlał okno dialogowe „Czy na pewno chcesz nacisnąć przycisk OK?” i postanowiłem go usunąć, ponieważ przynosi niewiele dobrego i denerwuje użytkowników, a następnie czy ponownie przefakturowałem kod, przeprojektowałem go, poprawiłem, usunąłem błędy, inne?
Job
@job: zmieniłeś program, aby spełnić nowe specyfikacje.
jmoreno
IMHO, możesz mieszać różne kryteria tutaj. Przepisywanie kodu od podstaw nie jest tak naprawdę refaktoryzacją, ale dzieje się tak niezależnie od tego, czy zmieniło zachowanie zewnętrzne, czy nie. Ponadto, jeśli zmiana interfejsu klasy nie była refaktoryzacją, to dlaczego Move Method i in. istnieje w katalogu refaktoryzacji ?
Péter Török,
@ Péter Török - Zależy to całkowicie od tego, co rozumiesz przez „zmianę interfejsu klasy”, ponieważ w języku OOP, który implementuje dziedziczenie, interfejs klasy obejmuje nie tylko to, co jest implementowane przez samą klasę przez wszystkich jej przodków. Zmiana interfejsu klasy oznacza usunięcie / dodanie metody do interfejsu (lub zmianę podpisu metody - tj. Liczby typu przekazywanych parametrów). Refaktoryzacja oznacza, kto odpowiada na metodę, klasę lub nadklasę.
Zeke Hansell
IMHO - całe to pytanie może być zbyt ezoteryczne, aby miało jakąkolwiek przydatną wartość dla programistów.
Zeke Hansell
1

Przez „zachowanie zewnętrzne” mówi przede wszystkim o interfejsie publicznym, ale dotyczy to również wyników / artefaktów systemu. (tzn. masz metodę generującą plik, zmiana formatu pliku zmieniłaby zachowanie zewnętrzne)

e: Uważam, że „metoda przenoszenia” jest zmianą w zachowaniu zewnętrznym. Pamiętaj, że Fowler mówi o istniejących bazach kodów, które zostały wypuszczone na wolność. W zależności od twojej sytuacji możesz być w stanie sprawdzić, czy twoja zmiana nie psuje żadnych zewnętrznych klientów i kontynuować swoją drogę.

e2: „Więc jakie jest właściwe słowo dla przeróbek, które zmieniają zachowanie zewnętrzne?” - Refaktoryzacja API, Breaking Change itp. ... wciąż refaktoryzuje, po prostu nie przestrzega najlepszych praktyk refaktoryzacji publicznego interfejsu, który jest już na wolności z klientami.


źródło
Ale jeśli chodzi o interfejs publiczny, to co z refaktoryzacją „Metoda przenoszenia”?
SiberianGuy
@Idsa Edytowałem moją odpowiedź, aby odpowiedzieć na twoje pytanie.
@kekela, ale wciąż nie jest dla mnie jasne, gdzie kończy się ta „refaktoryzacja”
SiberianGuy
@idsa Zgodnie z opublikowaną definicją przestaje być refaktoryzacją w momencie zmiany publicznego interfejsu. (przeniesienie metody publicznej z jednej klasy do drugiej byłoby tego przykładem)
0

„Metoda przenoszenia” jest techniką refaktoryzacji, a nie samą refaktoryzacją. Refaktoryzacja to proces stosowania kilku technik refaktoryzacji do klas. Tam, gdzie mówisz, zastosowałem „metodę przenoszenia” do klasy, tak naprawdę nie masz na myśli „przebudowałem (klasę)”, masz na myśli „zastosowałem technikę refaktoryzacji w tej klasie”. Refaktoryzacja, w tym sensie, najczystsze znaczenie ma zastosowanie do projektu, a dokładniej, do pewnej części projektu aplikacji, którą można postrzegać jako czarną skrzynkę.

Można powiedzieć, że „refaktoryzacja” stosowana w kontekście klas oznacza „technikę refaktoryzacji”, a zatem „metoda przenoszenia” nie łamie definicji refaktoryzacji procesu. Na tej samej stronie „refaktoryzacja” w kontekście projektu nie psuje istniejących funkcji w kodzie, tylko „psuje” projekt (co i tak jest jego celem).

Podsumowując, „granica”, o której mowa w pytaniu, zostaje przekroczona, jeśli pomylisz (mix: D) refaktoryzację techniki z refaktoryzacją procesu.

PS: Dobre pytanie, przy okazji.

Belun
źródło
Przeczytaj to kilka razy, ale nadal nie rozumiem
SiberianGuy
jakiej części nie zrozumiałeś? (istnieje proces refaktoryzacji, do którego odnosi się twoja definicja lub refaktoryzacja techniki, w twoim przykładzie metoda przenoszenia, do której definicja nie ma zastosowania; w związku z tym metoda move nie narusza definicji refaktoryzacji-the- proces lub nie przekracza jego granic, czymkolwiek są). mówię, że twoja troska nie powinna istnieć dla twojego przykładu. granica refaktoryzacji nie jest czymś rozmytym. po prostu stosujesz definicję czegoś do czegoś innego.
Belun
0

jeśli mówimy o faktorowaniu liczb, to opisujemy grupę liczb całkowitych, które po pomnożeniu równą się liczbie początkowej. Jeśli weźmiemy tę definicję do faktorowania i zastosujemy ją do programistycznego terminu refactor, wówczas refaktoryzacja rozbije program na najmniejsze jednostki logiczne, tak że gdy zostaną uruchomione jako program, wygenerują takie same dane wyjściowe (przy tych samych danych wejściowych ) jako oryginalny program.

użytkownik34691
źródło
0

Granice refaktoryzacji są specyficzne dla danego refaktoryzacji. Po prostu nie może być jednej odpowiedzi i obejmuje wszystko. Jednym z powodów jest to, że sam fakt refaktoryzacji jest niespecyficzny.

Możesz refaktoryzować:

  • Kod źródłowy
  • Architektura programu
  • Projektowanie interfejsu użytkownika / interakcja użytkownika

i jestem pewien, że kilka innych rzeczy.

Być może refaktor można zdefiniować jako

„akcja lub zestaw akcji, które zmniejszają entropię w danym systemie”

sylvanaar
źródło
0

Zdecydowanie są pewne granice, jak daleko można refaktoryzować. Zobacz: kiedy dwa algorytmy są takie same?

Ludzie zwykle uważają algorytmy za bardziej abstrakcyjne niż programy, które je implementują. Naturalnym sposobem sformalizowania tej idei jest to, że algorytmy są klasami równoważności programów w odniesieniu do odpowiedniej relacji równoważności. Twierdzimy, że taka relacja równoważności nie istnieje.

Więc nie idź za daleko, bo nie będziesz mieć zaufania do wyniku. Z drugiej strony doświadczenie wskazuje, że często można zastąpić jeden algorytm innym i uzyskać te same odpowiedzi, czasem szybciej. To jest jego piękno, prawda?

Bruce Ediger
źródło
0

Możesz myśleć o znaczeniu „zewnętrznym”

Poza codziennym wstawaniem.

Tak więc każdą zmianę w systemie, która nie ma wpływu na żadne świnie, można uznać za refaktoryzację.

Zmiana interfejsu klasy nie stanowi problemu, jeśli z klasy korzysta tylko jeden system zbudowany i utrzymywany przez zespół. Jeśli jednak klasa jest klasą publiczną w środowisku .net używanym przez każdego programistę .net, jest to zupełnie inna sprawa.

Ian
źródło