views:

104

answers:

3

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.

+7  A: 

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
I agree, that's the way I would do it. Makes it flexible and manageable as Justin has stated
w69rdy
like implementing `INotifyPropertyChanged` :)
Andreas Niedermair
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
wdrone
Actually scratch that - it would be an issue because there would be no way to get them in the same transaction...
wdrone
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
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
I like where your head's at Daniel - my thoughts weren't on the injection more than on interception
Josh E
Justin in regards to your UpdateMy 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
@user373565 - You could do it that way. I would add it to the constructor of the Account object though.
Justin Niessner
A: 

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
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
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
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
A: 

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
Great code, but how would you specifically apply it to the proposed example above.
wdrone
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