Co jest nie tak z magicznymi łańcuchami?

164

Jako doświadczony programista nauczyłem się unikać magicznych ciągów.

Mój problem polega na tym, że minęło tyle czasu, odkąd ich użyłem, zapomniałem o większości powodów. W rezultacie mam problem z wyjaśnieniem, dlaczego stanowią one problem dla moich mniej doświadczonych kolegów.

Jakie są obiektywne powody, aby ich unikać? Jakie problemy powodują?

Kramii
źródło
38
Co to jest magiczna struna? To samo, co magiczne liczby ?
Laiv
14
@Laiv: Są podobne do magicznych liczb, tak. Podoba mi się definicja na deviq.com/magic-strings : „Magiczne łańcuchy to wartości łańcuchowe określone bezpośrednio w kodzie aplikacji, które mają wpływ na zachowanie aplikacji.”. (Definicja na en.wikipedia.org/wiki/Magic_string wcale nie jest tym, o czym myślę)
Kramii
17
to zabawne, którego nauczyłem się nie znosić ... później Jakich argumentów mogę użyć, aby przekonać moich juniorów ... Nigdy nie kończąca się historia :-). Nie próbowałbym „przekonać”, że wolę, żeby się uczyli samodzielnie. Nic nie trwa dłużej niż lekcja / pomysł osiągnięty przez twoje własne doświadczenie. To, co próbujesz zrobić, to indoktrynacja . Nie rób tego, chyba że chcesz mieć drużynę lemingów.
Laiv
15
@Laiv: Chciałbym pozwolić ludziom uczyć się z własnego doświadczenia, ale niestety nie jest to dla mnie opcja. Pracuję dla szpitala finansowanego ze środków publicznych, w którym subtelne błędy mogą zagrozić opiece nad pacjentem i gdzie nie możemy sobie pozwolić na koszty utrzymania, których można uniknąć.
Kramii
6
@DavidArno, właśnie to robi, zadając to pytanie.
user56834

Odpowiedzi:

212
  1. W języku, który się kompiluje, wartość ciągu magicznego nie jest sprawdzana podczas kompilacji . Jeśli ciąg musi pasować do określonego wzorca, musisz uruchomić program, aby zagwarantować, że pasuje do tego wzorca. Jeśli użyłeś czegoś takiego jak wyliczenie, wartość jest co najmniej ważna w czasie kompilacji, nawet jeśli może to być niepoprawna wartość.

  2. Jeśli magiczny ciąg jest zapisywany w wielu miejscach , musisz zmienić je wszystkie bez żadnego bezpieczeństwa (np. Błąd czasu kompilacji). Można temu przeciwdziałać, deklarując go tylko w jednym miejscu i ponownie wykorzystując zmienną.

  3. Literówki mogą stać się poważnymi błędami. Jeśli masz funkcję:

    func(string foo) {
        if (foo == "bar") {
            // do something
        }
    }
    

    i ktoś przypadkowo pisze:

    func("barr");
    

    Im gorzej, tym rzadszy lub bardziej złożony jest łańcuch, szczególnie jeśli masz programistów, którzy nie znają języka ojczystego projektu.

  4. Magiczne ciągi rzadko są samo-dokumentujące. Jeśli widzisz jeden ciąg, nie mówi to nic o tym, co jeszcze mógłby / powinien być. Prawdopodobnie będziesz musiał zajrzeć do implementacji, aby upewnić się, że wybrałeś odpowiedni ciąg.

    Tego rodzaju implementacja jest nieszczelna i wymaga zewnętrznej dokumentacji lub dostępu do kodu, aby zrozumieć, co należy napisać, zwłaszcza, że ​​musi być perfekcyjnie znakowy (jak w punkcie 3).

  5. Poza funkcjami „znajdowania łańcucha” w IDE istnieje niewielka liczba narzędzi, które obsługują wzorzec.

  6. Możesz przypadkowo użyć tego samego magicznego sznurka w dwóch miejscach, gdy tak naprawdę są to różne rzeczy, więc jeśli zrobiłeś Znajdź i zamień i zmieniłeś oba, jeden z nich mógłby się zepsuć, podczas gdy drugi działał.

