One approach would be to create a bunch of objects (or just functions) each of which takes an Order and only checks for the unique combination that is supposed to trigger a business outcome, put them in a list, and pass the order through the whole list.
These objects (or functions) should then produce desired side-effects if their respective condition is triggered. If the side-effects aren't costly or aren't something that would obstruct normal operation, design your objects to execute them immediately, otherwise, make the side effect schedule the task to be executed later (e.g. some emails might be sent in a batch at a specific time of day).
This way, you can specify these business rules independently and combine them however you like.
My code examples will be in C#, but I think you can get the idea from them.
Object version:
interface IOrderProcessor {
void Process(Order order);
}
// This is a member field somewhere:
List<IOrderProcessor> orderProcessors = new List<IOrderProcessor>();
// initialize it:
orderProcessors.Add(new PaidFurnitureProcessor());
orderProcessors.Add(new SomeOtherProcessor());
...
// In some method:
foreach (var processor in orderProcessors)
processor.Process(order);
// Then for each business rule:
class PaidFurnitureProcessor : IOrderProcessor {
public void Process(Order order) {
if (order.order_type == OrderType.ORDER_TYPE_RETAIL &&
order.order_category == OrderCategory.ORDER_CATEGORY_FURNITURE &&
order.order_status == OrderStatus.ORDER_STATUS_PAID)
{
// ...
}
}
}
Lambda version:
// This is a member field somewhere:
List<Action<Order>> orderProcessors;
// initialize it:
orderProcessors = new List<Action<Order>>() {
ProcessPaidFurniture,
ProcessSomethingElse,
...
};
// foreach loop:
foreach (var processor in orderProcessors)
processor(order);
// For each business rule:
void ProcessPaidFurniture(Order order) {
// do checks & side effects as before
}
If you need to reuse the same conditions for different side-effects, pass the side effect as a parameter (as a lambda). You'll have to work out the details depending on the needs of your project, though. If the lambda requires parameters that cannot be passed in by an IOrderProcessor, you'll have to find a way to create them somewhere else so that they can capture those parameters, or perhaps you can supply them as a data structure along with the Order instance.
Object Version:
// Add a lambda either to the constructor, or to the Process method.
// Here's the constructor version:
class PaidFurnitureProcessor : IOrderProcessor {
public PaidFurnitureProcessor(Action action) {
this.action = action;
}
public void Process(Order order) {
if(/* do checks */) {
this.action();
}
}
// ...
}
// then
orderProcessors.Add(new PaidFurnitureProcessor(() => SendEmail()));
orderProcessors.Add(new PaidFurnitureProcessor(() => DoSomethingElse()));
...
//------------------------------------------------
// if it makes more sense to pass the action to the Process method,
// change the IOrderProcessor interface, and then:
foreach (var processor in orderProcessors)
processor.Process(order, action);
Lambda version:
// For each business rule:
void ProcessPaidFurniture(Order order, Action action) {
if(/* do checks */) {
action();
}
}
//------------------------------------------------
// If it makes more sense to have preconfigured side effects:
orderProcessors = new List<Action<Order>>() {
(order) => ProcessPaidFurniture(order, () => SendEmail()),
(order) => ProcessSomethingElse(order, () => DoSomethingElse()),
...
};
// foreach loop:
foreach (var processor in orderProcessors)
processor(order);
//------------------------------------------------
// If it makes more sense to pass the action at call time:
orderProcessors = new List<Action<Order, Action>>() {
(order, action) => ProcessPaidFurniture(order, action),
(order, action) => ProcessSomethingElse(order, action),
...
};
// foreach loop:
foreach (var processor in orderProcessors)
processor(order, action);
Or some variation of that. Or a combination (e.g., the action can be a full blown object if the action itself has complex behavior).
Alternatively, you can make these into predicates and have them return a bool
, with true
indicating that the order instance satisfies the conditions. This would then allow you to sort these into different "buckets" (e.g., you could preprocess the orders to find all orders that have to do with paid furniture and put them in a single list, then send that list, that now contains only "paid furniture" orders, somewhere to be processed according to that specific business rule.
So, something like:
var paidFurnitureOrders = allOrders.Where(order => IsPaidFurniture(order)).ToList();
ProcessPaidFurnitureOrders(paidFurnitureOrders);
// elsewhere:
public bool IsPaidFurniture(Order order) {
bool result = order.order_type == OrderType.ORDER_TYPE_RETAIL &&
order.order_category == OrderCategory.ORDER_CATEGORY_FURNITURE &&
order.order_status == OrderStatus.ORDER_STATUS_PAID;
return result;
}
Again, this could also be an object if you think there's a need, or if it's more convenient, or if you prefer it. Note also that you can combine multiple rules via composition, because they all have the same signature:
public bool ComboPredicate(Order order) {
return IsPaidFurniture(order) && IsSomethingElse(order);
}
P.S. You can also achieve composition with objects, but you have to separate out the rule and the action. Here are two ways to do it.
One might look something like this (again, each lambda can be a full blown object if required).
Instead of an IOrderProcessor
interface that's implemented by different subtypes, you'd just have a single OrderProcessor
class that takes in a rule and an action:
class OrderProcessor
{
Function<Order, bool> rule;
Action<Order> action;
public OrderProcessor(Function<Order, bool> rule, Action<Order> action) {
this.rule = rule;
this.action = action;
}
public void Process(Order order) {
if(rule(order)) {
action(order);
}
}
}
Then, for the simpler (non-composite rules), define rules and corresponding actions in the same file, and provide some way to get an OrderProcessor that combines them, e.g. a factory function:
public static CreateFurnitureProcessor() {
return new OrderProcessor(paidFurnitureRule, paidFurnitureAction);
}
For rules defined in terms of other rules, you can create something like the ComboPredicate
above, and assign a different action in the corresponding factory.
Another way is to have an abstract base class that provides a protected interface that's only visible to inheritors, and that separates the check from the action:
abstract class OrderProcessor
{
public void Process(Order order) {
if(CheckRule(order)) {
PerformAction(order);
}
}
protected abstract bool CheckRule(Order order);
protected abstract void PerformAction(Order order);
}
Simple, non-composite rules just implement the two protected methods in a straightforward way. A composite rule can implement PerformAction
in the usual way, but for the check, it could create sub-rules (in the constructor (preferably), or right there in the method), and do:
protected override bool CheckRule(Order order) {
bool result = this.paidFurnitureProcessor.CheckRule(order) &&
this.someOtherProcessor.CheckRule(order));
// or an OR, XOR, something more complicated...
return result;
}
protected void PerformAction(Order order) { /* ... */ }
P.P.S. It really helps for code readability if you can find good names for methods/objects that implement these rules - you can try to infer some of them from the conditions you're checking, but ask the business people (domain experts) if you can, they likely already have terminology for these things.