Много магических чисел

У меня есть проект, в котором я преобразовываю сообщения из одного формата в другой. Сообщения идентифицируются идентификатором. У меня есть MappingTable с идентификатором в одном формате и соответствующим идентификатором в другом формате.

Например: 2000 = 153

Идентификаторы являются целыми числами

Я добавляю все эти записи через:

this.AddMappingEntry(2400, 2005, true, false);

Этот метод добавляет запись сопоставления в список сопоставления. Я могу отфильтровать список через linq, чтобы найти правильное сопоставление, если я получу сообщение со значением 2400.

С тех пор, как проект запущен, он довольно сильно вырос, что привело к множеству рефакторингов и появлению множества идентификаторов с особым поведением. Я должен контролировать, относится ли сообщение к особому типу.

if(message.id == 153)
{
    //special behavior
}

Как мне справиться с этим самым элегантным? Должен ли я использовать много констант, описывающих тип сообщения, или есть другой более элегантный способ?

ИЗМЕНИТЬ:

Я переформулирую вопрос. У меня есть главные записи и вспомогательные записи. Но идентификаторы этих записей используются во всем Кодексе. Поскольку существует около 200 различных идентификаторов, мне было интересно, что мне делать с этими магическими числами. Инструмент, который я пишу, это конвертер.

Структура выглядит следующим образом

     +------+         +-------------+      +---------+
     | DAL  +-------> |  CONVERTER  +----> | WRITER  |
     +------+         +-------------+      +---------+

Классы Converter выглядят примерно так

    +------------+
    |BaseClass   |
    +-----+------+
          ^
          |
    +-----+------+
    |BaseRecord  +^-------------+----------------------+
    +------+-----+              |                      |
           ^                    |                      |
           |                    |                      |
    +------+-----+      +-------+--------+     +-------+--------+
    | HeadRecord |      |   RecordType1  |     |   RecordType2  |
    +------------+      +----------------+     +----------------+

BaseRecord расширяет Baseclass, а все остальные классы расширяют BaseRecord. Всего у меня есть 5 типов записей с идентификатором записи 1-5. Внутри этой записи есть несколько subrecordId (~ 50), которые используются только для идентификации записей, стоящих за автором, соответственно, после процесса записи.

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

Это приводит к проблеме: у меня есть много магических чисел, о которых никто не знает, для чего они нужны, если я использую их в своих классах и конвертере. Как мне избежать этих магических чисел? Я боюсь, что мои классы заполнены значениями 50+ const, если я их использую. Есть ли способ избежать магических чисел и большого количества константных значений? Каков правильный рефакторинг для этого?


person Bongo    schedule 17.10.2016    source источник
comment
поэтому в вашем случае 2000 = 0153 равно 0153 varchar или целому числу.. вместо использования оператора If вы подумали об операторе case, и я лично не стал бы использовать начальные нули, если вы ожидаете, что это будет 153, тогда используйте правильное целочисленное представление, а не varchar это также можно исправить с помощью оператора switch   -  person MethodMan    schedule 17.10.2016
comment
дело не в случае выключателя или если. У меня есть несколько классов, которые обрабатывают заголовки и подзаписи. Если обрабатываются в подзаписях и т.д.   -  person Bongo    schedule 17.10.2016
comment
Если я правильно понимаю ваш вопрос, то похоже, что вы захотите использовать enum, что позволит вам использовать осмысленные имена вместо магических чисел.   -  person Pieter Witvoet    schedule 19.10.2016
comment
@PieterWitvoet Это тоже был бы вариант, но кажется странным создавать такие большие перечисления или писать так много констант.   -  person Bongo    schedule 20.10.2016
comment
@Bongo: если каждый идентификатор имеет определенное значение, почему наличие множества разных идентификаторов / значений делает странным использование enum?   -  person Pieter Witvoet    schedule 20.10.2016


Ответы (7)


Если вы можете обобщить специальное поведение как набор действий, вы сможете сделать это следующим образом:

private static readonly IDictionary<int,Action> messageBehavior =
    new Dictionary<int,Action> {
        {153, () => { Console.WriteLine("Special action"); } },
        {154, () => { Console.WriteLine("Another special action"); } }
    };

Теперь вы можете получить действие из словаря по идентификатору сообщения и запустить его, если оно доступно:

Action special;
if (messageBehavior.TryGetValue(message.id, out special)) {
    special();
}

Если для действий требуется какой-то особый контекст, в котором они выполняются, например, сообщение, вызвавшее действие, вы можете использовать Action<Message> и передать ему сообщение:

private static readonly IDictionary<int,Action<MyMessageType>> messageBehavior =
    new Dictionary<int,Action> {
        {153, (m) => { Console.WriteLine("Special action; message {0}", m); } },
        {154, (m) => { Console.WriteLine("Another special action ({0})", m); } }
    };
...
Action<MyMessageType> special;
if (messageBehavior.TryGetValue(message.id, out special)) {
    special(message);
}
person Sergey Kalinichenko    schedule 17.10.2016
comment
Это лучший вариант, если action маленький. Если у вас есть 5+ LOC для каждого специального идентификатора, это может стать грязным. - person Leandro Soares; 17.10.2016

Здесь может пригодиться шаблон проектирования Factory.

Определите интерфейс для класса, который будет знать, как работать с одним особым поведением:

public interface IMessageHandler
{
    IMessage Transform(IMessage message);
}

Тогда у вас будут разные реализации. Например:

public class DefaultMessageHandler
{

    IMessage Transform(IMessage message)
    {
        return message;
    }
}

public class BehaviorXMessageHandler
{

