If you want to use inversion of control, unit testing and adhere to SOLID principals in your C# code this often means you have a lot of interfaces. Core considerations when dealing with interfaces are things like:
- Where should the interface be defined – alongside the main implementation or in a separate assembly?
- Should the interface be generic or not?
- Am I breaking interface segregation principal?
The one that sometimes falls by the wayside is:
- Does the the interface definition match my intended usage?
Example
A trivial example of this might be where you have a database containing a Log table of messages from an application where each has an ID of some kind, type, source, message and date/time recorded. The interface for the data access to this table might be:
public interface ILogRepository { IEnumerable<Log> GetLogs(); }
Innocuous enough however what if all our usages of this interface and method require that the resultant IEnumerable is ordered by the recorded time of the log message. IEnumerable alone doesn’t guarantee anything about the order and reordering the output at each point of use would be very inefficient, not to mention that the database would likely be a much better place to perform the ordering action.
Attempt 1 – Be more descriptive
The simplest option is simply to bake the ordering information in to the interface definition e.g.
public interface ILogRepository { IEnumerable<Log> GetLogsOrderedByDate(); }
This way we are clear at the point of implementation and the point of use about what the ordering of the items should be. Of course, renaming a method still doesn’t guarantee the result will be ordered correctly but at least if an ordering is missing you have the additional information in the definition about what the correct order should be.
The major problem with this option is that we head towards potentially violating the Open/Closed principal where our API should be open for extension but closed for modification. If we need to change the order log items are returned then we have to rename the method (violating OCP) or add a new method which specifies a different ordering potentially making the original completely redundant in the codebase.
Attempt 2 – Expose IQueryable instead
Another option is to swap from using IEnumerable to using IQueryable and allow the calling code to specify its own ordering e.g.
public interface ILogRepository { IQueryable<Log> GetLogs(); }
...
var logs = logRepo.GetLogs().OrderBy(l => l.DateTime);
This method would be more efficient, always performing the ordering in the database, but with this option we have to repeat the OrderBy part at every point of use to ensure our ordering will be correct. This gives us flexibility but isn’t particularly DRY and may be difficult to change.
It’s also somewhat of a leaky abstraction as we’re spilling data access innards into our other layers and losing control of the queries being executed on our database – calling code can do more with IQueryable than specify an order which may not be desirable.
Attempt 3 – Allow ordering to be passed in
This is somewhat similar to option 2 however by allowing order to be passed in we can use a specified default ordering while also giving the calling code the ability to override it if necessary without exposing the all-powerful IQueryable.
public interface ILogRepository
{
IEnumerable<Log> GetLogs<TKey>(Expressions<Func<Log, TKey>> ordering);
}
Of course this option still has the potential for a lot of repetition of desired ordering and OCP may rear its head again if we need to expose some other IQueryable feature in a similar controlled fashion. Another undesirable feature of this method is that the specified ordering cannot be easily validated, much like with option 2 it may provide the caller with too much power.
Attempt 4 – Return IOrderedEnumerable
An interesting option is amending the interface definition so that the method returns IOrderedEnumerable instead of plain IEnumerable e.g.
public interface ILogRepository { IOrderedEnumerable<Log> GetLogs(); }
A very slight tweak to the definition with no specific ordering defined in the API but it provides a cue to the calling code that an ordering is being applied, should it care, and also makes it difficult for the interface implementation to accidentally miss out the ordering.
Obviously with this option we return to the problem of there being no particular guarantee of the specific ordering being applied not to mention it being quite tricky to return IOrderedEnumerable in the first place.
Alternatives?
Perhaps a better question than:
- Does the the interface definition match my intended usage?
would be:
- Can I describe my intended usage sufficiently with an interface?
It’s difficult to define this, and many other kinds of behaviours, using interfaces alone. A better approach in this case would probably be to not interface this class out at all and have the business code expect an instance of the concrete type as its dependency thus providing a guarantee of order. The class would still be abstracted from the backing store such that it can itself be tested e.g.
// Data access code
internal interface ILogStore { IQueryable<Log> Logs { get; } }
public class LogRepository
{
private ILogStore _store;
public LogRepository() : this(null) {}
internal LogRepository(ILogStore store)
{
_store = store ?? new DatabaseLogStore();
}
public IEnumerable<Log> GetLogs()
{
return _store.Logs.OrderBy(l => l.DateTime);
}
}
// Business code
public class LogReader
{
private LogRepository _logRepo;
public LogReader(LogRepository logRepo)
{
if (logRepo == null) throw new ArgumentNullException("logRepo");
_logRepo = logRepo;
}
...
}
2 comments:
What about IOrderedLogRepository ? It makes the interface role explicit
This has the same problem as renaming the interface method. The main point was that not all dependencies have to be specified in terms of interfaces, sometimes it just doesn't make sense.
For example, instead of an IOrderedLogRepository you could define an abstract OrderedLogRepository and then derive OrderedByDateLogRepository from that which specifies your ordering function and use that class as the dependency.
Post a Comment