Is this "functional" approach the right way to Go?

315 views
Skip to first unread message

Levieux Michel

unread,
Oct 19, 2021, 4:17:10 AM10/19/21
to golang-nuts
Hi all,

I'm facing a particular issue in my company at the moment and can't seem to figure out how to fix it. Hope you can help. For the sake of IP, all code presented here will be crafted.

We have part of our code that performs operations with certain properties attached to a specific operation or sequence of operations. Guys at my company have different background languages and took kind of a functional approach (please correct any mistakes of mine) at solving this problem. Let's say I have a Dog structure and I want this dog to do things, but this dog can hold a bone of different sizes and weights. Each bone's set of properties is specific to a certain sequence of (possibly one) operations, and I don't want to mutate my initial Dog instance. In our code this is done with the following:


This presents different advantages, most notably that:
1. the code reads simply
2. it's fast to type
3. ... it answers our first needs (those given above)

But, as in any software system, we added complexity over time and now for our tests we mock the behavior of the Dog. So any function previously taking a Dog now takes a DogOperator (couldn't find a better name for this but naming's not the subject), which is an interface, and we have a Dog and FakeDog, that both implement DogOperator, the sole difference being that FakeDog really does nothing except faking... See the snippet updated:


The problem is that our functions use the WithBone method, and so this one can't return a Dog anymore if we want to be able to mock our Dog... So it should return a DogOperator:


Okay now this works but everytime we need to change the behavior of the Dog, say by adding a function that needs not be mocked anywhere, or a field that could a priori simply be accessed as is, we need to add a method to the method set of DogOperator and update every single implementation. In short, our interfaces (at least those following this pattern) are 100% coupled to their implementations (or vice versa), which I think is clearly not ideal.

Moreover, we have kind of a reflect bunch of hard-to-understand code that I'd like to remove, that checks for such patterns, as we are waiting for them to be followed in some cases, notable in our handlers.

The question I'm trying to answer is "How do I tackle this?", the goal being to avoid a whole rewrite of the code. Small incremental steps that can each be performed in a matter of days would be perfect but each time I think I found a working way, I hit a wall because I end up breaking one of our prior needs and need to do it all at once for things to keep working.

Note: our codebase is about 80K LOC, this is not *so much* but I can't afford to change everything at once, I need to find something to break this task down into smaller ones, hence my mail... 😅

I thank you all very, very much in advance for your time and energy (and advice!)

Have a wonderful day
Cheers

Brian Candler

unread,
Oct 19, 2021, 9:06:53 AM10/19/21
to golang-nuts
I think you should first decide what you want the final code to look like, before planning the incremental steps to get there.

However, are you sure really want to change everything to interfaces - purely to facilitate mocking, and for no other good architectural reason?  It doesn't sound right to me, but the "Dog" example is too abstract to know what you're really trying to achieve here.

You have a function that takes and/or manipulates a Dog, but the behaviour at test time can't be satisfied by a real Dog.  Why is that?  Is it because the Dog depends on some other object?  Maybe *that's* what needs mocking.

Maybe you do want an interface here to represent the behaviour that you're mocking.  But your comment talked about "adding a function that needs not be mocked anywhere, or a field that could a priori simply be accessed as is".  Surely those things would be private implementation details of a Dog, and not part of the interface?  Equally, if users of Dog are directly accessing fields, then they are tightly bound to the implementation of Dog anyway.

Maybe the behaviour you're talking about belongs outside of Dog altogether, like in a DogHandler object (which could have different implementations for Dog and FakeDog).

Levieux Michel

unread,
Oct 19, 2021, 12:07:13 PM10/19/21
to golang-nuts
Thanks Brian for your answer.
Indeed it is hard to convey the whole problem with just a metaphor... But your suggestion that maybe mocking the "contained" parts would work is helpful here. So if the Dog does not itself interact directly with critical systems (DB, git...) then we could provide a Dog that does exactly what it's supposed to do, just that some things it will do are mocked under the hood.

For the last part of your answer, I did not express myself clearly about the fields and methods of Dog that don't belong in the interface. Here there would be a method "Bone" in the interface "DogOperator" to retrieve the bone, only because the (exported) field is hidden behind the interface itself, which I find quite ugly to be honest.

The final state I would like is to have functions using a Dog everywhere with helpers taking (correctly designed) interfaces for particular behaviors eventually. So the functions using the Dog would use the Dog directly and send it to functions taking a DogOperator for factorable parts, that can be represented by the method set of those interfaces.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/07ddff7a-48e4-48fa-abe5-9683912e47aen%40googlegroups.com.

Levieux Michel

unread,
Oct 20, 2021, 4:00:20 AM10/20/21
to Filip Dimitrovski, golang-nuts
Thanks Filip,

I know about the builder pattern. I did not call it that for different reasons, first because generally the builder pattern implies generating instances of a struct with another struct (another type), which is not the case here. Second, this pattern is for creating objects with specific properties, here we're not as such "creating" objects (in practice we are but it's not mandatorily the goal), we're just deriving an object from another so as to direct a certain operation.
Using generics is out of scope since I think we will avoid them where possible and also because as you mentioned, they're not usable yet in production.

The idea of our WithXX pattern here is to reduce typing boilerplate and increase readability. The question I'm trying to answer here is how do I keep those advantages will decoupling (at least partially) my interfaces from their implementations?

Thank you very much for you time.

Le mar. 19 oct. 2021 à 19:02, Filip Dimitrovski <filipdimi...@gmail.com> a écrit :
The whole WithXX(attr) thing is known as a builder pattern. I won't dispute your use of it, but let's call it that.

A builder usually builds an object corresponding to a single implementation, although it can inject generic implementations inside. Let's say -- carBuilder.WithColor("red").WithYear(2021).WithEngine(genericEngineInterface).

Your mock objects shouldn't need a builder, because they don't hold much data anyway. So I'd restrict WithXX() for strictly producing a nice, concrete struct. Then comes the usage of the structs. If you'd really like one-builder-for-all, you'd need generics. In Go 1.18 this works:

type DogBuilder[T dogishType] struct {
   dog T
}

func (dog DogBuilder[T]) WithBone(bone Bone) T {
 ...
}


However, Go 1.18 is not released, and furthermore dogishType would still need setXX() for each T since struct members cannot be put as generic constraints (as far as I know).

Reply all
Reply to author
Forward
0 new messages