Що не так з магічними струнами?


164

Як досвідчений розробник програмного забезпечення, я навчився уникати магічних рядків.

Моя проблема полягає в тому, що це так давно, як я їх використовував, я забув більшість причин, чому. Як результат, у мене виникають труднощі пояснити, чому вони проблеми моїм менш досвідченим колегам.

Які об’єктивні причини для їх уникнення? Які проблеми вони викликають?


38
Що таке чарівна струна? Те саме, що і магічні числа ?
Laiv

14
@Laiv: Вони схожі на магічні числа, так. Мені подобається визначення на deviq.com/magic-strings : "Магічні рядки - це рядкові значення, які задаються безпосередньо в коді програми, які впливають на поведінку програми." (Визначення на сайті en.wikipedia.org/wiki/Magic_string зовсім не те, що я маю на увазі)
Крамій

17
це смішно, я навчився ганьбити ... пізніше Які аргументи я можу використати, щоб переконати своїх юніорів ... Історія ніколи не закінчується :-). Я б не намагався "переконати", я б швидше дозволяв навчатися самостійно. Ніщо не триває більше ніж урок / ідея, досягнута власним досвідом. Те, що ви намагаєтеся зробити, - це непідвищення . Не робіть цього, якщо не хочете команди Леммінга.
Laiv

15
@Laiv: Я хотів би дозволити людям вчитися на власному досвіді, але, на жаль, це не є для мене варіантом. Я працюю в лікарні, що фінансується державою, де непомітні помилки можуть поставити під загрозу догляд за пацієнтами, і де ми не можемо дозволити собі уникнути витрат на обслуговування.
Крамій

6
@DavidArno, саме це він робить, задаючи це запитання.
користувач56834

Відповіді:


212
  1. У мові, що компілюється, значення магічної рядки не перевіряється під час компіляції . Якщо рядок повинен відповідати певному шаблону, вам потрібно запустити програму, щоб гарантувати, що вона відповідає цьому шаблону. Якщо ви використовували щось таке, як enum, значення принаймні є дійсним під час компіляції, навіть якщо воно може бути неправильним.

  2. Якщо чарівний рядок пишеться в декількох місцях, вам потрібно змінити їх без будь-якої безпеки (наприклад, помилка під час компіляції). Цьому можна протидіяти, лише оголосивши його в одному місці та повторно використовуючи змінну.

  3. Опечатки можуть стати серйозними помилками. Якщо у вас є функція:

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

    а хтось випадково набирає:

    func("barr");
    

    Це гірше, чим рідше або складніший рядок, особливо якщо у вас є програмісти, незнайомі з рідною мовою проекту.

  4. Чарівні струни рідко самодокументуються. Якщо ви бачите один рядок, це нічого не говорить про те, що ще може бути / повинно бути. Можливо, вам доведеться заглянути в реалізацію, щоб переконатися, що ви вибрали правильний рядок.

    Така реалізація є герметичною , для розуміння того, що слід писати, потрібна або зовнішня документація, або доступ до коду, тим більше, що він повинен бути ідеальним для символів (як у пункті 3).

  5. Окрім функцій "знайти рядок" в IDE, існує невелика кількість інструментів, які підтримують шаблон.

  6. Ви можете випадково використовувати одну і ту ж магічну струну в двох місцях, коли вони справді різні речі, тож якщо ви зробили функцію "Знайти і замінити" і змінили обидва, одна з них може зламатися, а інша працює.


34
Щодо першого аргументу: TypeScript - це компільована мова, яка може перевірити рядкові літерали. Це також визнає недійсним аргумент два-чотири. Тому проблема не є самими рядками, а використанням типу, який дозволяє занадто багато значень. Це ж міркування можна застосувати і для використання магічних цілих чисел для перерахунку.
Йогу

11
Оскільки у мене немає досвіду роботи з TypeScript, я відкладу ваше судження там. Я б сказав тоді, що проблема є неперевіреними рядками (як це стосується всіх мов, якими я користувався).
Ердрік Айронроз

23
@Yogu Typescript не перейменовує всі ваші рядки для вас, якщо ви зміните тип статичного рядка, який ви очікуєте. Ви отримаєте помилки компіляції часу, які допоможуть вам знайти їх усі, але це лише часткове поліпшення на 2. Не кажучи, що це щось менше, ніж абсолютно дивовижне (адже це те, і я люблю цю функцію), але це, безумовно, не так відверто усувають перевагу перерахунків. У нашому проекті, коли використовувати переписки, а коли не залишатись питанням відкритого стилю, в якому ми не впевнені; обидва підходи мають роздратування і переваги.
KRyan