    IMessage Transform(IMessage message)
    {
        message.SomeProperty = "hello world";
        return message;
    }
}

Тогда у вас есть ваша фабрика:

public interface IMessageHandlerFactory
{
    IMessageHandler GetHandler(int messageCode);
}

Возможной реализацией для фабрики будет использование switch-case, но я думаю, вам следует использовать внедрение зависимостей:

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

public class MappingMessageHandlerFactory : IMessageHandlerFactory
{
    public MappingMessageHandlerFactory(Dictionary<int,IMessageHandler> mapping, IMessageHandler defaultBehavior)
    {
        Mapping = mapping;
        DefaultBehavior = defaultBehavior;
    }

    public IMessageHandler GetHandler(int messageCode)
    {
        IMessageHandler output = DefaultBehavior;
        Mapping.TryGetValue(messageCode, out output);

        return output;
    }

    public Dictionary<int,IMessageHandler> Mapping {get; set;}
    public IMessageHandler DefaultBehavior {get;set;}
}

Чтобы Фабрика получила сопоставление, вы можете инициализировать его самостоятельно или использовать один из многих Контейнеры IoC, такие как Castle Windsor, Ninject, Unity и т. д.

Некоторые из этих контейнеров даже имеют общие реализации для фабрик, и все, что вам нужно предоставить, это интерфейс. В замке Виндзор он называется TypedFactoryFacility

Что бы вы ни выбрали, после прохождения структуры выше код вашего сервиса должен выглядеть примерно так:

public IMessage ProcessMessage(IMessage message)
{
    var handler = _messageHandlerFactory.GetHandler(message.Code);
    return handler.Transform(message);
}

Обновление после комментария:

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

Что вы можете сделать, так это определить другое свойство в IMessageHandler из Code и иметь абстрактный базовый класс, который обеспечивает установку Code. Поступая так, вы на самом деле говорите: «Обработчик сообщений — это тот, кто знает, за какое сообщение он отвечает».

public interface IMessageHandler
{
    IMessage Transform(IMessage message);
    int Code {get; set;}
}

public abstract class MessageHandlerBase : IMessageHandler
{
    public MessageHandlerBase(int code) 
    {
        Code = code;
    }

    public abstract IMessage Transform(IMessage message);
    public int Code {get; set;}
}

Он больше не будет получать словарь, а будет получать IEnumerable<IMessageHandler>, чтобы числа снова не определялись где-то. (вы можете реализовать его так, чтобы он преобразовывал IEnumerable в словарь, поэтому поиск по-прежнему будет в o(n), но это детали реализации.

Например, сообщение 2002 (сменить пользователя) — это одно сообщение в отправляющей системе. Принимающая система не имеет пользователя смены, а только логин (106) и выход (107).

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

Опять же, если я использую Castle Windsor в качестве примера, у него есть возможность настроить зависимости через xml. Таким образом, у каждой службы может быть такой раздел, в котором app.config вы создаете сопоставление того, какое поведение вы действительно хотите иметь, и какие Code сопоставляются с ними

person Gilad Green    schedule 17.10.2016
comment
Я использую это allready :). Но в процессе преобразования мне придется использовать магические числа в некоторых других местах, что плохо, потому что у меня есть магические числа во многих разных местах. - person Bongo; 17.10.2016
comment
Например, сообщение 2002 (сменить пользователя) — это одно сообщение в отправляющей системе. Принимающая система не имеет пользователя смены, а только логин (106) и выход (107). Если я получаю 2002, я создаю выход (107), а затем вход (106) с новым пользователем. Если я получаю 2002, имя пользователя находится в поле отправляющей системы. Если я получаю 2001(логин в отправляющей системе)то выбираю поле b. - person Bongo; 17.10.2016

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

person NikxDa    schedule 17.10.2016

Я бы установил его в App.Config и использовал Configuration Manager для получения данных.

person David Soler    schedule 17.10.2016

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

person Sample Player    schedule 17.10.2016

У меня был бы набор классов SpecialMessageHandler и класс StandardMessageHandler, а затем какой-то поиск (например, словарь). Вместо того, чтобы if/switches вы искали идентификатор сообщения в словаре, если он присутствует, используйте специализированный класс, если нет, используйте стандартный. Вы даже можете указать содержимое словаря вне кода (например, web/app.config).

person Dave Becker    schedule 17.10.2016

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

Я бы создал перечисление со специальными кодами (SpecialIds). Затем, проверяя, является ли число особенным, я бы преобразовал Id в строку.

К этой строке я бы добавил префикс «Special», который будет выглядеть как Special_153.

Это имя будет class.

Затем я бы создал папку со всеми специальными классами

Special_153.cs Special_154.cs

Каждый класс Special будет наследоваться от класса ISpecial, который реализует только метод Run.

Метод Run будет вызываться внутри конструктора. Конструктор получит полный объект сообщения.

Тогда я бы использовал его так:

if (Enum.GetNames(typeof(SpecialIds)).Contains(message.id))
{
    //special behavior
    string id = message.Id.ToString();
    string prefix = "Special_";
    string className = prefix + id;

    Type t = Type.GetType(className);
    ConstructorInfo constructor = t.GetConstructor(new Type[] { message.GetType() });
    constructor.Invoke(message);
}

constructor.Invoke(message); вызовет конструктор с аргументом message. Внутри метода класса Run вы можете изменять все, что хотите, внутри объекта message.

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

Изменить 1

Да, и, наконец, вы могли бы написать расширение, которое бы выполняло только этот вызов constructor, и просто вызывало бы:

message.CheckSpecial();

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

person Leandro Soares    schedule 17.10.2016