Erdrik Ironrose
źródło
34
W odniesieniu do pierwszego argumentu: TypeScript to skompilowany język, który może sprawdzać literały łańcuchowe. To także unieważnia argument od drugiego do czwartego. Dlatego problemem nie są same łańcuchy, ale użycie typu, który dopuszcza zbyt wiele wartości. To samo rozumowanie można zastosować do używania magicznych liczb całkowitych do wyliczeń.
Yogu
11
Ponieważ nie mam doświadczenia z TypeScript, odłożę się do twojego osądu. Powiedziałbym wtedy, że problem stanowią niesprawdzone ciągi znaków (tak jak w przypadku wszystkich używanych przeze mnie języków).
Erdrik Ironrose,
23
@ Yogu Typescript nie zmieni nazwy wszystkich ciągów, jeśli zmienisz oczekiwany typ literału ciągu statycznego. Dostaniesz błędy czasu kompilacji, które pomogą Ci znaleźć je wszystkie, ale to tylko częściowa poprawa 2. Nie mówienie, że jest to coś mniej niż absolutnie niesamowite (ponieważ tak jest i uwielbiam tę funkcję), ale na pewno nie całkowicie wyeliminuj przewagę wyliczeń. W naszym projekcie, kiedy używać wyliczeń, a kiedy nie, pozostaje rodzajem pytania o otwartym stylu, którego nie jesteśmy pewni; oba podejścia mają irytujące i zalety.
KRyan
30
Jeden duży, który widziałem nie dla łańcuchów tak samo jak liczb, ale może się zdarzyć z łańcuchami, gdy masz dwie magiczne wartości o tej samej wartości. Potem jedna z nich się zmienia. Teraz przechodzisz przez kod zmieniający starą wartość na nową wartość, która działa sama, ale wykonujesz również pracę EXTRA, aby upewnić się, że nie zmieniasz niewłaściwych wartości. Dzięki stałym zmiennym nie tylko nie musisz przechodzić przez to ręcznie, ale nie martwisz się, że zmieniłeś niewłaściwą rzecz.
corsiKa
35
@Yogu Dalej argumentowałbym, że jeśli wartość literału łańcuchowego jest sprawdzana w czasie kompilacji, to przestaje być ciągiem magicznym . W tym momencie jest to zwykła wartość const / enum, która jest napisana w zabawny sposób. Biorąc pod uwagę tę perspektywę, faktycznie argumentowałbym, że twój komentarz faktycznie popiera punkty Erdrika, a nie je odrzuca.
GrandOpener,
89

Szczytem, ​​na który sięgnęły inne odpowiedzi, nie jest to, że „magiczne wartości” są złe, ale że powinny być:

  1. rozpoznawalne jako stałe;
  2. zdefiniowane tylko raz w ramach całej ich dziedziny zastosowania (jeżeli jest to możliwe z architektonicznego punktu widzenia);
  3. zdefiniowane razem, jeśli tworzą zestaw stałych, które są w jakiś sposób powiązane;
  4. zdefiniowane na odpowiednim poziomie ogólności w aplikacji, w której są używane; i
  5. zdefiniowane w taki sposób, aby ograniczyć ich użycie w nieodpowiednich kontekstach (np. podatne na sprawdzanie typu).

To, co zazwyczaj odróżnia dopuszczalne „stałe” od „magicznych wartości”, to naruszenie jednego lub więcej z tych reguł.

Dobrze użyte, stałe pozwalają nam po prostu wyrazić pewne aksjomaty naszego kodu.

Co prowadzi mnie do ostatecznego punktu, że nadmierne użycie stałych (a zatem nadmierna liczba założeń lub ograniczeń wyrażonych w kategoriach wartości), nawet jeśli w inny sposób spełnia powyższe kryteria (ale zwłaszcza jeśli odbiega od nich), może sugerować, że opracowane rozwiązanie nie jest wystarczająco ogólne lub dobrze ustrukturyzowane (i dlatego tak naprawdę nie mówimy już o zaletach i wadach stałych, ale o zaletach i wadach dobrze ustrukturyzowanego kodu).

Języki wysokiego poziomu zawierają konstrukcje wzorców w językach niższego poziomu, które musiałyby wykorzystywać stałe. Te same wzorce można również zastosować w języku wyższego poziomu, ale nie powinno tak być.

