3

There exists an "Audit" object that is used throughout the code base that I'm trying to refactor to allow for dependency injection, and eventually better unit testing. Up until this point I have had no problems creating interfaces for my classes, and injecting those through the constructor. This class however, is different. I see why/how it's different, but I'm not sure how to go about fixing it to work "properly".

Here is an example (dumbed down version, but the problem persists even in the example):

namespace ConsoleApplication1.test.DI.Original
{
    public class MultiUseDependencies
    {
        public MultiUseDependencies()
        {

        }

        public void Update()
        {
            Audit a = new Audit();
            a.preAuditValues = "Update";

            // if data already exists, delete it
            this.Delete();

            // Update values, implementation not important

            // Audit changes to the data
            a.AuditInformation();
        }

        public void Delete()
        {
            Audit a = new Audit();
            a.preAuditValues = "Delete";

            // Delete data, implementation omitted.

            a.AuditInformation();
        }
    }

    public class Audit
    {
        public string preAuditValues { get; set; }

        public void AuditInformation()
        {
            Console.WriteLine("Audited {0}", preAuditValues);
        }
    }
}

In the above, the Update function (implementation not shown) gets the "pre change" version of the data, deletes the data (and audits it), inserts/updates the changes to the data, then audits the insert/update.

If I were to run from a console app:

Console.WriteLine("\n");
test.DI.Original.MultiUseDependencies mud = new test.DI.Original.MultiUseDependencies();
mud.Update();

I would get:

Audited Delete

Audited Update

This is the expected behavior. Now in the way the class is implemented, I can already see there will be a problem, but I'm not sure how to correct it. See the (initial) refactor with DI:

namespace ConsoleApplication1.test.DI.Refactored
{
    public class MultiUseDependencies
    {

        private readonly IAudit _audit;

        public MultiUseDependencies(IAudit audit)
        {
            _audit = audit;
        }

        public void Update()
        {
            _audit.preAuditValues = "Update";

            // if data already exists, delete it
            this.Delete();

            // Update values, implementation not important

            // Audit changes to the data
            _audit.AuditInformation();
        }

        public void Delete()
        {
            _audit.preAuditValues = "Delete";

            // Delete data, implementation omitted.

            _audit.AuditInformation();
        }
    }

    public interface IAudit
    {
        string preAuditValues { get; set; }
        void AuditInformation();
    }

    public class Audit : IAudit
    {
        public string preAuditValues { get; set; }

        public void AuditInformation()
        {
            Console.WriteLine("Audited {0}", preAuditValues);
        }
    }
}

Running:

Console.WriteLine("\n");
test.DI.Refactored.MultiUseDependencies mudRefactored = new test.DI.Refactored.MultiUseDependencies(new test.DI.Refactored.Audit());
mudRefactored.Update();

I get (as expected, but incorrect):

Audited Delete

Audited Delete

The above is expected based on the implementation, but incorrect as per the original behavior. I'm not sure how exactly to proceed. The original implementation relies on distinct Audits to correctly keep track of what's changing. When I'm passing in the implementation of IAudit in the refactor, I am only getting a single instance of Audit, where the two are butting heads with each other.

Basically before the refactor, Audit is scoped on the function level. After the refactor, Audit is scoped on the class.

Is there an easy way to correct this?

Here's a fiddle with it in action: https://dotnetfiddle.net/YbpTm4

Community
  • 1
  • 1
Kritner
  • 13,557
  • 10
  • 46
  • 72
  • 1
    Auditing, together with logging, security, and caching, is a **Cross Cutting Concern**, and one of the usual Aspect Oriented Programming (AOP) suspects. Use Decorators or Interception instead of injecting Services for such concerns. See e.g. http://stackoverflow.com/a/7906547/126014 or http://programmers.stackexchange.com/a/139113/19115 – Mark Seemann Dec 21 '15 at 13:30
  • @MarkSeemann, while decoration or interception work well for something like logging entry and exit, what approach would support detailed logging, from within a method, for example? – David Osborne Dec 21 '15 at 16:41
  • @DavidOsborne Why would you need detailed logging from within a method? – Mark Seemann Dec 21 '15 at 17:22
  • Detailed diagnostics. Would you consider this too granular? – David Osborne Dec 21 '15 at 19:41

