Cross Business Logic using Subscribers in Golang

Faris M. Syariati
Sep 8, 2018 · 8 min read

Go is very flexible. Like what South Jakarta people will said, “I literally mean it”.

Because it’s flexibility, it is a challange to sustain go idiomaticity. Lets said you have 10 people in your team code in Go, everyone has its own way, and in some point, your code does not make any sense anymore. But hey, we have the 11th person here :p

How about design pattern? like what we have since years ago, Okay, lets not making any more bloodshed.

A. Maintain Go Idiomaticity

Is… very hard to me... The best thing to maintain it, is not to bring any other language terminology. Words like factory, bridge, adapter, etc should be cast away. Trust me, Go well known libraries actually use those pattern, but pstt… they don’t explicitly tell them.

B. Making Go Project Testable

Is easy, but only if the code is well-maintained.

The first key is to make it as independent as possible. I love dependency injection schema. It helps most of my project in Go. At least it tell me what to mock and how to achieve a higher test coverage.

The second key is no more global variable and lazy init. Those things makes the code very hard to test, and there is no way, to limit dependency to it. Lets use a singleton instead using Go-way. sync.

The third key is bound the handler and use-case (business logic) together. “hey, we can use factory for it”. Yes you sure can, but lets cast it aside for this language. So lets just use a Go closure / anonymous function. “Thats actually…”, “Uh, shut up”.

The fourth key is make the logic accessable to others elegantly. Your product owner, may be very keen to boost the product or business, and keep asking you to perform changes. Here we need to avoid endless refactor.

C. Lets Code

  1. Defining Components

First of all, lets define the server definition using simple interface, such as:

type Server interface {
Run() error
}

Then. we can implement this server for most of service such as HTTP, gRPC, Message Queue, and etc.

server/http.go

type httpServer struct {
ui *uires.UIResource
router *httprouter.Router
port int
}
func InitHTTPServer(ui *uires.UIResource, port int) Server {
return &httpServer{
ui: ui,
port: port,
router: ui.Router,
}
}
func (h *httpServer) Run() error {
return gracehttp.Serve(&http.Server{
Addr: fmt.Sprint("0.0.0.0:", h.port),
Handler: h.router,
})
}

So you will question it, what is UIResource. It is just a simple holder struct which holds all component needed by handler or server. In this code we are going to put all of those component and inject them to Resource structure.

Here we are going to have 2 type of structure: UIResource and second one is UseCaseResource .

resource/uires/resource.go

type UIResource struct {
UseCaseResource *usecaseres.UseCaseResource
Router *httprouter.Router
}
func (u *UIResource) RenderJSON(w http.ResponseWriter, data interface{}, statusCode int) {
byteData, _ := json.Marshal(data)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(statusCode)
w.Write(byteData)
}
func (u *UIResource) GetPostData(r *http.Request, v interface{}) error {
if r.Method == http.MethodPost {
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
return err
}
return nil
}
return fmt.Errorf("method %s not allowed %d", r.Method, http.StatusMethodNotAllowed)
}

See. This code is just a simple helper where we can put all magic here. All UI related components can be put here, including router if you would.

Also, since we are going to put use case and handler together, we need to knit the UseCaseResource inside the UIResource .

resource/usecaseres/resource.go

type UseCaseResource struct {
//put all usecase component here, i.e
//db repo etc
}

UseCaseResource is just another struct which holds all dependecies needed by usecase / business logic. Such as: database repo, redis repo, or another libraries needed by the business logic.

If everything has been set, then now we are going to go to next phase which is to define the handler.

2. Define the Handler

Handler, or maybe you can call it as a controller (ups), is a function where you can attach it to a certain route, and perform an action.

The main idea to this method is to attach a certain handler, not juts to a route, but also to a use-case, and its subscribers. You may think it is very weird, why dont we just write the logic directly to handler right. But thats not answering the issues address at the beginning at this article.

First, lets define how route function should be

server/server.go

