C # - должен ли объект нести ответственность за создание объекта истории, когда он меняет что-то вроде статуса?

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

Вот образец объекта Account:

public class Account
{
   private IList<Transaction> _transactions;

   public AddTransaction(trans as Transaction)
   {
      _transaction.add(trans)
   }
}

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

public class AccountHistory
{
   private DateTime _historyDate;
   private String _details;

   public AccountHistory(string details)
   {
      _historyDate = DateTime.Now;
      _details = details;
   }
}

На этом уровне я обычно добавляю коллекцию событий истории к объекту учетной записи, а также добавляю строку кода для создания события истории внутри метода AddTransaction (), подобного этому.

public AddTransaction(trans as Transaction)
{
   _transaction.add(trans);
   **_historyEvents.add(new AccountHistory("Transaction Added: " + trans.ToString());**
}

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

public class HistoryGroup()
{
   private IList<AccountHistory> _events;
}

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

1) Создайте функцию в объекте типа Service, которая просматривает список учетных записей, вызывая метод AddTransaction (), а также создает записи истории, связанные с HistoryGroup.

 public void AddTransactions(IList<Account> accounts, Transaction trans)
    {
       HistoryGroup history = new HistoryGroup(); 
       for (int x=0;x <=accounts.Count - 1; x++)
       {
         accounts(x).AddTransaction(trans);
         history.AddEvent(new AccountHistory("Added Transaction: " + trans.ToString();
       }
    }

2) Передайте некоторый тип объекта HistoryManager в метод AddTransaction вместе с транзакцией, которую нужно добавить. Затем функция может использовать диспетчер истории для создания записей.

Хорошо, этот пост достаточно длинный. Если я недостаточно ясно выразился, дайте мне знать. Спасибо за ваш вклад.


person wdrone    schedule 13.07.2010    source источник
comment
Это приложение, которое использует какое-то постоянное хранилище (например, базу данных)? Если да, то этот вопрос, вероятно, актуален: stackoverflow.com/questions/1798156/   -  person James Gaunt    schedule 13.07.2010
comment
Да, он использует постоянное хранилище, но меня не интересует отслеживание всего, например журнала аудита. Это больше просто способ сгруппировать набор изменений, внесенных в некоторые объекты. Так, например, я могу распечатать отчет обо всех объектах, статус которых я только что изменил.   -  person wdrone    schedule 13.07.2010


Ответы (3)


Ваш метод может работать нормально, но позвольте мне предложить альтернативу.

Почему бы не добавить событие TransactionAdded в класс Account.

Затем вы можете подписаться на событие из (я предполагаю, здесь) объекта HistoryGroup, чтобы новый объект AccountHistory добавлялся каждый раз при запуске события.

ОБНОВЛЕНИЕ

Как упоминалось в комментариях, другим методом достижения цели было бы реализовать HistoryGroup интерфейс (ITransactionLogger или что-то подобное), а затем изменить Account, чтобы можно было внедрить зависимость ITransactionLogger.

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

Это сделает ваш код немного более гибким и в то же время позволит другим потребителям, заинтересованным в событии TransactionAdded, подписаться.

person Justin Niessner    schedule 13.07.2010
comment
Я согласен, я бы так и поступил. Делает его гибким и управляемым, как заявил Джастин. - person Iain Ward; 13.07.2010
comment
вроде реализации INotifyPropertyChanged :) - person ; 13.07.2010
comment
Я бы вернулся к вопросу о том, что за магазин? История подразумевает постоянное хранилище? Если вы проектируете историю в объектной модели таким образом, вы надеетесь, что в вашем приложении не произойдет сбоя в период между моментом, когда вы инициируете событие TransactionAdded, и моментом обновления базы данных. - person James Gaunt; 13.07.2010
comment
Мне нравится ваше предложение, я не думал об использовании событий и слушателей. Это также помогло бы мне реализовать тип системы оповещений для отправки электронных писем, если что-то изменится, что потребует внимания другого отдела. Джеймс: есть постоянное хранилище, но все делается за одну транзакцию (т. Е. Изменение объекта и истории публикаций), так что это не должно быть проблемой. - person wdrone; 13.07.2010
comment
На самом деле поцарапайте это - это было бы проблемой, потому что не было бы возможности получить их в одной транзакции ... - person wdrone; 13.07.2010
comment
Мои два цента: избегайте использования событий, когда это возможно. Их сложно отлаживать и поддерживать, потому что в коде нет подсказок о том, что произойдет при вызове события. Вам нужно найти в коде всех возможных подписчиков, чтобы понять, что может случиться, а что нет. Вы должны управлять всеми подписками и отказами от подписки. Таким образом, события по-прежнему являются хорошим выбором для сценариев с несколькими подписчиками, но я обычно не предлагаю использовать их в сценариях с одним подписчиком только потому, что в какой-то момент в будущем может быть несколько подписчиков. - person Daniel Brückner; 13.07.2010
comment
В данном сценарии я бы просто ввел зависимость от службы ведения журнала транзакций и использовал ее там, где это необходимо. Расширение функциональности службы ведения журнала транзакций для массовых операций также будет прямым. - person Daniel Brückner; 13.07.2010
comment
Мне нравится твоя голова у Даниэля - мои мысли были не о уколе, а о перехвате - person Josh E; 13.07.2010
comment
Джастин в отношении вашего обновления. Моя инъекция зависимостей немного слабовата, но вы предлагаете мне сделать что-то вроде того, что я изложил в варианте номер 2 моего сообщения? - person wdrone; 13.07.2010
comment
@ user373565 - Можно и так. Я бы добавил его в конструктор объекта Account. - person Justin Niessner; 13.07.2010