30
Одне велике, що я бачив не для рядків стільки, скільки цифр, але може трапитися з рядками, це коли у вас є два магічні значення з однаковим значенням. Потім одна з них змінюється. Тепер ви переглядаєте код, змінюючи старе значення на нове значення, яке є самостійною роботою, але ви також виконуєте роботу EXTRA, щоб переконатися, що ви не змінюєте неправильних значень. Із постійними змінними не тільки вам не доведеться переглядати це вручну, але і ви не переживаєте, що ви змінили неправильну річ.
corsiKa

35
@Yogu Я б також стверджував, що якщо значення літерального рядка перевіряється під час компіляції, воно перестає бути магічним рядком . У цей момент це просто нормальне значення const / enum, яке, можливо, записується смішно. Враховуючи цю точку зору, я б фактично стверджував, що ваш коментар насправді підтримує точки Ердрика, а не спростовуючи їх.
GrandOpener

89

Вершина того, що інші відповіді зрозуміли, полягає не в тому, що "магічні значення" погані, а в тому, що вони повинні бути:

  1. визначені визнано як константи;
  2. визначено лише один раз у межах усієї сфери їх використання (якщо можливо архітектурно);
  3. визначені разом, якщо вони утворюють набір констант, які якимось чином пов'язані між собою;
  4. визначені на відповідному рівні спільності в додатку, в якому вони використовуються; і
  5. визначені таким чином, щоб обмежити їх використання у невідповідних контекстах (наприклад, піддаються перевірці типу).

Те, що зазвичай відрізняє прийнятні "константи" від "магічних значень", є деяким порушенням одного або декількох із цих правил.

Добре використовувані константи дозволяють нам висловити певні аксіоми нашого коду.

Що підводить мене до остаточного моменту, що надмірне використання констант (а отже, надмірна кількість припущень чи обмежень, виражених у значеннях), навіть якщо воно інакше відповідає вищезазначеним критеріям (але особливо, якщо воно відхиляється від них), може означати, що розроблене рішення не є достатньо загальним чи добре структурованим (і тому ми вже не говоримо вже про плюси і мінуси констант, а про плюси і мінуси добре структурованого коду).

Мови високого рівня мають конструкції для моделей мов нижчого рівня, які повинні використовувати константи. Ті ж зразки можуть бути використані і в мові вищого рівня, але цього не повинно бути.

Але це може бути експертне судження, яке базується на враженні всіх обставин, і як має виглядати рішення, і як саме це рішення буде виправдане, буде сильно залежати від контексту. Дійсно, це не може бути виправданим з точки зору будь-якого загального принципу, за винятком твердження "Я досить старий, щоб уже бачив таку роботу, з якою я знайомий, зробив краще"!

EDIT: прийнявши одну редакцію, відхилив іншу та виконавши власну редагування, чи можу я тепер вважати стиль форматування та пунктуації мого списку правил врегульованим раз і назавжди ха-ха!


2
Мені подобається ця відповідь. Зрештою, "struct" (і кожне інше зарезервоване слово) - це чарівний рядок для компілятора C. Існують хороші та погані способи кодування для них.
Альфред Армстронг

6
Наприклад, якщо хтось бачить "X: = 898755167 * Z" у вашому коді, він, ймовірно, не дізнається, що це означає, і ще менше шансів дізнатися, що це неправильно. Але якщо вони побачать "Speed_of_Light: постійний цілий число: = 299792456", хтось перегляне його і підкаже правильне значення (а може бути, навіть кращий тип даних).
WGroleau

26
Деякі люди повністю пропускають суть і пишуть COMMA = "," замість SEPARATOR = ",". Перший не робить нічого зрозумілішим, тоді як другий констатує передбачуване використання та дозволяє змінити роздільник пізніше в одному місці.
березня

1
@marcus, справді! Звичайно, існує випадок використання простих буквальних значень на місці - наприклад, якщо метод розділяє значення на два, його можна зрозуміліше і простіше просто записати value / 2, а не value / VALUE_DIVISORз останнім, визначеним як в 2іншому місці. Якщо ви мали намір узагальнити метод обробки CSV-файлів, ви, мабуть, хочете, щоб роздільник передався як параметр, а не був визначений як константа взагалі. Але все це питання судження в контексті - приклад @ WGroleau - SPEED_OF_LIGHTце те, що ви хотіли б чітко назвати, але не кожен буквальний це потребує.
Стів

