I have a requirement to have a component that will check input against some rules in a strict order. The order of the rules is defined by the business.
To keep example code simple, I'll try to define an analogy.
Let's say business defines order of some int values. You'll be given sequence of int numbers and requirement is to check if sequence contains a number from the numbers defined by the business. If it does, return the number, if none of those numbers are contained in sequence, return -1
. Checks should be made in order (this is important!).
For example:
Let's say business defines order of numbers to be checked as {3, 7, 5}
.
When input integer sequence (example {4, 6, 7}
) is received we should check in the defined order if number exists in received sequence. So in our example
- We check if number
3
exists in the sequence, clearly it doesn't - Then we check if number
7
exists in the sequence, it does, so we return 7
As I said this is simplified analogy, the rules in real application are much more complicated, so is the input.
My implementation is making use of chain of rules
pattern. There is a chain of rule, where each rule knows what's the next rule & upon checking input against it's logic, in case it's not satisfied, passes input to the next rule.
Implementation for our numbers analogy
public interface INumberRule
{
int GetNumber(IEnumerable<int> numbers);
}
INumberRule
interface defines API for our rules.
public sealed class ChainedRule : INumberRule
{
private INumberRule NextRule { get; }
private int Number { get; }
public ChainedRule(INumberRule nextRule, int number)
{
NextRule = nextRule;
Number = number;
}
public int GetNumber(IEnumerable<int> numbers)
{
if (numbers.Contains(Number))
return Number;
return NextRule.GetNumber(numbers);
}
}
ChainedRule
defines the skeleton of our rule. All rules are going to be ChainedRule
, except of chain termination rule.
public sealed class MinusOneRule: INumberRule
{
private MinusOneRule()
{ }
public int GetNumber(IEnumerable<int> numbers) => -1;
public static MinusOneRule Instance { get; } = new MinusOneRule();
}
Sole purpose of MinusOneRule
is to terminate the chain when none of the rules could satisfy their logic against input. As outlined above, if none of the numbers contained in the input sequence, we should return -1
. MinusOneRule
does just that.
We define factory that creates chain of rules:
public interface INumberRuleFactory
{
INumberRule GetChainedRules();
}
public sealed class ChainedNumberRuleFactory : INumberRuleFactory
{
public INumberRule GetChainedRules()
{
var minusOneRule = MinusOneRule.Instance;
var sevenRule = new ChainedRule(minusOneRule, 7);
var fiveRule = new ChainedRule(sevenRule, 5);
var threeRule = new ChainedRule(fiveRule, 3);
return threeRule;
}
}
And finally the usage:
public sealed class NumberProvider2
{
private INumberRule NumberRules { get; }
public NumberProvider2(INumberRuleFactory numberRuleFactory)
{
NumberRules = numberRuleFactory.GetChainedRules();
}
public int GetNumber(IEnumerable<int> numbers) => NumberRules.GetNumber(numbers);
}
class Program
{
static async Task Main(string[] args)
{
IEnumerable<int> numbers = new[] {4, 6, 7};
var numberRulesFactory = new ChainedNumberRuleFactory();
var numberProvider2 = new NumberProvider2(numberRulesFactory);
WriteLine($"Number Provider v. 2: {numberProvider2.GetNumber(numbers)}");
ReadLine();
}
}
I like this approach because it is robust, it clearly communicates intention & importance of the order.
Colleague of mine suggested that this might be too much code and instead it could be done using sequence of rules. This sequence is also created by a factory & factory is responsible for properly ordered number rules.
public interface IOrderedNumberSequenceFactory
{
IEnumerable<int> GetOrderedNumbers();
}
public sealed class OrderedNumberSequenceFactory : IOrderedNumberSequenceFactory
{
public IEnumerable<int> GetOrderedNumbers() => new[] {3, 7, 5};
}
And the usage:
public sealed class NumberProvider
{
private IEnumerable<int> OrderedNumbers { get; }
public NumberProvider(IOrderedNumberSequenceFactory orderedNumberLisProvider)
{
OrderedNumbers = orderedNumberLisProvider.GetOrderedNumbers();
}
public int GetNumber(IEnumerable<int> numbers)
{
foreach (int orderedNumber in OrderedNumbers)
{
if (numbers.Contains(orderedNumber))
return orderedNumber;
}
return -1;
}
}
class Program
{
static async Task Main(string[] args)
{
IEnumerable<int> numbers = new[] {4, 6, 7};
var rulesFactory = new OrderedNumberSequenceFactory();
var numberProvider = new NumberProvider(rulesFactory);
WriteLine($"Number Provider v. 1: {numberProvider.GetNumber(numbers)}");
}
}
I clearly can see this approach is less code, less components and easier to read & understand at first glance. However, what I don't like is it doesn't seem to communicate strongly the importance of rule order & it is easier to mess order with this implementation than with the previous one.
The opposite argument was if factory method that creates ordered numbers is properly named (e.g. GetOrderedNumbers()
), then it is clearly communicating that order is important here.
Any thoughts around those approaches with pros/cons ?
-1
becomes a valid result...IndexOf
do still rely on-1
values because the context of the method makes it clear that-1
will always be a nonsensical value. The same might apply to OP's real use case.