Czy „zmienne powinny żyć w możliwie najmniejszym zakresie” obejmują przypadek „zmienne nie powinny istnieć, jeśli to możliwe”?

32

Zgodnie z przyjętą odpowiedzią „ Racjonalne uzasadnienie preferowania zmiennych lokalnych zamiast zmiennych instancji? ” Zmienne powinny mieć możliwie najmniejszy zakres.
Uprość problem do mojej interpretacji, oznacza to, że powinniśmy zmienić kod tego rodzaju:

public class Main {
    private A a;
    private B b;

    public ABResult getResult() {
        getA();
        getB();
        return ABFactory.mix(a, b);
    }

    private getA() {
      a = SomeFactory.getA();
    }

    private getB() {
      b = SomeFactory.getB();
    }
}

w coś takiego:

public class Main {
    public ABResult getResult() {
        A a = getA();
        B b = getB();
        return ABFactory.mix(a, b);
    }

    private getA() {
      return SomeFactory.getA();
    }

    private getB() {
      return SomeFactory.getB();
    }
}

ale zgodnie z „duchem” zmiennych zmienne powinny żyć w najmniejszym możliwym zakresie, czy „nigdy nie mieć zmiennych” ma mniejszy zakres niż „mieć zmiennych”? Myślę więc, że powyższa wersja powinna zostać refaktoryzowana:

public class Main {
    public ABResult getResult() {
        return ABFactory.mix(getA(), getB());
    }

    private getA() {
      return SomeFactory.getA();
    }

    private getB() {
      return SomeFactory.getB();
    }
}

więc getResult()nie ma żadnych lokalnych zmiennych. Czy to prawda?

ocomfd
źródło
72
Tworzenie wyraźnych zmiennych ma tę zaletę, że trzeba je nazwać. Wprowadzenie kilku zmiennych może szybko przekształcić nieprzejrzystą metodę w czytelną.
Jared Goguen
11
Szczerze mówiąc, przykład w zakresie nazw zmiennych aib jest zbyt wymyślony, aby wykazać wartość zmiennych lokalnych. Na szczęście i tak masz dobre odpowiedzi.
Doc Brown
3
W drugim fragmencie zmienne nie są tak naprawdę zmiennymi . Są to stałe lokalne, ponieważ ich nie modyfikujesz. Kompilator Java potraktuje je tak samo, jeśli zdefiniujesz je jako ostateczne, ponieważ są one w zasięgu lokalnym, a kompilator wie, co się z nimi stanie. To kwestia stylu i opinii, czy rzeczywiście powinieneś użyć finalsłowa kluczowego, czy nie.
hyde
2
@JaredGoguen Tworzenie wyraźnych zmiennych wiąże się z koniecznością ich nazwania i korzyścią z możliwości ich nazwania.
Bergi,
2
Absolutnie zero zasad stylu kodu, takich jak „zmienne powinny żyć w najmniejszym możliwym zakresie”, są uniwersalnie stosowane bez zastanowienia. Jako taki, nigdy nie chcesz opierać decyzji w stylu kodu na uzasadnieniu formularza w tym pytaniu, w którym bierzesz prostą wytyczną i rozszerzasz ją na mniej oczywisty obszar („zmienne powinny mieć małe zakresy, jeśli to możliwe” -> „zmienne nie powinny istnieć, jeśli to możliwe”). Albo istnieją uzasadnione powody, które potwierdzają twoją konkluzję, albo twoja konkluzja jest niewłaściwym przedłużeniem; tak czy inaczej, nie potrzebujesz oryginalnej wytycznej.
Ben

Odpowiedzi:

110

Nie. Jest kilka powodów, dla których:

  1. Zmienne o znaczących nazwach mogą ułatwić zrozumienie kodu.
  2. Podział skomplikowanych formuł na mniejsze etapy może ułatwić odczytanie kodu.
  3. Buforowanie
  4. Przechowywanie odniesień do obiektów, aby można było z nich korzystać więcej niż jeden raz.

I tak dalej.

