Что не так с волшебными струнами?

164

Как опытный разработчик программного обеспечения, я научился избегать волшебных струн.

Моя проблема в том, что с тех пор, как я их использовал, прошло так много времени, что я забыл большинство причин. В результате у меня возникают проблемы с объяснением, почему они являются проблемой для моих менее опытных коллег.

Какие объективные причины существуют, чтобы их избегать? Какие проблемы они вызывают?

Kramii
источник
38
Что за волшебная нить? То же самое, что магические числа ?
Laiv
14
@Laiv: Да, они похожи на магические числа. Мне нравится определение на deviq.com/magic-strings : «Магические строки - это строковые значения, которые указываются непосредственно в коде приложения и влияют на поведение приложения». (Определение на en.wikipedia.org/wiki/Magic_string совсем не то, что я имею в виду)
Kramii
17
это забавно, я научился ненавидеть ... позже Какие аргументы я могу использовать, чтобы убедить своих юниоров ... История никогда не кончается :-). Я бы не пытался «убедить», я бы лучше научился самостоятельно. Ничто не длится дольше, чем урок / идея, достигнутая вашим собственным опытом. То, что вы пытаетесь сделать, это внушать . Не делайте этого, если вам не нужна команда Леммингов.
января
15
@Laiv: Я бы хотел, чтобы люди учились на собственном опыте, но, к сожалению, это не вариант для меня. Я работаю в финансируемой государством больнице, где незначительные ошибки могут поставить под угрозу обслуживание пациентов, и где мы не можем позволить себе избежать затрат на техническое обслуживание.
Kramii
6
@DavidArno, именно это он и делает, задавая этот вопрос.
user56834

Ответы:

211
  1. В языке, который компилируется, значение магической строки не проверяется во время компиляции . Если строка должна соответствовать определенному шаблону, вы должны запустить программу, чтобы гарантировать, что она соответствует этому шаблону. Если вы использовали что-то, например, перечисление, значение по крайней мере допустимо во время компиляции, даже если это может быть неправильное значение.

  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. Не говоря уже о том, что это совсем не удивительно (потому что это так, и мне нравится эта функция), но это определенно не прямо исключить преимущество перечислений. В нашем проекте, когда использовать enums, а когда нет, остается вопрос открытого стиля, в котором мы не уверены; Оба подхода имеют свои недостатки и преимущества.
KRyan
30
Одна большая проблема, которую я видел не для строк, а для чисел, но может случиться со строками, это когда у вас есть два магических значения с одинаковым значением. Затем один из них меняется. Теперь вы просматриваете код, изменяющий старое значение на новое, что само по себе является работой, но вы также выполняете ДОПОЛНИТЕЛЬНУЮ работу, чтобы убедиться, что вы не меняете неправильные значения. С постоянными переменными вам не только не нужно проходить их вручную, но и не беспокоиться, что вы изменили не то, что нужно.
CorsiKa
35
@Yogu Я бы также сказал, что если во время компиляции проверяется значение строкового литерала, он перестает быть волшебной строкой . В этот момент это просто обычное значение const / enum, которое пишется забавным образом. Учитывая эту точку зрения, я бы на самом деле утверждал, что ваш комментарий на самом деле поддерживает точку зрения Эрдрика, а не опровергает их.
GrandOpener
89

Вершина того, за что ухватились другие ответы, состоит не в том, что «магические ценности» плохие, а в том, что они должны быть:

  1. определяется узнаваемо как константа;
  2. определяется только один раз во всей области их использования (если это возможно с точки зрения архитектуры);
  3. определены вместе, если они образуют набор констант, которые так или иначе связаны между собой;
  4. определяется на соответствующем уровне общности в приложении, в котором они используются; а также
  5. определяется таким образом, чтобы ограничить их использование в неподходящих контекстах (например, поддается проверке типов).

Что обычно отличает приемлемые «константы» от «магических ценностей», так это некоторое нарушение одного или нескольких из этих правил.

При правильном использовании константы просто позволяют нам выражать определенные аксиомы нашего кода.

Что подводит меня к окончательному выводу, что чрезмерное использование констант (и, следовательно, чрезмерное количество допущений или ограничений, выраженных в терминах значений), даже если оно иным образом соответствует вышеуказанным критериям (но особенно, если оно отклоняется от них), Это может означать, что разрабатываемое решение не является достаточно общим или хорошо структурированным (и поэтому мы больше не говорим о плюсах и минусах констант, а о плюсах и минусах хорошо структурированного кода).

Языки высокого уровня имеют конструкции для шаблонов на языках низкого уровня, которые должны использовать константы. Те же шаблоны можно использовать и в языке более высокого уровня, но этого не следует делать.

Но это может быть экспертное суждение, основанное на впечатлении всех обстоятельств и того, как должно выглядеть решение, и то, как именно это суждение будет оправдано, будет сильно зависеть от контекста. В самом деле, это может быть неоправданным с точки зрения какого-либо общего принципа, кроме как утверждать: «Я достаточно взрослый, чтобы уже видеть такую ​​работу, с которой я знаком, лучше»!

РЕДАКТИРОВАТЬ: приняв одно редактирование, отклонив другое и теперь выполнив свое собственное редактирование, могу ли я теперь считать, что стиль форматирования и пунктуации в моем списке правил должен быть установлен раз и навсегда, ха-ха!