type httpHandlerFunc func(ui *uires.UIResource, request *http.Request, useCase usecase.HandleUseCase, subscribers ...usecase.UseCase) (interface{}, error)

This will be a contract for how the handler function should be.

Secondly, we are going to define how the logic should look like

usecase/usecase.go

The use-case/ logic layer should be more or less like this

type HandleUseCase func(ctx context.Context, res *usecaseres.UseCaseResource, data *UseCaseData) (interface{}, error)type UseCase interface {
HandleUseCase(ctx context.Context, res *usecaseres.UseCaseResource, data *UseCaseData) (interface{}, error)
Notify(ctx context.Context, res *usecaseres.UseCaseResource, data *UseCaseData) error
}
type UseCaseData struct {
HTTPData interface{}
Subscribers []UseCase
}

Now, we need an abstraction for use-case, since we are going to implement a use-case subscribers pattern here. As you can see, we have a data holder called UseCaseData in this structure, it will holds data from the request handler called HTTPData as interface, and it also has slice of subscriber which refer to UseCase itself.

Now, back to http.go we are going to create a middleware to handle the route, which is able to bind a certain use-case alongside with it.

func (h *httpServer) handle(uiRes *uires.UIResource, httpHandlerFunc httpHandlerFunc, useCase usecase.HandleUseCase, subscribers ...usecase.UseCase) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
response, err := httpHandlerFunc(uiRes, request, useCase, subscribers...)
if err != nil {
h.ui.RenderJSON(writer, err.Error(), http.StatusInternalServerError)
return
}
h.ui.RenderJSON(writer, response, http.StatusOK)
}
}

Now, we can create a custom use-case with by wrapping it to this middleware.

3. Define the Handler

In this part, we are going start doing the real job. First of all, we are going to create a handler to perform a certain task called DoSomething . Then, we are going to create the handler called DoSomethingHandler .

func DoSomethingHandler(ui *uires.UIResource, request *http.Request, doSomethingUseCase usecase.HandleUseCase, subscribers ...usecase.UseCase) (interface{}, error) {
data := new(usecase.UseCaseData)
err := ui.GetPostData(request, &data.HTTPData)
if err != nil {
return nil, err
}
data.Subscribers = subscribers
return doSomethingUseCase(request.Context(), ui.UseCaseResource, data)
}

Very short. And clear. As you can see, it does not have meaning at all, except it just accept the request data, read it, put it to the UseCaseData and pass it to the usecase function. In this coding project, I would like to emphasize that handler should not do a rigorous and complex job. The handler should handle the HTTP request only, and the next job, should be taken care by another layer. Thats it.

4. Define the Use Case

Another real job to do is to create the logic. The logic part (use-case) will be created using a singleton, and it is implementing UseCase interface which has been made previously.

usecase/do_something.go

var doSomethingUC usecase.UseCase
var doSomethingOnce sync.Once

func GetDoSomethingUseCase() usecase.UseCase {
doSomethingOnce.Do(func() {
doSomethingUC = &doSomethingUseCase{}
})
return doSomethingUC
}

func GetHandleDoSomething() usecase.HandleUseCase {
return GetDoSomethingUseCase().HandleUseCase
}

type doSomethingUseCase struct{}

func (i *doSomethingUseCase) HandleUseCase(ctx context.Context, res *usecaseres.UseCaseResource, data *usecase.UseCaseData) (interface{}, error) {
return "test", nil
}

func (i *doSomethingUseCase) Notify(ctx context.Context, res *usecaseres.UseCaseResource, data *usecase.UseCaseData) error {
fmt.Println("notify on do something do something use case")
return nil
}

In this use-case, we will return a simple string only. This structure has 2 main part: HandleUseCase, and Notify. HandleUseCase function will be bounded to the handler. Thats the point, and Notify will be called by another use-case which subscribed the event here.

5. Knitting Handler and Use Case

Since we have both use-case and handler, now we can create a routing part of the HTTP handler. Now back to server/http.go