Robert Harvey
źródło
22
Warto również wspomnieć: niezależnie od tego, wartość będzie przechowywana w pamięci, więc i tak kończy się na tym samym zakresie. Może i nazwać to (z powodów, o których Robert wspomina powyżej)!
Maybe_Factor
5
@Maybe_Factor Wytycznych tak naprawdę nie ma ze względu na wydajność; chodzi o to, jak łatwo jest utrzymać kod. Ale to wciąż oznacza, że ​​znaczący miejscowi są lepsi niż jedno wielkie wyrażenie, chyba że komponujesz tylko dobrze nazwane i zaprojektowane funkcje (np. Nie ma powodu, aby to robić var taxIndex = getTaxIndex();).
Luaan,
16
Wszystkie są ważnymi czynnikami. Sposób, w jaki patrzę na to: zwięzłość jest cel; jasność jest inna; solidność to kolejna; wydajność jest inna; i tak dalej. Żaden z nich nie jest celem bezwzględnym i czasami powoduje konflikt. Dlatego naszym zadaniem jest wypracowanie najlepszego sposobu zrównoważenia tych celów.
gidds
8
Kompilator zajmie się 3 i 4.
Stop Harming Monica
9
Liczba 4 może być ważna dla poprawności. Jeśli wartość wywołania funkcji może się zmienić (np. now()) Usunięcie zmiennej i wywołanie metody więcej niż jeden raz może spowodować błędy. Może to stworzyć sytuację, która jest naprawdę subtelna i trudna do debugowania. Może się to wydawać oczywiste, ale jeśli masz ślepą misję refaktoryzacji w celu usunięcia zmiennych, łatwo jest wprowadzić wady.
JimmyJames
16

Zgadzam się, należy unikać zmiennych, które nie są konieczne i nie poprawiają czytelności kodu. Im więcej zmiennych znajduje się w danym punkcie kodu, tym bardziej skomplikowany jest ten kod.

Naprawdę nie widzę korzyści ze zmiennych ai bw twoim przykładzie, więc napisałbym wersję bez zmiennych. Z drugiej strony funkcja jest tak prosta, że ​​nie sądzę, żeby miała to duże znaczenie.

Staje się to coraz większym problemem, im dłużej funkcja jest dostępna, tym więcej zmiennych ma zasięg.

Na przykład jeśli masz

    a=getA();
    b=getB();
    m = ABFactory.mix(a,b);

Na górze większej funkcji zwiększasz obciążenie psychiczne związane z rozumieniem reszty kodu, wprowadzając trzy zmienne zamiast jednej. Musisz przeczytać resztę kodu, aby zobaczyć, aczy bsą ponownie używane. Miejscowi, których zasięg jest dłuższy niż potrzeba, są niekorzystni dla ogólnej czytelności.

Oczywiście w przypadku, gdy zmienna jest to konieczne (np przechowywanie tymczasowy rezultat) lub gdy zmienna ma poprawić czytelność kodu, to powinny być przechowywane.

JacquesB
źródło
2
Oczywiście, do momentu, gdy metoda jest tak długa, że ​​liczba mieszkańców jest zbyt duża, aby można ją było łatwo utrzymać, prawdopodobnie i tak chcesz ją rozbić.
Luaan,
4
Jedną z zalet zmiennych „obcych”, takich jak var result = getResult(...); return result;to, że można nałożyć punkt przerwania returni dowiedzieć się, co dokładnie resultjest.
Joker_vD
5
@Joker_vD: Debugger godny swojej nazwy powinien i tak być w stanie pozwolić ci to wszystko (wyświetlić wyniki wywołania funkcji bez zapisywania go w zmiennej lokalnej + ustawić punkt przerwania przy wyjściu z funkcji).
Christian Hackl
2
@ChristianHackl Bardzo niewiele debuggesr pozwala ci to zrobić bez ustawiania możliwie skomplikowanych zegarków, których dodanie zajmuje trochę czasu (a jeśli funkcje mają efekty uboczne, mogą wszystko zepsuć). Wezmę wersję ze zmiennymi do debugowania w dowolnym dniu tygodnia.
Gabe Sechan
1
@ChristianHackl i aby dalej na tym opierać się, nawet jeśli debugger nie pozwala na to domyślnie, ponieważ jest on strasznie zaprojektowany, możesz po prostu zmodyfikować lokalny kod na komputerze, aby zapisać wynik, a następnie sprawdzić go w debuggerze . Nadal nie ma powodu, aby przechowywać wartość w zmiennej tylko w tym celu.
The Great Duck
11

Oprócz innych odpowiedzi chciałbym wskazać coś jeszcze. Korzyścią z utrzymywania małego zakresu zmiennej jest nie tylko zmniejszenie ilości kodu dostępnego do zmiennej, ale także zmniejszenie liczby możliwych ścieżek kontroli, które mogą potencjalnie zmodyfikować zmienną (przez przypisanie jej nowej wartości lub wywołanie metoda mutacji na istniejącym obiekcie przechowywanym w zmiennej).

Zmienne o zasięgu klasowym (instancja lub statyczne) mają znacznie więcej możliwych ścieżek kontroli niż zmienne o zasięgu lokalnym, ponieważ można je mutować metodami, które można wywoływać w dowolnej kolejności, dowolną liczbę razy, a często także kodem spoza klasy .

Rzućmy okiem na twoją początkową getResultmetodę:

public ABResult getResult() {
    getA();
    getB();
    return ABFactory.mix(this.a, this.b);
}

