I am working on some code right now that involves processing user requests. Each request requires going through an approval. When a request is made, one or more records are created on the database recording what the request entails. Then a ticket is created and placed on the approver’s work queue.
For a typical request, approving it involves these steps:
- The request is processed (usually resulting in a table getting updated)
- The changes are recorded to a history table (record auditing).
- The request is marked as approved by the approver (approval auditing).
- The ticket is removed from the work queue.
- The original requester is sent an email telling them their request was approved.
We have a single class that kicks off these steps sequentially within a transaction (http://martinfowler.com/eaaCatalog/transactionScript.html). For the most part, the sequence doesn’t really matter and the email isn’t really essential.
The problem is each of these classes has at least 5+ dependencies (in reality, it’s closer to 10+). I am stumped about how to reduce the number of dependencies when it comes to a workflow like this.
I thought about grouping some of these items together, but there’s no logical grouping. I don’t see much of a point in creating an unnecessary layer like that, anyway.
I also thought about doing some kind of observer pattern (.NET events). A class higher up could listen for the event, instantiate the needed classes and kick them off. However, that “higher-up class” would then be dependent on all of the classes again, so I’d be right back in the same situation.
So, the only ways I’ve been able to think of reducing the number of dependencies is to either do artificial grouping of classes or to just make it the layer above’s problem. Neither seem ideal and I wonder if I am missing something. If it helps, I am using Ninject with ASP.NET MVC. I have access to an IOC container (DependencyResolver
). I am wondering if the solution is some sort of combination between using listeners/events and an IOC container.
3
If you’re calling these steps, then clearly the order is relevant to some degree. The exact order may not matter, but there is going to be a certain amount of “X and Y happen before Z”. For example, you probably don’t want to send the email saying the request was approved, before it has actually been approved. Likewise, the request probably cannot be approved before it is actually processed/added to the queue/etc.
Transaction scripts (or “workflow managers”) are basically procedural code. The number of dependencies isn’t really the issue, it’s just a proxy for the issue. The actual issue is the number of responsibilities the script has. And in your case, the number of responsibilities grows with each new step you add. And that, of course, goes against the Single Responsibility Principle.
The most common antithesis to procedural code is event-driven code. You say this:
A class higher up could listen for the event, instantiate the needed classes and kick them off. However, that “higher-up class” would then be dependent on all of the classes again, so I’d be right back in the same situation.
But that’s missing the point. You’re still trying to have one class that’s responsible for every step. Instead, you want to have a class for each step – it listens for whatever precursor events are supposed to trigger that step, and then starts that step and that step alone. One responsibility, and very few dependencies, since the events themselves are just small bags of “arguments”, or in some cases just strings or marker interfaces.
There are lots of frameworks around to coordinate the event handlers, ranging from highly-reliable, transactional service buses like NServiceBus or Mass Transit, which are heavy on infrastructure and designed for distributed systems, all the way to Appcelerate (formerly bbvCommom/bbvEventBroker, which has a Ninject extension) or Udi Dahan’s simple Domain Events implementation, which are designed to run entirely in-process.
The basic idea is always the same, though:
- A client sends a message to initiate some process.
- A handler (or several) responds to that message.
- Each handler publishes some event whenever it is finished.
- Subsequent handlers may respond to those events and continue the process.
This goes on for as long as it has to, until there is no longer anything “listening”.
So, in your example, you might have these classes (note: this is not written to be specific to any particular framework, it’s more or less pseudocode):
// Messages
public class SubmitOrderCommand { ... }
public class ApproveOrderCommand { ... }
public class OrderProcessedEvent { ... }
public class OrderApprovedEvent { ... }
// Handlers
public class OrderProcessor : IConsume<SubmitOrderCommand>
{
public void Handle(SubmitOrderCommand message)
{
// Process the order
Events.Publish<OrderProcessedEvent>();
}
}
public class OrderAuditor : IConsume<OrderCommand>, IConsume<OrderProcessedEvent>
{
public void Handle(OrderCommand message) { // Write to audit log }
public void Handle(OrderProcessedEvent message) { // Mark order as processed }
}
public class OrderApprover : IConsume<OrderProcessedEvent>, IConsume<ApproveOrderCommand>
{
public void Handle(OrderProcessedEvent message)
{
// Notify somebody that an order needs to be approved
}
public void Handle(ApproveOrderCommand message)
{
// Remove ticket from work queue
Events.Publish<OrderApprovedEvent>();
}
}
public class OrderNotifier : IConsume<OrderApprovedEvent>
{
public void Handle(OrderApprovedEvent message) { // Send an email }
}
I assume you get the idea. In this architecture, the number of dependencies for each handler is entirely limited to the actions that it is directly responsible for, and the one or two events that it consumes or publishes.
Of course, this means giving up the security blanket of having the entire workflow described in one place. Managers and business analysts really love their flow charts, and even programmers tend to get attached to the ability to see, at a glance, every step that’s involved in a particular process. The practical upside to the event-driven model is much looser coupling, testability, and the ability to expand the workflow “horizontally” without changing any existing handlers, or actually needing to understand or care about the exact order of events.
P.S. All of the frameworks I mentioned perform dependency injection on the handlers. You don’t instantiate the handlers, the framework does. That’s what makes this so different from a transaction script.
2