- awareness
- change is inevitable
- change related to sharing
- pros and cons
From the simplest "func main" to a large service. What
choices to consider when your project grows and how to keep it
on track for future changes.
We begin with a small project and then evolve it. Along
the way depending on the choices we make, the design
will change. I'll refer to directory layout, not as
structure but as design.
$ tree rebel
| Let's kick off our project. We'll name it "rebel" and use the repository domain "github.com/preferit/rebel". We choose to create a command, ie. package main. The implications for our ability to evolve Pros
Cons
|
You have no intention to share any logic, the command is for you alone. You are happy and code along the nice feature of randomizing rebelious statements.
| $ tree rebel At this point your coworkers Max and Lisa see the work and you end up in a discussion; |
- Max: Would be nice to see that phrase when I login as the message of the day - You: Easy peasy, just go install ... and run it. - Lisa: Can we include it on the intranet? - You: Hmm.. (you pause and start thinking) - You: Not really; you could use the binary, but it would be slow with all the traffic we have Here you are faced with a decision on how to share the logic of generating a random rebelious statement.
The first and second option will both require some effort. As the consumers are your friends the first seems more fitting and much easier to do. The third option, though viable, does not help this presentation :-). You go for option no. 1 |
How do you convert the current state, a command, into an importable package while also keeping the command. First attempt; keep command in root and create a package with logic to generate the phrases.
$ tree rebel
|
|
We now have multiple stakeholders depending on it and going forward they might get affected. Our goal is to be able to make changes as freely as possible without affecting the stakeholders. Before we release these changes can we improve the design?
$ tree rebel
|
What are the implications of our current design?
|
It feels ok, current needs are met. You and Max quickly notice that the same phrase appears over and over and add a the feature of keeping last shouted phrase in a temporary file to minimize the repetition when you run your commands.
|
|
Shortly after, Lisa swings by your office asking if you could implement something that doesn't repeat the same phrase every day? But you just did, how do you now share it with Lisa? At this point you'll realize that the initial redesign with the added phrase package, may need to change and your own code updated. This is one of those deadends where you have to turn around and find another path.
We can explore ways to use the current design, e.g. put some caching mechanism into the phrase package, but that seems out of place. Also the caching for the command invocation locally on your computer may differ from what is available on the intranet site, you don't know at this point. If we backtrack to the time where Max and Lisa came onboard, could we have taken a different route that would avoid this scenario?
Let's go through our reasoning and see if we can find the culprit before trying to solve it. Lisa and Max where not very specific about their needs just that they wanted to show a random phrase in different ways, in the terminal and on a webpage. We choose to extract the phrase generating logic for Lisa to use and told Max to use the command. Then we added more logic in the command, because it was You and Max that saw the problem. What Lisa wants now is the logic that is found in the command, but her current dependency is elsewhere.
Looking at the code more closely we placed func Shout inside the phrase package, why? phrases don't shout, rebels do. So if we want to keep func Shout in the package rebel And we want to share it with Lisa, how do we solve that?
$ tree rebel
|
|
$ tree rebel
|
|
This design, is as effortless as the first attempt at the decision point, but it makes it easier to evolve the rebel logic. It also forces you to design rebel features in such a way that they may be used by Lisa as well as Max.
With this design, if Lisa came along with the same request; you could easily tell her to set MinimizeRepetition to true.
import github.com/preferit/rebel
go install github.com/preferit/rebel/cmd/rebel@latest
The project will grow and at some point Lisa comes by and
says they are switching languages for the intranet
implementation but they really want to have access to the
logic of generating rebelious statements. Could you write a
service that exposes it?
Easy peasy, but where to put it?
we a few choices
1Add the service logic in existing command | 2Add the service logic in package rebel | 3Add the service logic in package rebel/service | 4New command only | 5Combine option 3 and 4 |
The project will grow and at some point Lisa comes by and
says they are switching languages for the intranet
implementation but they really want to have access to the
logic of generating rebelious statements. Could you write a
service that exposes it?
Easy peasy, but where to put it?
we a few choices
1Add the service logic in existing command$ tree rebel Pros
Cons
| 2Add the service logic in package rebel | 3Add the service logic in package rebel/service | 4New command only | 5Combine option 3 and 4 |
The project will grow and at some point Lisa comes by and
says they are switching languages for the intranet
implementation but they really want to have access to the
logic of generating rebelious statements. Could you write a
service that exposes it?
Easy peasy, but where to put it?
we a few choices
1Add the service logic in existing command$ tree rebel Pros
Cons
| 2Add the service logic in package rebel$ tree rebel Pros
Cons
| 3Add the service logic in package rebel/service | 4New command only | 5Combine option 3 and 4 |
The project will grow and at some point Lisa comes by and
says they are switching languages for the intranet
implementation but they really want to have access to the
logic of generating rebelious statements. Could you write a
service that exposes it?
Easy peasy, but where to put it?
we a few choices
1Add the service logic in existing command$ tree rebel Pros
Cons
| 2Add the service logic in package rebel$ tree rebel Pros
Cons
| 3Add the service logic in package rebel/service$ tree rebel Pros
Cons
| 4New command only | 5Combine option 3 and 4 |
The project will grow and at some point Lisa comes by and
says they are switching languages for the intranet
implementation but they really want to have access to the
logic of generating rebelious statements. Could you write a
service that exposes it?
Easy peasy, but where to put it?
we a few choices
1Add the service logic in existing command$ tree rebel Pros
Cons
| 2Add the service logic in package rebel$ tree rebel Pros
Cons
| 3Add the service logic in package rebel/service$ tree rebel Pros
Cons
| 4New command only$ tree rebel Pros
Cons
| 5Combine option 3 and 4 |
The project will grow and at some point Lisa comes by and
says they are switching languages for the intranet
implementation but they really want to have access to the
logic of generating rebelious statements. Could you write a
service that exposes it?
Easy peasy, but where to put it?
we a few choices
1Add the service logic in existing command$ tree rebel Pros
Cons
| 2Add the service logic in package rebel$ tree rebel Pros
Cons
| 3Add the service logic in package rebel/service$ tree rebel Pros
Cons
| 4New command only$ tree rebel Pros
Cons
| 5Combine option 3 and 4$ tree rebel Let's go with this one. Lisa got her service and Max is unaffected by the change. |
The service is up and running and you get an idea of adding a feature to package rebel for scanning the web for statements that the rebel can shout. It doesn't feel like the feature fits in the rebel package directly but will be used by it. Let's call the feature crawl, but where to put it?
1Add it directly in package rebel | 2Add to a sub package | 3Add it to an internal/crawl package |
The service is up and running and you get an idea of adding a feature to package rebel for scanning the web for statements that the rebel can shout. It doesn't feel like the feature fits in the rebel package directly but will be used by it. Let's call the feature crawl, but where to put it?
1Add it directly in package rebel$ tree rebel Pros
Cons
| 2Add to a sub package | 3Add it to an internal/crawl package |
The service is up and running and you get an idea of adding a feature to package rebel for scanning the web for statements that the rebel can shout. It doesn't feel like the feature fits in the rebel package directly but will be used by it. Let's call the feature crawl, but where to put it?
1Add it directly in package rebel$ tree rebel Pros
Cons
| 2Add to a sub package$ tree rebel Pros
Cons
| 3Add it to an internal/crawl package |
The service is up and running and you get an idea of adding a feature to package rebel for scanning the web for statements that the rebel can shout. It doesn't feel like the feature fits in the rebel package directly but will be used by it. Let's call the feature crawl, but where to put it?
1Add it directly in package rebel$ tree rebel Pros
Cons
| 2Add to a sub package$ tree rebel Pros
Cons
| 3Add it to an internal/crawl package$ tree rebel Pros
Cons
|
You've come a long way since that early func
main
. The service is growing as Lisa finds new features
to add. At some point you need to bring in more people to work
on the service side. Initially you might start working in the
same repository but down the road it may make more sense to
split the service into it's own. The latter is what we're
interested in.
$ tree rebel |
The design for change requires awareness of where we might end up in the future and selecting a route that enables it as frictionless as possible. Ie. if we'd selected option 4 when adding the service, the move at the end would have been even easier at the expense of mixing service logic with command logic. Depending on the service this may be a good tradeof.
You see these layerd packages e.g. net/http and encoding/json
|