4
Верхня відповідь краще, ніж ця відповідь, якщо потрібно переконати, що магічні рядки - це «погана річ». Ця відповідь краще, якщо ви знаєте і приймаєте, що вони "погана річ" і вам потрібно знайти найкращий спосіб задовольнити потреби, які вони обслуговують в ремонтопридатному вигляді.
corsiKa

34
  • Їх важко відстежити.
  • Змінення всіх може зажадати зміни декількох файлів у можливо декількох проектах (важко підтримувати).
  • Іноді важко сказати, яка їх мета, просто переглянувши їхню цінність.
  • Без повторного використання.

4
Що означає "без повторного використання"?
до побачення

7
Замість створення однієї змінної / константи тощо та повторного використання її у всьому проекті / коді ви створюєте нову рядок у кожній, що спричиняє непотрібне дублювання.
Ясон

Тож точки 2 і 4 однакові?
Томас

4
@ThomasMoors Ні, він не говорить про те, як вам доводиться створювати нову струну кожного разу, коли ви хочете використовувати вже наявну магічну струну, пункт 2 - про зміну самої струни
Pierre Arlaud

25

Приклад із реального життя: я працюю із системою третьої сторони, де "сутності" зберігаються з "полями". В основному система EAV . Оскільки додати інше поле досить просто, ви отримуєте доступ до нього, використовуючи ім'я поля як рядок:

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

(зверніть увагу на чарівний рядок "ProductName")

Це може призвести до кількох проблем:

  • Мені потрібно звернутися до зовнішньої документації, щоб знати, що "ProductName" навіть існує і його точний написання
  • Плюс мені потрібно звернутися до цього документа, щоб побачити тип даних цього поля.
  • Друкарські помилки в цій магічній рядку не потраплять, поки цей рядок коду не буде виконаний.
  • Коли хтось вирішить перейменувати це поле на сервері (важко, запобігаючи втраті даних, але не неможливо), я не можу легко шукати свій код, щоб побачити, де мені слід змінити це ім'я.

Тож моїм рішенням для цього було створення констант для цих імен, організованих за типом сутності. Тож тепер я можу використовувати:

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

Він все ще є констатою рядка і компілюється в точно той же бінарний, але має ряд переваг:

  • Після того, як я набрав "Модель", мій IDE показує лише доступні типи об'єктів, тому я можу легко вибрати "Продукт".
  • Тоді мій IDE постачає лише імена полів, доступних для цього типу сутності, також вибір.
  • Автоматично створена документація показує, яке значення має це поле плюс тип даних, який використовується для зберігання його значень.
  • Починаючи з константи, мій IDE може знайти всі місця, де використовується ця точна константа (на відміну від її значення)
  • Друкарські помилки будуть спіймані компілятором. Це також застосовується, коли для регенерування констант використовується нова модель (можливо, після перейменування або видалення поля).

Далі в моєму списку: схойте ці константи за генерованими сильно набраними класами - тоді також тип даних захищений.


+1 ви пропонуєте багато хороших моментів, не обмежених структурою коду: підтримка та інструменти IDE, які можуть стати рятівниками у великих проектах
kmdreko

Якщо деякі частини типу вашої сутності є досить статичними, що насправді визначати постійну назву для цього варто, я думаю, було б більш придатним просто визначити для неї належну модель даних, щоб ви могли просто зробити nameField = myEntity.ProductName;.
Лежати Райан

@LieRyan - набагато простіше було генерувати звичайні константи та модернізувати існуючі проекти, щоб їх використовувати. Те є , я маю працювати на генерації статичних типів , так що я можу зробити точно , що
Hans Ke вул ІНГ

9

Чарівні струни не завжди погані , тому це може стати причиною того, що ви не можете придумати ковдру, щоб уникнути їх. (Під "магічним рядком" я припускаю, що ви маєте на увазі рядок буквально як частина виразу, а не визначається як константа.)