2 Answers2

4

The problem is in your design. Audit is an object that is mutatable and that makes it runtime data. Injecting runtime data into the constructors of your components is an anti-pattern.

The solution is to change the design, for instance by defining an IAudit abstraction like this:

public interface IAuditHandler {
    void AuditInformation(string preAuditValues);
}

For this abstraction you can create the following implementation:

public class AuditHandler : IAuditHandler {
    public void AuditInformation(string preAuditValues) {
        var audit = new Audit();
        audit.preAuditValues = preAuditValues;
        audit.AuditInformation();
    }
}

The consumers can now depend on IAuditHandler:

public class MultiUseDependencies
{
    private readonly IAuditHandler _auditHandler;

    public MultiUseDependencies(IAuditHandler auditHandler) {
        _auditHandler = auditHandler;
    }

    public void Update() {
        this.Delete();

        _auditHandler.AuditInformation("Update");
    }

    public void Delete() {
        // Delete data, implementation omitted.

        _auditHandler.AuditInformation("Delete");
    }
}

But I should even take it a step further, because with your current approach you are polluting business code with cross-cutting concerns. The code for the audit trail is spread out and duplicated throughout your code base.

This however would be quite a change in your application's design, but would probably be very beneficial. You should definitely read this article to get an idea how you can improve your design this way.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • I agree with @Steven: the problem lies with the statefulness of the Audit component not the DI design. IMHO, injecting the Audit component appears reasonable.There's an argument that an Ambient Context might be a better fit but it's up for debate. – David Osborne Dec 21 '15 at 12:57
  • 1
    @DavidOsborne: I would disagree that the Ambient Context pattern is useful here. That would still cause the business components to have the second responsibility of logging that audit information. That it a concern that should be separated from such component. Audit trailing can be applied to a system using decoration. – Steven Dec 21 '15 at 13:02
  • Surely that would depend on the granularity of logging required though? If you only want to log the entry and exit, then decoration is ideal. However, I'm not sure how decorating will give you the ability to weave detailed logging into business code? – David Osborne Dec 21 '15 at 16:05
  • @DavidOsborne: I found that with a well designed system, the number of times you need this are really small, especially with Audit trailing; it is done one the granularity of the use case. And in cases you need this, injecting it as a dependency results in better code, because the Ambient Context is a 'hidden dependency' (i.e. not specified in the constructor). – Steven Dec 21 '15 at 16:19
  • I agree. An Ambient Context is attractive on many levels but brings with it more complexity. It has its place though. – David Osborne Dec 21 '15 at 16:37
  • @DavidOsborne: Sure, it has its place. But I see the pattern being overused all the time, because of design flaws. – Steven Dec 21 '15 at 16:40
  • @Steven thanks, yeah I know design is a problem, hoping to redo some of this stuff eventually. One step at a time though, it's a big code base :) – Kritner Dec 22 '15 at 12:17
0

Try this:

public void Update()
{
    // if data already exists, delete it
    this.Delete();

    //preAuditValues should be changed after the delete or it will keep 
    //the old value

    _audit.preAuditValues = "Update";

    // Update values, implementation not important

    // Audit changes to the data
    _audit.AuditInformation();
}

Or this should work too:

public void Delete()
{
    string oldValue = _audit.preAuditValues;
    _audit.preAuditValues = "Delete";

    // Delete data, implementation omitted.

    _audit.AuditInformation();
    //Restoring oldValue after finished with Delete
    _audit.preAuditValues = oldValue ;
}
Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Levdad
  • 21
  • 3