Teraz nazwy getAi getBmogą sugerować , że zostaną przypisane do this.ai this.b, nie możemy na pewno wiedzieć po samym spojrzeniu getResult. Dlatego możliwe jest, że wartości this.ai this.bprzekazane do mixmetody zamiast tego pochodzą ze stanu thisobiektu getResult, który został wcześniej wywołany, co jest niemożliwe do przewidzenia, ponieważ klienci kontrolują sposób i czas wywoływania metod.

W zmienionym kodzie z lokalnymi ai bzmiennymi jest jasne, że istnieje dokładnie jeden (wolny od wyjątków) przepływ kontrolny od przypisania każdej zmiennej do jej użycia, ponieważ zmienne są deklarowane tuż przed ich użyciem.

Zatem znaczną zaletą jest przenoszenie (modyfikowalne) zmiennych z zakresu klasy do zakresu lokalnego (jak również przenoszenie (modyfikowalne) zmiennych z zewnątrz pętli do wewnątrz), ponieważ upraszcza rozumowanie przepływu sterowania.

Z drugiej strony wyeliminowanie zmiennych jak w poprzednim przykładzie ma mniejszą zaletę, ponieważ tak naprawdę nie wpływa na rozumowanie przepływu sterowania. Tracisz także nazwy nadane wartościom, co nie zdarza się, gdy po prostu przenosisz zmienną do zakresu wewnętrznego. Jest to kompromis, który należy wziąć pod uwagę, więc eliminacja zmiennych może być lepsza w niektórych przypadkach, a gorsza w innych.

Jeśli nie chcesz stracić nazw zmiennych, ale nadal chcesz zmniejszyć zakres zmiennych (w przypadku ich użycia w większej funkcji), możesz rozważyć zawarcie zmiennych i ich zastosowanie w instrukcji blokowej ( lub przeniesienie ich do własnej funkcji ).

YawarRaza7349
źródło
2

Jest to w pewnym stopniu zależne od języka, ale powiedziałbym, że jedną z mniej oczywistych korzyści programowania funkcjonalnego jest to, że zachęca programistę i czytnik kodu do ich niepotrzebowania. Rozważać:

