4

This is more of an architecture/best practices question than anything else, so please feel free to add your two cents. I know i stated status in the title, but this goes for any basic property of an object. I think the account example below will help demonstrate my question a little better than status.

Here is a sample Account object:

public class Account
{
   private IList<Transaction> _transactions;

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

Now lets say I want to start keeping a history of every time a transaction is added with this object.

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

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

At this level what I would normally do is add a collection of history events to the account object and also add a line of code to create a history event inside of the AddTransaction() method like this

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

Now the next part is where the problem starts to arise. Suppose I want to do a bulk posting and I want to retain a record of which accounts were changed in this bulk posting for something like a report or if I needed to undo it later. So I would create an object like this.

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

From here I see a few different options to handle this since it can't be handled by the example code above.

1) Create a function in a Service type object that loops through a list of accounts calling the AddTransaction() method and also creating history records tied to a 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) Pass some type of HistoryManager object into the AddTransaction method along with the transaction to be added. Then the function could use the history manager to create the records.

Ok this post is long enough. If i've not been clear enough let me know. Thanks for you input.

stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
wdrone
  • 87
  • 1
  • 6
  • Is this an application that uses some sort of persistent storage (i.e. a database)? If so then this question is probably relevant: http://stackoverflow.com/questions/1798156/how-to-keep-an-audit-history-of-changes-to-the-table – James Gaunt Jul 13 '10 at 16:29
  • Yes it uses persistent storage, but I'm not interested in tracking everything like an audit log. This is more of just a way to group a set of changes made to some objects. So for example I can print a report of all the objects I just changed the status on. – wdrone Jul 13 '10 at 16:58

3 Answers3

10

Your method might work just fine, but let me propose an alternative.

Why not add a TransactionAdded Event to the Account class.

You could then subscribe to the Event from (I'm guessing here) the HistoryGroup object so that a new AccountHistory object was added every time the Event fired.

UPDATE

As mentioned in the comments, another method of accomplishing the goal would be to have HistoryGroup implement an interface (ITransactionLogger or something similar) and then modify Account so that the ITransactionLogger dependency can be injected.

Going either of these routes makes things a little easier to manage from the complexity and debugging standpoint, but doesn't allow for multiple Loggers like Events.

That would make your code a little more flexible and at the same time allow other consumers interested in the TransactionAdded Event to subscribe.

Justin Niessner
  • 242,243
  • 40
  • 408
  • 536
  • I agree, that's the way I would do it. Makes it flexible and manageable as Justin has stated – Iain Ward Jul 13 '10 at 16:29
  • 1
    like implementing `INotifyPropertyChanged` :) –  Jul 13 '10 at 16:32
  • I would come back to the question of whats the underlying store? History implies persistent storage? If you design the history in the object model like this then you're hoping that something doesn't crash in your application between the time you raise the TransactionAdded event and the time the database is updated. – James Gaunt Jul 13 '10 at 16:32
  • I like your suggestion, I hadn't considered using events and listeners. This would also help me implement a type of alert system to send emails if something changed that needed the attention of another department. James - there is persistent storage, but everything is done in a single transaction (ie change object & post history) so it shouldn't be an issue. – wdrone Jul 13 '10 at 16:39
  • Actually scratch that - it would be an issue because there would be no way to get them in the same transaction... – wdrone Jul 13 '10 at 16:40
  • 2
    My two cents: avoid using events whenever it is possible. They are hard to debug and maintain because there are no hints in the code about what will happen when the event is invoked. You have to search through the code for all possible subscribers to get an idea what might happen and what not. You have to mange all the subscriptions and unsubscriptions. So events are still a good choice for multiple subscriber scenarios but I would usually not suggest to use them in single subscriber scenarios only because there might be multiple subscriber at some point in the future. – Daniel Brückner Jul 13 '10 at 16:42
  • In the given scenario I would just inject a dependency on a transaction logging service and use it where required. Extending the functionality of the transaction logging service for bulk operations will be straight forward, too. – Daniel Brückner Jul 13 '10 at 16:44
  • I like where your head's at Daniel - my thoughts weren't on the injection more than on interception – Josh E Jul 13 '10 at 16:46
  • Justin in regards to your Update My dependency injection is a bit weak, but are you suggesting that i do something like what I outlined in the number 2 option of my post? – wdrone Jul 13 '10 at 17:05
  • @user373565 - You could do it that way. I would add it to the constructor of the Account object though. – Justin Niessner Jul 13 '10 at 17:07
2

I agree with Justin's answer in some ways, but one of the tags on the OP is POCO; adding an event to the Account class would in some ways un-POCO your POCO.

If you're into AOP and other such, you could use interception (most IoC frameworks, including Unity and Castle offer this functionality) to grab transactions of interest.

The benefit of interception is that your Account class has no coupling whatsoever with the AccountHistory class, the interception is highly configurable according to whatever rules you want, and it is easily changed without forcing an application recompile (if you put AccountHistory into a different assembly with the interception handlers). By using interception you are making your code more focused on the business domain rather on what could be considered an infrastructure task (auditing).

Again, this is another alternative for your toolbox; if you don't need to serialize your POCO's over the wire for any reason, then implementing the Observer Pattern (GoF) through events as suggested by Justin may be a more light-weight approach.

Josh E
  • 7,390
  • 2
  • 32
  • 44
  • Josh - I see your point in using interceptors, but how doesn't this essentially turn 1 unit of work into 2 units of work. My concern is the interceptor failing after the change has been persisted. I'm highly reliant on if the transactions exist the history event (or batch grouping) must exist in the database also. I'm not overly familiar with interceptors, so fill me in if i'm missing the boat. I use NHibernate and i know that is supports interceptors, but i want to be specific on what i'm tracking and not tracking. I also use the HistoryGroup id in reports – wdrone Jul 13 '10 at 16:48
  • It doesn't have to necessarily be two units of work in the transactional sense -- the interceptor method could certainly participate in the transaction – Josh E Jul 13 '10 at 17:05
  • to elaborate on that, if there's a problem with the transaction logging, you can rollback and fail the entire transaction. For interception, you typically specify in the configuration an interception policy that governs which methods are intercepted. These policies can be as specific as parameter values, or as general as an interface implementation, attribute, or even having a method name that matches. You have a whole lot of fine-grained control over when a method is intercepted, in other words. – Josh E Jul 13 '10 at 17:13
1

The gang of four seem to think so. Transactions, history tracking, and un-doing are all part of a command pattern contract. You can implement history with a stack. Here's a snippet of relevant code including the contract, note that not all methods are or have to be implemented:

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);
        }
    }
}

Note that I also use a factory pattern.

P.Brian.Mackey
  • 43,228
  • 68
  • 238
  • 348
  • Great code, but how would you specifically apply it to the proposed example above. – wdrone Jul 13 '10 at 17:00
  • Personally I'd have Account implement ICommand and get rid of the concrete classes that currently perform the operations of the ICommand contract. Then you can use the code I posted as a starting point to implement the history and transactions that are currently performed by other types. I'm working right now, but I'll try to get something together for your specific type. – P.Brian.Mackey Jul 13 '10 at 17:16