Ale może to być osąd eksperta oparty na wrażeniu wszystkich okoliczności i jak powinno wyglądać rozwiązanie, a to, w jaki sposób uzasadnienie tego osądu będzie zależeć w dużej mierze od kontekstu. Rzeczywiście, może to nie być uzasadnione żadną ogólną zasadą, z wyjątkiem stwierdzenia: „Jestem na tyle dorosły, że widziałem już tego rodzaju pracę, z którą jestem zaznajomiony, zrobiony lepiej”!

EDYCJA: zaakceptowałem jedną edycję, odrzuciłem inną, a teraz dokonałem własnej edycji, czy mogę teraz rozważyć styl formatowania i interpunkcji mojej listy reguł, który należy ustalić raz na zawsze haha!

Steve
źródło
2
Podoba mi się ta odpowiedź. W końcu „struct” (i każde inne zastrzeżone słowo) jest magicznym ciągiem kompilatora C. Są dla nich dobre i złe sposoby kodowania.
Alfred Armstrong,
6
Na przykład, jeśli ktoś zobaczy „X: = 898755167 * Z” w twoim kodzie, prawdopodobnie nie będzie wiedział, co to znaczy, a jeszcze mniej prawdopodobne, że dowie się, że jest źle. Ale jeśli zobaczą „Speed_of_Light: constant Integer: = 299792456”, ktoś to przeszuka i zasugeruje poprawną wartość (a może nawet lepszy typ danych).
WGroleau
26
Niektórzy ludzie całkowicie pomijają ten punkt i piszą COMMA = "," zamiast SEPARATOR = ",". Ten pierwszy nie czyni niczego jaśniejszym, natomiast drugi określa zamierzone użycie i pozwala zmienić separator później w jednym miejscu.
marcus
1
@marcus, rzeczywiście! Oczywiście istnieje możliwość użycia prostych wartości literalnych w miejscu - na przykład, jeśli metoda dzieli wartość przez dwa, pisanie może być łatwiejsze i prostsze value / 2, niż w value / VALUE_DIVISORprzypadku tej ostatniej zdefiniowanej jak 2gdzie indziej. Jeśli zamierzałeś uogólnić metodę obsługującą CSV, prawdopodobnie chciałbyś, aby separator został przekazany jako parametr, a nie zdefiniowany jako stała. Ale to wszystko jest kwestia oceny w kontekście - przykład @ WGroleau na to SPEED_OF_LIGHTjest coś, co chciałbyś wyraźnie nazwać, ale nie każda literalna tego potrzebuje.
Steve
4
Najlepsza odpowiedź jest lepsza od tej, jeśli potrzebujesz przekonania, że ​​magiczne sznurki są „złą rzeczą”. Ta odpowiedź jest lepsza, jeśli wiesz i akceptujesz, że są one „złą rzeczą” i musisz znaleźć najlepszy sposób zaspokojenia potrzeb, którym służą w sposób możliwy do utrzymania.
corsiKa
34
  • Trudno je śledzić.
  • Zmiana wszystkich może wymagać zmiany wielu plików w możliwie wielu projektach (trudne do utrzymania).
  • Czasami trudno jest powiedzieć, jaki jest ich cel, po prostu patrząc na ich wartość.
  • Bez ponownego użycia.
Jason
źródło
4
Co oznacza „brak ponownego użycia”?
pa pa
7
Zamiast tworzyć jedną zmienną / stałą itp. I ponownie używać jej w całym projekcie / kodzie, tworzysz nowy ciąg w każdym, co powoduje niepotrzebne powielanie.
jason
Więc punkty 2 i 4 są takie same?
Thomas
4
@ThomasMoors Nie. Mówi o tym, w jaki sposób musisz zbudować nowy ciąg za każdym razem, gdy chcesz użyć już istniejącego magicznego ciągu, punkt 2 dotyczy zmiany samego łańcucha
Pierre Arlaud,
25

Przykład z życia: Pracuję z systemem innej firmy, w którym „byty” są przechowywane z „polami”. Zasadniczo system EAV . Ponieważ dodanie kolejnego pola jest dość łatwe, dostęp do niego uzyskuje się za pomocą nazwy pola jako ciągu:

Field nameField = myEntity.GetField("ProductName");

(zwróć uwagę na magiczny ciąg „ProductName”)

