Let us suppose we have an Order class and a method in which some services make a sequence of operations
fun doStuff(order: Order): Order {
val orderByServiceA = serviceA.operationA(order: Order)
val orderByServiceB = serviceB.operationB(orderByServiceA: Order)
val orderByServiceC = serviceC.operationC(orderByServiceB: Order)
return orderByServiceC
}
I create a common interface who can be implemented by the services
interface IOrderService {
fun operation(order: Order): Order
}
So the method above become
fun doStuff(order: Order): Order {
val orderByServiceA = serviceA.operation(order: Order)
val orderByServiceB = serviceB.operation(orderByServiceA: Order)
val orderByServiceC = serviceC.operation(orderByServiceB: Order)
return orderByServiceC
}
There's some kind of repetition in this code so, next step is to make this function open for extension and closed for modification. I add into a list all the instances of IOrderService, so the function changes like
fun doStuff(order: Order): Order {
orderServices.forEach { service ->
service.operation(order)
}
...
}
This solution is quite good because if I want to add another service who implement the operation function, it's very easy to do it.
But there is a problem.. the order object passed in the last solution it's the same for all the services so the solution proposed it's different from the first implementation. Every service need to do the operation function based on the previous result
So I'm quite stuck at this point, because I would like to find a clean solution.. I was thinking at some design pattern for example Observer (but it's a one-to-many solution and it's not the case) Memento pattern (but probably I would continue to have a sequential list of operations), publish subscribe... but I don't think any of those are the right choise. It's like a domino in which the object is passed to a first service, the result to the second one, etc. etc. and of course the final result returned by the doStuff method.
Any suggestions to complete the last piece of code? Thanks a lot!
CodePudding user response:
I think you really overcomplicate with all these patterns. What you described is... just a simple loop!
fun doStuff(order: Order): Order {
var curr = order
for (service in orderServices) {
curr = service.operation(curr)
}
return curr
}
If you like functional programming then it can be solved even simpler by reducing/folding the list of services:
fun doStuff(order: Order): Order {
return orderServices.fold(order) { o, svc -> svc.operation(o) }
}
See documentation for more info about fold()
: https://kotlinlang.org/docs/collection-aggregate.html#fold-and-reduce
CodePudding user response:
@broot's answer covers the general way of doing it—like they say, fold
is the basic operation for accumulating a result as you iterate over some stuff—but if you wanted to write it out explicitly, you can just chain your calls:
fun doStuff(order: Order) =
order.let { serviceA.operationA(it) }
.let { serviceB.operationB(it) }
.let { serviceC.operationC(it) }
And if you use function references instead, where the parameter or receiver is automatically passed to the function you're referencing, you can do this:
fun doStuff(order: Order) =
order.let(serviceA::operationA)
.let(serviceB::operationB)
.let(serviceC::operationC)
So you end up with this very clear pipeline of operations that the original Order
is being passed through, step by step.
The other nice thing about this is your types don't need to be consistent: operationB
could take a completely different parameter from operationA
, all that matters is that what comes out of operationA
is the same type that goes into operationB
. Whereas with a fold, the accumulator—the value being passed from one step to the next—is a fixed type, and the collection of functions you're iterating over have a fixed type too; in this case they all need to take an Order
and return an Order
. They're both useful, and you have options—use whatever fits the situation!