func (h *httpServer) registerV1() {
h.router.HandlerFunc(http.MethodPost, "/v1/do/something", h.handle(h.ui, server.DoSomethingHandler, usecase.GetHandleDoSomething()))
}
func (h *httpServer) Run() error {
h.registerV1()
return gracehttp.Serve(&http.Server{
Addr: fmt.Sprint("0.0.0.0:", h.port),
Handler: h.router,
})
}

Now you can see that the current handler will be bounded to the usecase directly. It means once we access the route, the handler will tell the use-case to perform an action. Easy.

So whats the point of this pointless another code design. Well I dare to tell you that it is not totally pointless.

Since we are able to split the functionalities of components apart, now we can test it easily. To test the handler, you can create an anonymous function implementation of the UseCase.HandleUseCase function inside the handler, since it is actually an interface :).

And, for the UseCase part, the trick is, since we use dependency injection. It is safe to tell, we can make sure the injected dependency are mockable. Or else, that is another homework for the developer.

6. Subscribing a Use-Case

So what is another plus of this design? The answer is we can vendor the functionalities of another use-case.

Sample Case:

After DoSomethingUseCase perform something, we expect another use-case to be executed. Lets say DoAnotherElseUseCase which at the first hand were developed by your friend.

The problem is, those use-case are totally in different package. And it is using totally different resources, repo, and etc. So now you have two options:

  1. Use DoAnotherElseUseCase component such as repo, library, and etc. It means the DoSomethingUseCase will import the component of DoAnotherElseUseCase and use it right away.
  2. Move DoAnotherElseUseCase component to DoSomethingUseCase and make the ownership to the DoSomethingUseCase.

The thing is, I will personally escape for both option. We need to treat the job clean, and I don’t want to responsible with another person tasks. Refactor is the last thing that we need to do, when changes happens.

Another approach is, by using a subscribers. And we arleady ready to do that at this point.

Next, lets create another else use-case:

usecase/do_another_else.go

var doAnotherElseUC usecase.UseCase
var doAnotherElseOnce sync.Once

func GetDoAnotherElseUseCase() usecase.UseCase {
doAnotherElseOnce.Do(func() {
doAnotherElseUC = &doAnotherElseUseCase{}
})
return doAnotherElseUC
}

func GetHandleDoAnotherElse() usecase.HandleUseCase {
return GetDoAnotherElseUseCase().HandleUseCase
}

type doAnotherElseUseCase struct{}

func (i *doAnotherElseUseCase) HandleUseCase(ctx context.Context, res *usecaseres.UseCaseResource, data *usecase.UseCaseData) (interface{}, error) {
return "test", nil
}

func (i *doAnotherElseUseCase) Notify(ctx context.Context, res *usecaseres.UseCaseResource, data *usecase.UseCaseData) error {
fmt.Println("notify on do another else use case")
return nil
}

Then, lets make it subscribe the DoSomethingUseCase on the handler route definition.

server/http.go

func (h *httpServer) registerV1() {
sampleSubscribers := []usecase.UseCase{usecase.GetDoAnotherElseUseCase()}
h.router.HandlerFunc(http.MethodPost, "/v1/do/something", h.handle(h.ui, server.DoSomethingHandler, usecase.GetHandleDoSomething(), sampleSubscribers...))
}

voilà…

Now we are making DoAnotherElseUseCase subscribe the action of DoSomethingUseCase . But this is not the final. We need to make the DoSomethingUseCase trigger the DoAnotherElseUseCase at a certain event.

usecase/do_something.go

func (i *doSomethingUseCase) HandleUseCase(ctx context.Context, res *usecaseres.UseCaseResource, data *usecase.UseCaseData) (interface{}, error) {
data.Subsribers[0].Notify(ctx, res, data)
return "test", nil
}

Done!

By using this simple call. This use case will trigger DoAnotherElseUseCase Notify function. And the PIC of that task/use-case can perform something else there.

Wrap Up

If you have been into design pattern, you may thing it is very cheesy. Yes. I understand you will. Because it is actually the implementation of Observer design pattern.

But the question is, if you need it and it does not hurt you, why should we avoid it. It is very simple.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade