I attempted the Business Rules Kata. Here's a video overview.
However, I am not confident that going functional is a good strategy for the following objective:
How can you tame these wild business rules? How can you build a system that will be flexible enough to handle both the complexity and the need for change? And how can you do it without condemning yourself to years and years of mindless support?
Is there an alternative FP approach that satisfies the objective stated above?
module PaymentSystem
(*Types*)
type ProductId = ProductId of string
type MemberId = MemberId of string
type Email = Email of string
type Agent = Agent
type RoyaltyDepartment = RoyaltyDepartment
type PackingSlip = {
MemberId:MemberId
ProductId:ProductId
}
type PhysicalProducts =
| Book
| Video
| Other
type MembershipType =
| Membership of MemberId
| Upgrade of MemberId
type PaymentFor =
| PhysicalProduct of PhysicalProducts * PackingSlip
| Membership of MembershipType
type PackingSlipOptions =
| PackingSlip of PackingSlip
| DuplicateSlips of PackingSlip
| WithFirstAidVideo of PackingSlip
type PaymentResponse =
| PackingSlip of PackingSlipOptions
| ActivateMembership of MemberId
| UpgradeMembership of MemberId
| EmailOwner of MembershipType
| CommissionPayment of Agent
(*Functions*)
let publish payload = () // Stub
let getAgent productId = Agent // Stub
let respondTo (payment:PaymentFor) =
match payment with
| PhysicalProduct (kind , packingSlip) ->
publish (CommissionPayment (getAgent packingSlip.ProductId))
match kind with
| Book -> publish (DuplicateSlips packingSlip)
| Video -> publish (WithFirstAidVideo packingSlip)
| Other -> publish packingSlip
| Membership kind ->
publish(EmailOwner kind)
match kind with
| MembershipType.Membership memberId -> publish(ActivateMembership memberId)
| MembershipType.Upgrade memberId -> publish(UpgradeMembership memberId)
1 Answer 1
The original requirement
How can you tame these wild business rules? How can you build a system that will be flexible enough to handle both the complexity and the need for change? And how can you do it without condemning yourself to years and years of mindless support?
seems, to me, to insinuate some sort of generic rules engine. This is, I believe, a red herring. The problem with rules engines, in my experience, is that the selling point is always that 'business users' can maintain them without involving developers.
This is a pipe dream, because if business rules can be arbitrarily complex, you're going to need a rules engine that can handle all of this complexity. In other words, you're going to need a programming language, and then it becomes too technical for business users.
This is called the inner platform effect and is, in my opinion, best avoided.
So you might as well use a programming language to implement the rules, and I see no reason you can't use a functional language like F#.
Here's a sketch of some of the rules. Like in the real world, some of the rules are vaguely defined, so I wasn't sure how to interpret them...
type Membership = Basic | Gold
type Good =
| PhysicalProduct of string
| Book of string
| Video of string
| Membership of Membership
| Upgrade
type Command =
| Slip of string * (Good list)
| Activate of Membership
| Upgrade
| PayAgent
These are simply some types, but given these types, you can implement various business rules, e.g.
// Good -> Command list
let slipForShipping = function
| PhysicalProduct name
| Book name
| Video name -> [Slip ("Shipping", [PhysicalProduct name])]
| _ -> []
// Good -> Command list
let slipForRoyalty = function
| Book name -> [Slip ("Royalty", [Book name])]
| _ -> []
// Good -> Command list
let activate = function | Membership x -> [Activate x] | _ -> []
// Good -> Command list
let upgrade = function | Good.Upgrade -> [Upgrade] | _ -> []
You'll notice that all these rules have the same type, which means that you can collect all of them:
// ('a -> 'b list) list -> 'a -> 'b list
let handle handlers good = handlers |> List.collect (fun h -> h good)
// Good -> Command list
let handleAll = handle [slipForShipping; slipForRoyalty; activate; upgrade]
Example:
> handleAll (Book "The Annotated Turing");;
val it : Command list =
[Slip ("Shipping",[PhysicalProduct "The Annotated Turing"]);
Slip ("Royalty",[Book "The Annotated Turing"])]
As far as I can tell, this would be fairly maintainable, because if you need to add a new rule, you'll have to add a new function, and add that function to handleAll
.
-
4\$\begingroup\$ Regarding the Inner Platform Effect, this blog post may also be of interest: mikehadlow.blogspot.co.uk/2012/05/… "Soon enough you’ll find that there’s little difference in the length of time it takes between changing a line of code and changing a line of configuration. Rather than a commonly available skill, such as coding C#, you find that your organisation relies on a very rare skill: understanding your rules engine or DSL." \$\endgroup\$TheQuickBrownFox– TheQuickBrownFox2016年12月12日 10:13:17 +00:00Commented Dec 12, 2016 at 10:13
-
\$\begingroup\$ Feels like the Decorator Pattern... \$\endgroup\$Scott Nimrod– Scott Nimrod2016年12月12日 17:20:59 +00:00Commented Dec 12, 2016 at 17:20
-
1\$\begingroup\$ @ScottNimrod Not really; it's the Composite pattern... \$\endgroup\$Mark Seemann– Mark Seemann2016年12月12日 17:22:11 +00:00Commented Dec 12, 2016 at 17:22
-
2\$\begingroup\$ @ScottNimrod FWIW, I've now elaborated on the relationship to the Composite design pattern. \$\endgroup\$Mark Seemann– Mark Seemann2018年05月17日 06:56:28 +00:00Commented May 17, 2018 at 6:56