HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpModerate

Reusable Unit Of Work Interface / Factory

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
factoryinterfacereusableworkunit

Problem

Given my IUnitOfWork interface

using System;

public interface IUnitOfWork : IDisposable
{
    void Commit();
}


I then create an abstract factory interface called IUnitOfWorkFactory

using System.Transactions;

public interface IUnitOfWorkFactory
{
    IUnitOfWork GetUnitOfWork(IsolationLevel isolationLevel);
}


I then create a default implementation of my IUnitOfWork called TransactionScopeUnitOfWork

using System;
using System.Transactions;

public class TransactionScopeUnitOfWork : IUnitOfWork
{
    private bool disposed = false;

    private readonly TransactionScope transactionScope;

    public TransactionScopeUnitOfWork(IsolationLevel isolationLevel)
    {
        this.transactionScope = new TransactionScope(
                TransactionScopeOption.Required,
                new TransactionOptions
                {
                    IsolationLevel = isolationLevel,
                    Timeout = TransactionManager.MaximumTimeout
                });
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                this.transactionScope.Dispose();
            }

            disposed = true;
        }
    }

    public void Commit()
    {
        this.transactionScope.Complete();
    }
}


I then create the factory to return that implementation called TransactionScopeUnitOfWorkFactory

using System.Transactions;

public class TransactionScopeUnitOfWorkFactory : IUnitOfWorkFactory
{
    public IUnitOfWork GetUnitOfWork(IsolationLevel isolationLevel)
    {
        return new TransactionScopeUnitOfWork(isolationLevel);
    }
}


The reason for creating the factory is to allow DI (Dependency Injection) frameworks to use different unit of work implementations depending on configuration.

If TransactionScopeUnitOfWorkFactory was mapped

Solution

Leaky Abstractions

public interface IUnitOfWork : IDisposable
{
    void Commit();
}


I like that there's only a Commit method here - it makes the IUnitOfWork interface very well segregated/focused. However the interface specifying that all implementations must also implement IDisposable, is a leaky abstraction - you have a specific implementation in mind, and that implementation is leaking into the abstraction: what if I wanted a mock-up implementation that doesn't need to be disposed?

public interface IUnitOfWorkFactory
{
    IUnitOfWork GetUnitOfWork(IsolationLevel isolationLevel);
}


GetUnitOfWork makes it sound like the "factory" is hiding a Singleton or something (the name is reminiscent of "GetInstance") - I like my abstract factories when they expose a single parameterless Create method, and intake their dependencies from the constructor; IsolationLevel is also an implementation detail that's leaking into an abstraction - one specific implementation requires an "isolation level", and that implementation detail is leaked into the abstraction.

I would move : IDisposable to TransactionScopeUnitOfWork, because that specific implementation must implement IDisposable.

Then I'd change the abstract factory interface to look something like this:

public interface IUnitOfWorkFactory
{
    IUnitOfWork Create();
}


And the specific implementation that creates a TransactionScopeUnitOfWork would be responsible for specifying an IsolationLevel:

public class TransactionScopeUnitOfWorkFactory : IUnitOfWorkFactory
{
    private readonly IsolationLevel _isolationLevel;

    public TransactionScopeUnitOfWorkFactory(IsolationLevel isolationLevel)
    {
        _isolationLevel = isolationLevel;
    }

    public IUnitOfWork Create()
    {
        return new TransactionScopeUnitOfWork(_isolationLevel);
    }
}


With IoC Containers such as Ninject, you can easily specify IsolationLevel.Serializable or IsolationLevel.ReadCommitted as needed, depending on whether the factory dependency is being injected into a Smurf or into a FooBar's constructor.

That said I've noticed I seldom need to actually implement an abstract factory with Ninject: you can easily configure DI to inject the result of an anonymous method whenever a class has an IUnitOfWork dependency to be constructor-injected: you just do the binding with .ToFactory and let Ninject do the hard work.

The client code shouldn't be bothered with configuring a transaction isolation level if all it needs to care for is that it's depending on an IUnitOfWork that provides a Commit method; if the client code needs to know about an isolation level, then it knows more than it should about the implementation behind the interface it's presented.

Code Snippets

public interface IUnitOfWork : IDisposable
{
    void Commit();
}
public interface IUnitOfWorkFactory
{
    IUnitOfWork GetUnitOfWork(IsolationLevel isolationLevel);
}
public interface IUnitOfWorkFactory
{
    IUnitOfWork Create();
}
public class TransactionScopeUnitOfWorkFactory : IUnitOfWorkFactory
{
    private readonly IsolationLevel _isolationLevel;

    public TransactionScopeUnitOfWorkFactory(IsolationLevel isolationLevel)
    {
        _isolationLevel = isolationLevel;
    }

    public IUnitOfWork Create()
    {
        return new TransactionScopeUnitOfWork(_isolationLevel);
    }
}

Context

StackExchange Code Review Q#77610, answer score: 16

Revisions (0)

No revisions yet.