A DSL For Handling Zero, One, Many
All this talk about refactoring to clean up code and monads to create pipelines has put me on a code composition kick… I wrote this code today:
1: private AddAssetResult HandleAssetFamily(SortContainer container, SystemAsset systemAsset)
2: {
3: AddAssetResult result = null;
4: var assetFamilies = assetFamilyDao.GetByAssetType(systemAsset.AssetTypeId);
5:
6: if (assetFamilies == null || assetFamilies.Count == 0)
7: {
8: result = new AddAssetResult(SortContainerResources.NoAssetFamily);
9: }
10:
11: if (assetFamilies != null && assetFamilies.Count > 1)
12: {
13: result = new AddAssetResult(SortContainerResources.MultipleAssetFamilies);
14: }
15:
16: if (assetFamilies != null && assetFamilies.Count == 1)
17: {
18: var assetFamily = assetFamilies[0];
19: SetContainerFamily(container, assetFamily);
20:
21: result = VerifyFamilyIsCompatible(systemAsset, container);
22: }
23:
24: return result;
25: }
</div> </div>
Being the good little geeky me that I am, I wanted to use my shiny new monads hammer to pound this code into shape. As I was thinking about the monads that I have come across in my reading and my current understanding of them, though, I decided that I should probably see if this problem is actually a nail before whacking it with my hammer. So… rather than forcing myself to do something that might be dumb, I decided to approach this code from a composition standpoint – because after all, that’s one of the things a monad gives me.
Zero, One, Many
As I thought about the problem, I remembered an article that I read a long time about that talked about the three significant numbers: zero, one and many. The idea is that most of the time, you only need to pay attention to these three numbers. In looking at the above code, you can see that these are the exact numbers that I am paying attention to. This popped an idea for a DSL into my head and I pseudo-coded this into existence:
1: var result = assetFamilies.CheckCount<AddAssetResult>()
2: .WhenZero(() => new AddAssetResult(SortContainerResources.NoAssetFamily))
3: .WhenOne(() => new AddAssetResult(SortContainerResources.MultipleAssetFamilies))
4: .WhenMany(AddAssetWithFamily);
</div> </div>
The idea was to have a fluent interface / DSL that allows me to pay attention to the three significant numbers that I need in my above scenario and compose the behavior of the system by stating what would happen for each of those numbers. It seems like a pretty good idea and I like the syntax (all C# ceremony aside, of course).
Adjusting For Reality
I expected to have to adjust this code for reality, of course. I wrote that above example in notepad just to bang it out without worrying about proper syntax and the actual constraints of C#.
I found out that I needed to specify two generics parameters to the CheckCount extension method. I originally wanted to use IEnumerable as the “this” parameter of the extension method, but there is no Count on it and no way to get one without looping over the entire enumeration. So, I decided to accept that I needed to pass in an IEnumerable
</div> </div>
Which should read like “check the count of asset families, and return an add asset result based on the count”.
The next thing I had to do was add a .Result property to the DSL so that I could get the result out of the method chains. I also had to adjust the WhenOne method I was calling and I realized that I had the WhenOne and WhenMany cases switched. Fixing those items makes the entire call chain look like this:
</div> </div>
Not too bad. It’s much better than the original code that I started with and I can live with this.
I started with an extension method for the CheckCount call and then built out a small class called CountChecker. This class does the actual work of deciding which of the numeric methods should have it’s Func
</div> </div>
It’s a simple implementation. Nothing terribly fancy, other than getting the count out of the IEnumerable
I mentioned that I wanted to use my shiny new monad hammer on this problem, at the beginning of this post. In the end, I don’t think I did. I’m fairly certain that what I’ve implemented is just simple method chaining and/or a fluent interface to create a DSL. Does anyone have an opinion and expertise to say otherwise? I’d be interested in hearing why you think this is, or know it is not, a monad (I realize that I’m wearing monad colored glasses these days. I’m sure this obsession will pass soon enough).
If you’re interested in the source for this DSL and example, I put it up on Github’s gists. You can grab it all, here: https://gist.github.com/5cb0241ea0123b76fbb1
1: var result = assetFamilies.CheckCount<AssetFamily, AddAssetResult>()
1: var result = assetFamilies.CheckCount<AssetFamily, AddAssetResult>()
2: .WhenZero(() => new AddAssetResult(SortContainerResources.NoAssetFamily))
3: .WhenOne(() => AddAssetWithFamily(systemAsset, container, assetFamilies[0]))
4: .WhenMany(() => new AddAssetResult(SortContainerResources.MultipleAssetFamilies))
5: .Result;
Implementing The DSL
1: public static class CountCheckerExtensions
2: {
3: public static CountChecker<TResult> CheckCount<TCollectionType, TResult>(this IEnumerable<TCollectionType> enumerable)
4: {
5: int theCount = 0;
6: if (enumerable != null)
7: theCount = enumerable.Count();
8: return new CountChecker<TResult>(theCount);
9: }
10: }
11:
12: public class CountChecker<TResult>
13: {
14: private readonly int theCount;
15:
16: public TResult Result { get; private set; }
17:
18: public CountChecker(int theCount)
19: {
20: this.theCount = theCount;
21: }
22:
23: public CountChecker<TResult> WhenZero(Func<TResult> zeroAction)
24: {
25: if (theCount == 0)
26: Result = zeroAction();
27:
28: return this;
29: }
30:
31: public CountChecker<TResult> WhenOne(Func<TResult> oneAction)
32: {
33: if (theCount == 1)
34: Result = oneAction();
35:
36: return this;
37: }
38:
39: public CountChecker<TResult> WhenMany(Func<TResult> manyAction)
40: {
41: if (theCount > 1)
42: Result = manyAction();
43:
44: return this;
45: }
46: }
Fluent Interface And DSL, But I Don’t Think This Is A Monad
Get The Source