У деяких конкретних випадках слід уникати чарівних струн:

  • Один і той же рядок з'являється кілька разів у коді. Це означає, що ви можете мати орфографічну помилку в одному з місць. І це матиме клопоту про зміну рядків. Перетворіть рядок у постійну, і ви уникнете цього питання.
  • Рядок може змінюватися незалежно від коду, де він відображається. Напр. якщо рядок є текстом, який відображається кінцевому користувачеві, він, ймовірно, зміниться незалежно від будь-якої зміни логіки. Якщо відокремити такий рядок в окремий модуль (або зовнішню конфігурацію чи базу даних), це полегшить самостійну зміну
  • Значення рядка не очевидно з контексту. У такому випадку введення константи полегшить розуміння коду.

Але в деяких випадках «чарівні струни» чудово. Скажіть, у вас простий парсер:

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

Тут справді немає ніякої магії, і жодна з описаних вище проблем не стосується. Не було б ніякої користі IMHO визначати string Plus="+"і т. Д. Нехай це буде просто.


7
Я думаю, що ваше визначення "магічної струни" є недостатнім, вона повинна мати якесь поняття про приховування / затемнення / створення загадкового. Я б не називав "+" і "-" в цьому зустрічному прикладі як "магія", більше, ніж я б називав нуль як магією if (dx != 0) { grad = dy/dx; }.
Рупій

2
@Rupe: Я погоджуюся, але ОП використовує визначення " рядкові значення, які задаються безпосередньо в коді програми, які впливають на поведінку програми. " відповідь.
ЖакБ

7
Посилаючись на ваш приклад, я бачив заяви переключення, які замінили "+"і "-"на, TOKEN_PLUSі на TOKEN_MINUS. Кожного разу, коли я читав це, я відчував, що через нього важче читати та налагоджувати! Однозначно місце, де я згоден, що краще використовувати прості рядки.
Корт Аммон

2
Я погоджуюся, що бувають випадки, коли магічні рядки доречні: уникати їх - це правило, і всі правила великого пальця мають винятки. Сподіваємось, коли нам буде зрозуміло, чому вони можуть бути поганими, ми зможемо зробити розумний вибір, а не робити речі, тому що (1) ми ніколи не розуміли, що може бути кращий спосіб, або (2) Старший розробник або стандарт кодування мені сказали робити щось інакше.
Крамій

2
Я не знаю, що тут "магія". Вони мені схожі на базові рядкові літерали.
tchrist

6

Щоб додати до існуючих відповідей:

Інтернаціоналізація (i18n)

Якщо текст, який відображатиметься на екрані, жорстко закодований і закопаний у шари функцій, вам буде дуже важко забезпечити переклад цього тексту на інші мови.

Деякі середовища розробки (наприклад, Qt) обробляють переклади шляхом пошуку з текстового рядка базової мови на перекладену мову. Чарівні струни зазвичай можуть пережити це - до тих пір, поки ви не вирішите, що хочете використовувати той самий текст в іншому місці і не отримаєте помилку. Навіть тоді дуже важко знайти, які магічні рядки потребують перекладу, коли ви хочете додати підтримку іншої мови.

Деякі середовища розробки (наприклад, MS Visual Studio) використовують інший підхід і вимагають, щоб усі перекладені рядки містилися в базі даних ресурсів і зчитувались для поточного локалу за допомогою унікального ідентифікатора цього рядка. У цьому випадку ваш додаток із магічними рядками просто не може бути перекладений на іншу мову без великих переробок. Ефективна розробка вимагає введення всіх текстових рядків у базу даних ресурсів та надання унікального ідентифікатора при першому написанні коду, а i18n після цього порівняно легко. Спроба засипати це після факту зазвичай вимагає дуже великих зусиль (і так, я там був!), Тому набагато краще робити речі правильно в першу чергу.


3

Це не є пріоритетним завданням для всіх, але якщо ви хочете коли-небудь мати можливість обчислювати показники з'єднання / згуртованості свого коду в автоматизованому вигляді, магічні рядки роблять це майже неможливим. Рядок в одному місці буде стосуватися класу, методу або функції в іншому місці, і немає простого, автоматичного способу визначити, що рядок поєднується з класом / методом / функцією просто шляхом аналізу коду. Тільки базовий каркас (кутовий, наприклад) може визначити наявність зв'язку - і це можна зробити лише під час виконання. Щоб отримати інформацію про з'єднання самостійно, вашому аналізатору необхідно було б знати все про рамки, якими ви користуєтесь, і за межі основної мови, в якій ви кодуєте.

Але знову ж таки, це не те, про що піклуються розробники.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.