Feel Good.

05 августа 2010

Marker Interface против Custom attributes

Бывают случаи, когда у разработчика C# возникает острая необходимость пометить некую сущность маркером (Marker), и в зависимости от типа маркера соответственно обрабатывать сущность. Например, Вы получаете сообщения, и для различных типов сообщений (но не для всех, а например только для типа MessageB) делаете запись в лог о их получении (например, в методе OnReceive(Message msg)):

class Message{}

class MessageA : Message { }

class MessageB : Message { }

class MessageC : Message { }



Существуют несколько способов решения данной задачи, и первое что приходит в голову написать следующее:

void OnReceive(Message msg)

{

    if(msg is MessageB)

    {

        // Делаем запись в лог, что

        // получили сообщение типа B

    }

}


очевидно, что такое быстрое решение не очень красивое, так если мы заходим логировать приход сообщения типа MessageC, то придется расширять if-условие.
Тогда на ум приходит идея ввести некий маркер, и если вспомнить про Marker Interface pattern, то мы получим следующую реализацию:

interface ILogMarker

{

    // Пусто, интерфейс-маркер.

}

 

class Message{}

class MessageA : Message { }

// пометим маркером ILogMarker

class MessageB : Message, ILogMarker

{

}

class MessageC : Message { }

 

void OnReceive(Message msg)

{

    if(msg is ILogMarker)

    {

        // Делаем запись в лог, что

        // получили сообщение помеченное

        // как ILogMarker.

    }

}


Уже намного лучше, условие if у нас уже фиксировано. Единственное что должно смутить, это странное сочетание типа MessageB и интерфейса ILogMarker, ведь ILogMarker несет в себе метаинформацию, а не саму информацию о типе.
Но на этом можно было бы и остановиться, если бы не наличие такого инструмента как Custom attributes, и в этом случае, использовать "Marker Interface" при проектировании типов не рекомендуется (Interface Design: Avoid using marker interfaces (interfaces with no members)).
Итак, принимая во внимание данную рекомендацию:

// Custom attribute-маркер

class LogAttrubute : Attribute

{

}

 

class Message{}

class MessageA : Message { }

// пометим маркером LogAttrubute

[LogAttrubute]

class MessageB : Message, ILogMarker

{

}

class MessageC : Message { }

 

void OnReceive(Message msg)

{

    if (msg.GetType().IsDefined(typeof(LogAttrubute), false))

    {

        // Делаем запись в лог, что

        // получили сообщение помеченное

        // как LogAttrubute.

    }

}


Итого, мы получили фиксированное if-условие, метаданные о типе сообщения вынесены в Custom attributes, а не в сам тип в случае Marker Interface.

Ссылки:
  1. What is the purpose of a marker interface?
  2. Interface without any methods - bad practice?

Progg it

10 комментариев:

  1. А просто field IsRequiredLogging в message добавить не судьба?

    ОтветитьУдалить
  2. @mphome
    IsRequiredLogging - не относится конкретно к типу Message, это ведь как бы служебная информация, зачем ее примешивать?

    ОтветитьУдалить
  3. Звучит логично с точки зрения проектирования. :)
    Только вот как быть если надо к примеру логированием "динамично" управлять? В ран-тайме разве можно добавить custom attribute? (про интерфейс я вообще молчу). Как быть?

    ОтветитьУдалить
  4. Думаю надо расширить атрибут каким-нибудь свойством-условием:
    class LogAttrubute : Attribute
    {
    /* Добавить свойство-условие, при выполнении которого осуществлять логирование */
    }
    а в OnReceive, получать атрибут у msg и проверять.

    ОтветитьУдалить
  5. Илья, как-то монстроидально это выглядит. Как по мне static field в таком случае практичней (что не означает - "правильнее" :) ).

    Хотя Вы правы что к самому message это никакого отношения не имеет. И его там быть не должно.

    ОтветитьУдалить
  6. О том, что оба подхода нормальные (не монстроидальные) говорит тот факт, что они используются в самом .NET Framework.
    Например, интерфейс INamingContainer и атрибут Serializable.

    ОтветитьУдалить
  7. @veselovski
    Возможно ответ предпочтения подхода кроется в сравнении быстродействии/сложности двух конструкций:
    "msg is ILogMarker"
    против
    "msg.GetType().IsDefined(typeof(LogAttrubute), false)"

    ОтветитьУдалить
  8. А по мне - проблема надумана. Оба подхода стоят друг друга и тут в большей степени выбор сводится к вкусам конкретного разработчика/архитектора.
    Хотя я обычно предпочитаю использовать атрибуты.

    ОтветитьУдалить
  9. Согласен с Sergun - реально никакой проблемы нет, т.к. оба подхода валидны, хотя конечно `is` всяко лучше чем reflection. Лично я не использую ни то ни другое - я фанат конфигурируемых контейнеров, в которых потом можно "передумать" и поменять подход без особых телодвижений.

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

    ОтветитьУдалить