Стив
источник
2
Мне нравится этот ответ. В конце концов, "struct" (и любое другое зарезервированное слово) является магической строкой для компилятора C. Есть хорошие и плохие способы их кодирования.
Альфред Армстронг
6
Например, если кто-то увидит «X: = 898755167 * Z» в вашем коде, он, вероятно, не будет знать, что это значит, и еще менее вероятно, узнает, что это неправильно. Но если они увидят «Speed_of_Light: constant Integer: = 299792456», кто-нибудь найдет его и предложит правильное значение (и, возможно, даже лучший тип данных).
WGroleau
26
Некоторые люди полностью упускают суть и пишут COMMA = "," вместо SEPARATOR = ",". Первое не делает ничего более понятным, в то время как второе устанавливает предполагаемое использование и позволяет вам изменить разделитель позже в одном месте.
Marcus
1
@ Маркус, действительно! Конечно, есть случай для использования простых литеральных значений на месте - например, если метод делит значение на два, проще написать и проще написать 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 - было гораздо проще генерировать простые константы и обновлять существующие проекты для их использования. Тем не менее, я работаю над созданием статических типов, поэтому я могу сделать именно это
Ганс Кеинг
9

Волшебные струны не всегда плохи , поэтому это может быть причиной, по которой вы не можете придумать общую причину, чтобы избегать их. (Под «волшебной строкой» я предполагаю, что вы подразумеваете строковый литерал как часть выражения, а не как константу.)

В некоторых особых случаях следует избегать волшебных строк:

  • Одна и та же строка появляется несколько раз в коде. Это означает, что вы можете иметь орфографическую ошибку в одном из мест. И это будет хлопот с изменениями строки. Превратите строку в константу, и вы избежите этой проблемы.
  • Строка может изменяться независимо от кода, в котором она появляется. Например. если строка является текстом, отображаемым для конечного пользователя, она, вероятно, изменится независимо от логического изменения. Разделение такой строки на отдельный модуль (или внешнюю конфигурацию или базу данных) облегчит самостоятельное изменение
  • Смысл строки не очевиден из контекста. В этом случае введение константы облегчит понимание кода.

Но в некоторых случаях «волшебные струны» хороши. Скажем, у вас есть простой парсер:

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

Здесь действительно нет волшебства, и ни одна из вышеописанных проблем не применима. ИМХО не было бы никакой пользы, чтобы определить string Plus="+"и т. Д. Держите это просто.

JacquesB
источник
7
Я думаю, что ваше определение «волшебной струны» является недостаточным, оно должно иметь некоторую концепцию сокрытия / затемнения / создания загадочного. Я бы не назвал «+» и «-» в этом контрпримере «магией», равно как и не назвал бы ноль магией в if (dx != 0) { grad = dy/dx; }.
рупия
2
@Rupe: Я согласен, но OP использует определение « строковые значения, которые указываются непосредственно в коде приложения, которые влияют на поведение приложения. », Которое не требует, чтобы строка была загадочной, поэтому это определение я использую в ответ.
JacquesB
7
Со ссылкой на ваш пример я видел операторы switch, которые заменяли "+"и "-"на TOKEN_PLUSи TOKEN_MINUS. Каждый раз, когда я читал это, я чувствовал, что из-за этого было труднее читать и отлаживать! Определенно место, где я согласен, что использование простых строк лучше.
Cort Ammon
2
Я согласен, что бывают моменты, когда уместны магические строки: избегать их - это правило большого пальца, и все правила большого пальца имеют исключения. Надеемся, что когда нам станет ясно, почему они могут быть плохими, мы сможем сделать разумный выбор, а не делать что-то, потому что (1) мы никогда не понимали, что может быть лучший способ, или (2) мы старшим разработчиком или стандартом кодирования велено поступать иначе.
Kramii
2
Я не знаю, что здесь "волшебство". Они выглядят как основные строковые литералы для меня.
tchrist
6

Чтобы добавить к существующим ответам:

Интернационализация (i18n)

Если текст, отображаемый на экране, жестко запрограммирован и скрыт внутри слоев функций, вам будет очень сложно обеспечить перевод этого текста на другие языки.

Некоторые среды разработки (например, Qt) обрабатывают переводы путем поиска из текстовой строки базового языка в переведенный язык. Волшебные струны, как правило, могут пережить это - пока вы не решите, что хотите использовать тот же текст в другом месте и получить опечатку. Даже тогда очень сложно найти, какие магические строки нужно перевести, если вы хотите добавить поддержку другого языка.

Некоторые среды разработки (например, MS Visual Studio) используют другой подход и требуют, чтобы все переведенные строки содержались в базе данных ресурсов и считывались обратно для текущей локали по уникальному идентификатору этой строки. В этом случае ваше приложение с волшебными строками просто не может быть переведено на другой язык без серьезной переделки. Эффективная разработка требует ввода всех текстовых строк в базу данных ресурсов и получения уникального идентификатора при первом написании кода, а после этого i18n относительно прост. Попытка заполнить это после факта, как правило, потребует очень больших усилий (и да, я был там!), Так что гораздо лучше сделать все правильно с самого начала.

Грэхем
источник
3

Это не является приоритетом для всех, но если вы когда-нибудь захотите иметь возможность автоматически вычислять метрики связности / сцепления в вашем коде, магические строки делают это практически невозможным. Строка в одном месте будет ссылаться на класс, метод или функцию в другом месте, и не существует простого, автоматического способа определить, что строка связана с классом / методом / функцией, просто анализируя код. Только базовый фреймворк (например, Angular) может определить наличие связи - и он может сделать это только во время выполнения. Чтобы получить информацию о сопряжении самостоятельно, вашему анализатору необходимо знать все об используемой вами среде, помимо базового языка, на котором вы кодируете.

Но опять же, это не то, что заботит многих разработчиков.

user3511585
источник