Może to prowadzić do kilku problemów:

  • Muszę odwołać się do zewnętrznej dokumentacji, aby wiedzieć, że „nazwa produktu” nawet istnieje i jej dokładna pisownia
  • Ponadto muszę odwołać się do tego dokumentu, aby zobaczyć, jaki jest typ danych tego pola.
  • Literówki w tym magicznym ciągu nie zostaną złapane, dopóki ten wiersz kodu nie zostanie wykonany.
  • Gdy ktoś decyduje się zmienić nazwę tego pola na serwerze (trudne, ale zapobiega utracie danych, ale nie niemożliwe), nie mogę łatwo przeszukać mojego kodu, aby zobaczyć, gdzie powinienem zmienić tę nazwę.

Więc moim rozwiązaniem było wygenerowanie stałych dla tych nazw, uporządkowanych według typu encji. Więc teraz mogę użyć:

Field nameField = myEntity.GetField(Model.Product.ProductName);

Nadal jest ciągiem stałym i kompiluje się dokładnie do tego samego pliku binarnego, ale ma kilka zalet:

  • Po wpisaniu „Modelu” moje IDE pokazuje tylko dostępne typy jednostek, dzięki czemu mogę łatwo wybrać „Produkt”.
  • Następnie moje IDE dostarcza tylko nazwy pól, które są dostępne dla tego typu encji, również do wyboru.
  • Automatycznie wygenerowana dokumentacja pokazuje, co oznacza to pole oraz typ danych używany do przechowywania jego wartości.
  • Zaczynając od stałej, moje IDE może znaleźć wszystkie miejsca, w których używana jest ta stała (w przeciwieństwie do jej wartości)
  • Literówki zostaną przechwycone przez kompilator. Dotyczy to również sytuacji, gdy nowy model (ewentualnie po zmianie nazwy lub usunięciu pola) jest używany do regeneracji stałych.

Dalej na mojej liście: ukryj te stałe za wygenerowanymi silnie typowanymi klasami - wtedy również typ danych jest zabezpieczony.

Hans Keinging
źródło
+1
dajesz
Jeśli niektóre części twojego typu jednostki są wystarczająco statyczne, że warto zdefiniować stałą nazwę dla tego, co jest warte, myślę, że bardziej odpowiednie byłoby po prostu zdefiniowanie odpowiedniego modelu danych, abyś mógł to zrobić nameField = myEntity.ProductName;.
Lie Ryan,
@LieRyan - znacznie łatwiej było generować stałe i uaktualniać istniejące projekty, aby z nich korzystać. Powiedział, że mam pracę na generowaniu rodzajów statyczne, więc mogę zrobić dokładnie, że
Hans Ke st ing
9

Magiczne łańcuchy nie zawsze są złe , więc może to być powód, dla którego nie możesz znaleźć ogólnego powodu do ich uniknięcia. (Przez „magiczny ciąg” zakładam, że masz na myśli dosłowność łańcucha jako część wyrażenia, a nie jako stałą.)

W niektórych szczególnych przypadkach należy unikać magicznych ciągów:

  • Ten sam ciąg pojawia się wielokrotnie w kodzie. Oznacza to, że możesz mieć błąd pisowni w jednym z miejsc. I będzie kłopot ze zmianami łańcucha. Zamień ciąg w stałą, a unikniesz tego problemu.
  • Ciąg może się zmieniać niezależnie od kodu, w którym się pojawia. Na przykład. jeśli ciąg zostanie wyświetlony użytkownikowi końcowemu, prawdopodobnie zmieni się niezależnie od jakiejkolwiek zmiany logiki. Rozdzielenie takiego ciągu na osobny moduł (lub zewnętrzną konfigurację lub bazę danych) ułatwi niezależną zmianę
  • Znaczenie ciągu nie jest oczywiste z kontekstu. W takim przypadku wprowadzenie stałej ułatwi zrozumienie kodu.

Ale w niektórych przypadkach „magiczne sznurki” są w porządku. Załóżmy, że masz prosty analizator składni:

switch (token.Text) {
  case "+":
    return a + b;
  case "-":
    return a - b;
  //etc.
}

Naprawdę nie ma tutaj magii i żaden z wyżej opisanych problemów nie ma zastosowania. IMHO nie miałoby żadnych korzyści string Plus="+"itp. Uprość to.