(reduce (fn [map string] (assoc map string (inc (map string 0))) 

Lub trochę LINQ:

var query2 = mydb.MyEntity.Select(x => x.SomeProp).AsEnumerable().Where(x => x == "Prop");

Lub Node.js:

 return visionFetchLabels(storageUri)
    .then(any(isCat))
    .then(notifySlack(secrets.slackWebhook, `A cat was posted: ${storageUri}`))
    .then(logSuccess, logError)
    .then(callback)

Ostatni to łańcuch wywoływania funkcji na wyniku poprzedniej funkcji, bez żadnych zmiennych pośrednich. Wprowadzenie ich uczyniłoby to o wiele mniej jasne.

Różnica między pierwszym przykładem a pozostałymi dwoma polega jednak na domyślnej kolejności działania . Może nie być to ta sama kolejność, w jakiej jest obliczana, ale to kolejność, w której czytelnik powinien o tym pomyśleć. W przypadku dwóch drugich jest to od lewej do prawej. W przykładzie Lisp / Clojure jest bardziej jak od prawej do lewej. Powinieneś być ostrożny w pisaniu kodu, który nie jest w „domyślnym kierunku” dla twojego języka, i zdecydowanie należy unikać wyrażeń „wyodrębnionych”, które łączą oba te języki.

Operator potoku F # |>jest przydatny częściowo, ponieważ pozwala pisać rzeczy od lewej do prawej, które w przeciwnym razie musiałyby być od prawej do lewej.

pjc50
źródło
2
Zacząłem używać podkreślenia jako mojej zmiennej w prostych wyrażeniach LINQ lub lambdach. myCollection.Select(_ => _.SomeProp).Where(_ => _.Size > 4);
Graham
Nie jestem pewien, czy odpowiada to w najmniejszym stopniu na pytanie OP ...
AC
1
Dlaczego opinie negatywne? Wydaje mi się, że to dobra odpowiedź, nawet jeśli nie odpowiada bezpośrednio na pytanie, to wciąż przydatna informacja
reggaeguitar
1

Powiedziałbym „nie”, ponieważ powinieneś przeczytać „najmniejszy możliwy zakres” jako „spośród istniejących zakresów lub tych, które można dodać”. W przeciwnym razie sugerowałoby to, że powinieneś tworzyć sztuczne zakresy (np. Nieuzasadnione {}bloki w językach podobnych do C), aby upewnić się, że zakres zmiennej nie wykracza poza ostatnie zamierzone użycie, i że byłoby to ogólnie odrzucone jako zaciemnienie / bałagan, chyba że już istnieje dobry powód, aby zakres istniał niezależnie.

R ..
źródło
2
Jednym z problemów związanych z określaniem zakresu w wielu językach jest to, że bardzo często zdarza się, że niektóre zmienne są używane do obliczania wartości początkowych innych zmiennych. Jeśli język miał konstrukcje jawnie do celów określania zakresu, co pozwalało na stosowanie wartości obliczonych przy użyciu zmiennych o zasięgu wewnętrznym do inicjowania zmiennych o zasięgu zewnętrznym, takie zakresy mogłyby być bardziej użyteczne niż ściśle zagnieżdżone zakresy, których wymaga wiele języków.
supercat
1

Rozważ funkcje ( metody ). Nie dzieli ani kodu na najmniejszą możliwą podzadanie, ani największego pojedynczego fragmentu kodu.

Jest to limit przesuwania z ograniczeniem logicznych zadań na części eksploatacyjne.

To samo dotyczy zmiennych . Wskazanie logicznych struktur danych na zrozumiałe części. Lub po prostu nazwij (określając) parametry:

boolean automatic = true;
importFile(file, automatic);

Ale oczywiście mając na górze deklarację i dwieście linii dalej pierwsze użycie jest obecnie akceptowane jako zły styl. To jest właśnie to, co „zmienne powinny żyć w możliwie najmniejszym zakresie” . Jak bardzo blisko „nie używaj ponownie zmiennych”.

Joop Eggen
źródło
1

Tym, czego brakuje jako przyczyny NIE, jest debugowanie / zdolność do czytania. Kod powinien być w tym celu zoptymalizowany, a jasne i zwięzłe nazwy bardzo pomagają, np. Wyobraź sobie trójdrożny

if (frobnicate(x) && (get_age(x) > 2000 || calculate_duration(x) < 100 )

ten wiersz jest krótki, ale już trudny do odczytania. Dodaj jeszcze kilka parametrów, a jeśli obejmuje wiele linii.

can_be_frobnicated = frobnicate(x)
is_short_lived_or_ancient = get_age(x) > 2000 || calculate_duration(x) < 100
if (can_be_frobnicated || is_short_lived_or_ancient )

Dla mnie ten sposób jest łatwiejszy do odczytania i przekazania znaczenia - więc nie mam problemu ze zmiennymi pośrednimi.

Innym przykładem mogą być języki takie jak R, gdzie ostatnia linia jest automatycznie wartością zwracaną:

some_func <- function(x) {
    compute(x)
}

jest to niebezpieczne, czy zwrot jest uzasadniony czy potrzebny? to jest jaśniejsze:

some_func <- function(x) {
   rv <- compute(x)
   return(rv)
}

Jak zawsze jest to wezwanie do oceny - wyeliminuj zmienne pośrednie, jeśli nie poprawią czytania, w przeciwnym razie zachowaj je lub wprowadź.

Kolejną kwestią może być debuggowanie: jeśli wyniki pośredników są interesujące, najlepiej po prostu wprowadzić pośrednika, tak jak w powyższym przykładzie R. Częstotliwość wywoływania tego jest trudna do wyobrażenia i uważaj na to, co się rejestruje - zbyt wiele zmiennych debugujących jest mylące - ponownie wezwanie do oceny.

Christian Sauer
źródło
0

Odnosząc się tylko do twojego tytułu: absolutnie, jeśli zmienna nie jest potrzebna, należy ją usunąć.

Ale „niepotrzebne” nie oznacza, że ​​można napisać równoważny program bez użycia zmiennej, w przeciwnym razie powiedziano by nam, że powinniśmy pisać wszystko w formacie binarnym.

Najczęstszym rodzajem niepotrzebnej zmiennej jest nieużywana zmienna, im mniejszy zakres zmiennej, tym łatwiej jest ustalić, że jest niepotrzebna. Trudniej jest ustalić, czy zmienna pośrednia jest niepotrzebna, ponieważ nie jest to sytuacja binarna, lecz kontekstowa. W rzeczywistości identyczny kod źródłowy w dwóch różnych metodach może dać inną odpowiedź tego samego użytkownika, w zależności od wcześniejszych problemów z naprawieniem otaczającego kodu.

Jeśli twój przykładowy kod był dokładnie taki, jak przedstawiono, sugerowałbym pozbycie się dwóch prywatnych metod, ale nie miałbym najmniejszej obawy, czy zapisałeś wynik wywołań fabrycznych do zmiennej lokalnej, czy po prostu użyłeś ich jako argumentów do miksu metoda.

Czytelność kodu przebija wszystko oprócz poprawnego działania (poprawnie obejmuje dopuszczalne kryteria wydajności, które rzadko „są tak szybkie, jak to możliwe”).

jmoreno
źródło