Question

I'm trying to build a project using the clean architecture, as described here. I found a great article on how to do this in Go.

The example is a very simple one, and the author puts their code into packages named based on the layer they are in. I like Uncle Bob's idea that an application's architecture should clearly communicate its intent. So I'd like my application to have top-level packages based on domain areas. So my file structure would look something like this:

/Customers
    /domain.go
    /interactor.go
    /interface.go
    /repository.go
/... the same for other domain areas

The problem with this is that multiple layers share the same package. So it's not all that clear when the dependency rule is being violated, because you don't have imports showing what depends on what.

I'm coming from a Python background, where this wouldn't be as much of an issue, because you can import individual files, so customers.interactorcould import customers.domain.

We could achieve something similar in gO by nesting packages, so that the customers package contains a domain package and an interactor package, and so on. This feels clunky, and identically named packages can be annoying to deal with.

Another option would be to make multiple packages per domain area. One called customer_domain, one called customer_interactor, etc. But this feels dirty as well. It doesn't fit well with Go's package naming guidelines, and looks like all of these separate packages should somehow be grouped, since their names have a common prefix.

So what would be a good file layout for this?

Was it helpful?

Solution

There are a few solutions to this:

  1. Package Separation
  2. Review Analysis
  3. Static Analysis
  4. Runtime Analysis

Each having their pros/cons.

Package Separation

This is the easiest way that doesn't require building anything extra. It comes in two flavors:

// /app/user/model/model.go
package usermodel
type User struct {}

// /app/user/controller/controller.go
package usercontroller
import "app/user/model"
type Controller struct {}

Or:

// /app/model/user.go
package model
type User struct {}

// /app/controller/user.go
package controller
import "app/user/model"

type User struct {}

This however breaks the wholeness of the concept of User. To understand or modify User you need to touch several packages.

But, it does have a good property that it's more obvious when model imports controller, and to some extents it's enforced by language semantics.

Review Analysis

If the application isn't large (less than 30KLOC) and you have good programmers, it's usually not necessary to build anything. Organizing structures based on value will be sufficient, e.g.:

// /app/user/user.go
package user
type User struct {}
type Controller struct {}

Often the "constraint violations" are of little significance or are easy to fix. It harms clarity and understandability -- as long as you don't let it get out of hand, you don't have to worry about it.

Static/Runtime Analysis

You can also use static or runtime analysis to find these faults, via annotations:

Static:

// /app/user/user.go
package user

// architecture: model
type User struct {}

// architecture: controller
type Controller struct {}

Dynamic:

// /app/user/user.go
package user

import "app/constraint"

var _ = constraint.Model(&User{})
type User struct {}

var _ = constraint.Controller(&Controller{})
type Controller struct {}

// /app/main.go
package main

import "app/constraint"

func init() { constraint.Check() }

Both static/dynamic can also be done via fields:

// /app/user/user.go
package user

import "app/constraint"

type User struct {   
    _ constraint.Model
}

type Controller struct {
    _ constraint.Controller
}

Of course the lookup for such things become more complicated.

Other versions

Such approaches can be used in elsewhere, not just type constraints, but also func naming, API-s etc.

https://play.golang.org/p/4bCOV3tYz7

Licensed under: CC-BY-SA with attribution
scroll top