JacquesB
źródło
7
Myślę, że twoja definicja „magicznego sznurka” jest niewystarczająca, musi mieć pewną koncepcję ukrywania / zasłaniania / tworzenia tajemnic. Nie nazwałbym „+” i „-” w tym kontrprzykładzie „magią”, podobnie jak nie znałbym zera jako magii if (dx != 0) { grad = dy/dx; }.
Rupe
2
@Rupe: Zgadzam się, ale OP używa definicji „ wartości ciągów, które są określone bezpośrednio w kodzie aplikacji, które mają wpływ na zachowanie aplikacji. ”, Która nie wymaga, aby ciąg był tajemniczy, więc tej definicji używam w odpowiedź.
JacquesB
7
Nawiązując do swojej przykład, widziałem oświadczenia przełączników, które zastąpiły "+"i "-"z TOKEN_PLUSa TOKEN_MINUS. Za każdym razem, gdy go czytam, miałem wrażenie, że trudniej go czytać i debugować! Zdecydowanie miejsce, w którym zgadzam się, że używanie prostych ciągów jest lepsze.
Cort Ammon
2
Zgadzam się, że są chwile, w których magiczne sznurki są odpowiednie: unikanie ich jest ogólną zasadą, a wszystkie reguły mają wyjątki. Mamy nadzieję, że kiedy wyjaśnimy, dlaczego mogą być złe, będziemy mogli dokonywać inteligentnych wyborów, zamiast robić rzeczy, ponieważ (1) nigdy nie zrozumieliśmy, że może być lepszy sposób lub (2) powiedziano mi, że powinien robić różne rzeczy inaczej przez starszego programistę lub standard kodowania.
Kramii
2
Nie wiem, co to jest „magia” tutaj. Dla mnie wyglądają jak podstawowe literały łańcuchowe.
tchrist
6

Aby dodać do istniejących odpowiedzi:

Internacjonalizacja (i18n)

Jeśli tekst do wyświetlenia na ekranie jest zakodowany na stałe i zakopany w warstwach funkcji, bardzo trudno będzie ci przetłumaczyć ten tekst na inne języki.

Niektóre środowiska programistyczne (np. Qt) obsługują tłumaczenia poprzez wyszukiwanie z ciągu tekstowego w języku podstawowym na przetłumaczony język. Magiczne ciągi zwykle mogą to przetrwać - dopóki nie zdecydujesz, że chcesz użyć tego samego tekstu w innym miejscu i dostać literówkę. Nawet wtedy bardzo trudno jest znaleźć, które ciągi magiczne wymagają tłumaczenia, jeśli chcesz dodać obsługę innego języka.

Niektóre środowiska programistyczne (np. MS Visual Studio) przyjmują inne podejście i wymagają przechowywania wszystkich przetłumaczonych ciągów w bazie danych zasobów i odczytywania bieżących ustawień narodowych przez unikalny identyfikator tego ciągu. W takim przypadku aplikacja z ciągami magicznymi po prostu nie może zostać przetłumaczona na inny język bez większych przeróbek. Efektywne programowanie wymaga wprowadzenia wszystkich ciągów tekstowych do bazy danych zasobów i nadania unikalnego identyfikatora podczas pierwszego pisania kodu, a następnie i18n jest stosunkowo łatwe. Próba wypełnienia tego po fakcie zazwyczaj wymaga bardzo dużego wysiłku (i tak, byłem tam!), Więc o wiele lepiej jest zrobić wszystko dobrze.

Graham
źródło
3

Nie jest to priorytetem dla wszystkich, ale jeśli kiedykolwiek chcesz mieć możliwość automatycznego obliczania wskaźników sprzężenia / kohezji w kodzie, magiczne łańcuchy sprawiają, że jest to prawie niemożliwe. Łańcuch w jednym miejscu będzie odnosił się do klasy, metody lub funkcji w innym miejscu i nie ma łatwego, automatycznego sposobu na określenie, czy łańcuch jest sprzężony z klasą / metodą / funkcją tylko przez parsowanie kodu. Tylko podstawowa struktura (np. Angular) może ustalić, że istnieje powiązanie - i może to zrobić tylko w czasie wykonywania. Aby samodzielnie uzyskać informacje o sprzęganiu, twój parser musiałby wiedzieć wszystko o używanym frameworku, wykraczającym poza podstawowy język, w którym kodujesz.

Ale znowu, nie jest to coś, na czym zależy wielu programistom.

użytkownik3511585
źródło