В некотором смысле я согласен с ответом Джастина, но один из тегов OP - POCO; добавление события в класс Account в некотором роде приведет к отключению POCO от POCO.

Если вы работаете с АОП и другим подобным, вы можете использовать перехват (большинство фреймворков IoC, включая Unity и Castle, предлагают эту функцию) для захвата интересующих транзакций.

Преимущество перехвата состоит в том, что ваш класс Account не имеет никакой связи с классом AccountHistory, перехват легко настраивается в соответствии с любыми правилами, которые вы хотите, и его легко изменить без принудительной перекомпиляции приложения (если вы поместите AccountHistory в другую сборку с помощью обработчики перехвата). Используя перехват, вы делаете свой код более сфокусированным на бизнес-сфере, а не на том, что можно рассматривать как задачу инфраструктуры (аудит).

Опять же, это еще одна альтернатива вашему набору инструментов; если вам не нужно сериализовать POCO по сети по какой-либо причине, то реализация шаблона наблюдателя (GoF) через события, как предлагает Джастин, может быть более легким подходом.

person Josh E    schedule 13.07.2010
comment
Джош, я понимаю, что ты понимаешь в использовании перехватчиков, но как это, по сути, не превратить 1 единицу работы в 2 единицы работы. Меня беспокоит, что перехватчик выйдет из строя после того, как изменение было сохранено. Я очень полагаюсь на то, существуют ли транзакции, событие истории (или группировка пакетов) также должно существовать в базе данных. Я не слишком хорошо знаком с перехватчиками, так что сообщите мне, если я пропущу лодку. Я использую NHibernate и знаю, что он поддерживает перехватчики, но я хочу уточнить, что я отслеживаю, а не отслеживаю. Я также использую идентификатор HistoryGroup в отчетах - person wdrone; 13.07.2010
comment
Это не обязательно должны быть две единицы работы в транзакционном смысле - метод перехватчика определенно может участвовать в транзакции. - person Josh E; 13.07.2010
comment
Чтобы уточнить это, если есть проблема с ведением журнала транзакции, вы можете выполнить откат и завершить транзакцию полностью. Для перехвата вы обычно указываете в конфигурации политику перехвата, которая определяет, какие методы перехватываются. Эти политики могут быть такими же конкретными, как значения параметров, или такими общими, как реализация интерфейса, атрибут или даже иметь совпадающее имя метода. Другими словами, у вас есть много детального контроля над перехватом метода. - person Josh E; 13.07.2010

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

public interface ICommand
{
    void execute();
    void undo();
    void store();
    void load();
}
public class ManagerMacro : ICommand
{
    List<ICommand> Commands;
    Stack commandStack;
    /// <summary>
    /// Use in combination with AddSteps
    /// </summary>
    //public ManagerMacro()
    //{

    //}
    public ManagerMacro(List<ICommand> commands)
    {
        this.Commands = commands;
        this.commandStack = new Stack();
    }

    #region ICommand Members

    public void execute()
    {
        for (int i = 0; i < Commands.Count; i++)
        {
            commandStack.Push(Commands[i]);
            Commands[i].execute();
        }
    }

    public void undo()
    {
        for (int i = 0; i < Commands.Count; i++)
        {
            if (commandStack.Count > 0)
            {
                ICommand Command = (ICommand)commandStack.Pop();
                Command.undo();
            }
        }
    }
    public void store()
    {
        throw new NotImplementedException();
    }

    public void load()
    {
        throw new NotImplementedException();
    }
    #endregion

    public void AddSteps(Steps[] steps)
    {
        foreach (Steps step in steps)
        {
            ICommand thisStep = null;
            switch (step)
            {
                case Steps.Manager1: thisStep = new Step1(); break;
                case Steps.Manager2: thisStep = new Step2(); break;
                case Steps.Manager3: thisStep = new Step3(); break;
                case Steps.Manager4: thisStep = new Step4(); break;
            }
            this.Commands.Add(thisStep);
        }
    }
}

Обратите внимание, что я также использую заводской узор.

person P.Brian.Mackey    schedule 13.07.2010
comment
Отличный код, но как бы вы конкретно применили его к предложенному выше примеру? - person wdrone; 13.07.2010
comment
Лично я бы попросил Account реализовать ICommand и избавиться от конкретных классов, которые в настоящее время выполняют операции контракта ICommand. Затем вы можете использовать опубликованный мной код в качестве отправной точки для реализации истории и транзакций, которые в настоящее время выполняются другими типами. Я работаю прямо сейчас, но я постараюсь собрать что-нибудь для вашего конкретного типа. - person P.Brian.Mackey; 13.07.2010