8

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?

  • As someone who has suffered under an architecture that organized packages exclusively by architectural level let me voice my support for feature based packages. I'm all for communicating intent but don't make me crawl through every obscure nook and cranny just to add one new feature. – candied_orange Jul 4 '17 at 0:00
  • By intent, I mean that packages should communicate what the application is about. So if you're building a library application, you'd have a books package, a lenders package, etc. – bigblind Jul 4 '17 at 0:10
4

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.