Hi guys! I’ve been working on spring boot with kot...
# server
a
Hi guys! I’ve been working on spring boot with kotlin recently. I have a question around architecting a complex stepwise process - might be more general software than kotlin/spring. I’ve got a endpoint for placing an ecommerce order that has several steps involved (say 1, 2, 3, 4), and a lot of params in the request as well. Each one of these steps ends up transforming the initial request in a way that gives a complex output as a result - which I’ve modelled into their own output
data class
es, like
Step1Output
, which becomes the input for step 2, and so on. I’ve managed to make the step functions are pure functions this way, which I believe is a best practice. My problem now is that in case I need to introduce anything new in the output that requires another piece of data from the input, I need to modify all my
StepXOutput
classes and keep passing on the data between them. This obviously makes doing any changes slow and painful. Any ideas on design patterns that can help with this? Or is there some well-architected, complex project that I can look at for reference? Thanks!
n
might be more general software than kotlin/spring.
it is general design hence... 😶
2
a
I was checking here if there was something specific to kotlin or spring that could help with this 🤷
🚫 1
k
isn't your problem usage of data classes?
a
Is there some better data structure that I could use in this case?
k
well I don't fully understand your usecase, but it looks like inheritance could help you
q
@Amitav Khandelwal, how often are you expecting the outputs to change? If it's not often then it may not be worth changing anything because the pain may be too infrequent. Otherwise, if you feel you need to make a change then maybe strong typing is what is getting in your way. What if instead of using
data class Step1Output(...)
you just passed around a
Map<String, Any>
? You still need to deal with setting and accessing any new keys you put into the map later on, but nothing about how you transport the data needs to change. It's just a map. If you have a need for a nested map, i.e., your existing data classes aren't just full of primitives, then you could wrap the map in a class the lets you more easily access the nested values. For example:
Copy code
outputMap.getValue("/firstKey/secondKey/thirdKey")
instead of
Copy code
outputMap.get("firstKey")?.get("secondKey")?.get("thirdKey")
In projects I work on we've had a lot of success in the past using classes that delegate to a map as well.
data class Foo(map: Map<String, Any>): Map<String, Any> by map
You can add functions to
Foo
to make certain patterns of data access easier, but you still get to treat it like a map. You can even add functions to set and retrieve values in a type safe way if that's important to you.
n
Looks like your real problem is that you connected the data structures to the indices of the steps. Therefore, inserting into the list requires renumbering of all following steps. I would instead use names for the steps like
AddressVerificationInput
/
AddressVerificationOutput
. This makes it much clearer what the data structures are use for, and if you e.g. break that step into "billing address" and "home address" steps, you only have to change this step. Regarding using a map: we used that in our legacy system, and I hate it with passion: no type safety, refactoring is horribly complicated (esp. if developers get creative by creating "dynamic key computation functions"), and just try to find all the places where your "id" key/value pair is used.
e
You could use interfaces + delegation by implementation to have all your step classes have all properties:
This way you only need to add the property to one of the interfaces and override it in its respective data class
👀 1
q
That's a really nice idea @edrd
🤘 1
d
(late to party) - I had a similar problem with code generation from CSV schema. Many of the records were nearly but not quite identical. Contained many common subsets of fields like FirstName/LastName, Address(1,2,3) , OrderNumber etc. But they didn't really fit into a class hierarchy and the code generator didn't have the capability of modeling anything other then flat. An architecture I considered (but did not implement - yet) was to have the code generator (or manual) create an Interface Per Property interface LastName { val lastName : String } ... data class Order( override val firstName : String , override val lastName : String ) : .. , FirstName , LastName Given many data classes that shared fields they would now share interfaces Allowing to instead of copy, create arbitrary collections of fields in a class by interface and use it by refrence. interface FancyOrder : FirstName, LastName, OrderNumber .> val order : Order = order(...) val fancyOrder : FancyOrder = order // Works if FancyOrder has a subset of interface parents as Order One reason I didnt implement this is I could hear the compiler exploding in my head.