Skip to content

Instantly share code, notes, and snippets.

@dgellow
Last active June 12, 2018 23:13
Show Gist options
  • Save dgellow/956d94916a638f0024f3d3b12cfb3fb8 to your computer and use it in GitHub Desktop.
Save dgellow/956d94916a638f0024f3d3b12cfb3fb8 to your computer and use it in GitHub Desktop.
// Should be in a 'domain', or 'entities'. Here we define our domain
// model.
package main
// A User is a citizen, that can be or not innocent.
type User struct {
ID string
Email string
}
package main
import (
"fmt"
"os"
)
// main parses potential flags and env vars, instanciate dependencies
// and inject them into the service via its constructor.
func main() {
// Setup a connection to a real database
db := NewRealDB("realdb://evilcorp.org:1234")
// Instantiate a client for our surveillance API
apiClient := NewRealAPIClient("api.evilcorp.org/metrics")
// Inject dependencies in our service
service := NewService(db, apiClient)
// Start our service
fmt.Println("Starting service...")
if err := service.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// Should be in a package 'app', with the rest of the business logic.
package main
// UserRepository represents an abstraction of a data store that
// returns and potentially manipulate users.
type UserRepository interface {
GetAll() ([]User, error)
}
// Overlord is a system doing surveillance on citizens
type Overlord struct {
// Access to stored user data (could be a postgres db, an
// in-memory, etc)
users UserRepository
// Access to a 'surveillance API', could be EvilCorp, or other
// 3rd parties providing similar features.
client APIClient
}
// NewOverlord is a constructor
func NewOverlord(users UserRepository, client APIClient) *Overlord {
return &Overlord{
users: users,
client: client,
}
}
// ApplySurveillance is the actual process managing surveillance on
// users.
func (ov *Overlord) ApplySurveillance() error {
users, err := ov.users.GetAll()
if err != nil {
return err
}
for _, u := range users {
if err := ov.client.SendSurveillanceMetrics(u); err != nil {
return err
}
}
return nil
}
// Should be in a package 'adapters/evilcorp', with other things
// specific to EvilCorp.
package main
import "log"
// RealAPIClient is an almost realistic client for the evilcorp API
type RealAPIClient struct {
apiURL string
}
// NewRealAPIClient is a constructor
func NewRealAPIClient(apiURL string) *RealAPIClient {
return &RealAPIClient{
apiURL: apiURL,
}
}
// Post something to the evilcorp API
func (client *RealAPIClient) post(s ...string) error {
log.Printf("realapi client: POST request: %q\n", s)
// do something real
return nil
}
func (client *RealAPIClient) SendSurveillanceMetrics(user User) error {
return client.post(user.ID, user.Email)
}
// Should be in a package 'infra/dbclient'
package main
import (
"log"
)
// RealDB is client to an almost real database
type RealDB struct {
// In reality conn would be a db connection
conn interface{}
}
// NewRealDB is a constructor
func NewRealDB(connstring string) *RealDB {
return &RealDB{
// should create a db connection here, created with
// connstring, or get it directly as a parameter as
// that could potentially return an error, and
// returning errors from constructors is gross.
conn: nil,
}
}
// Query sends the query to the actual database and return the
// response.
func (db *RealDB) Query(statement string) error {
log.Printf("realdb query: %q\n", statement)
// do something real
return nil
}
// Should be in a package 'cmd/sevice'
package main
import "time"
// DB is an abstraction of a database connection
type DB interface {
Query(string) error
}
// APIClient is an abstraction of a client to a surveillance API
type APIClient interface {
SendSurveillanceMetrics(User) error
}
// Service is our actual service. The main use is to abstract
// dependencies as a way to be able to swap them or replace them by
// fake, stubs or mock versions.
type Service struct {
db DB
client APIClient
overlord *Overlord
}
// NewService is a constructor, used for dependencies injection
func NewService(
db DB,
client APIClient,
) *Service {
userRepo := NewRealUserRepository(db)
return &Service{
db: db,
client: client,
overlord: NewOverlord(userRepo, client),
}
}
// Run starts the service
func (s *Service) Run() error {
c := time.Tick(1 * time.Second)
for _ = range c {
if err := s.process(); err != nil {
return err
}
}
return nil
}
func (s *Service) process() error {
return s.overlord.ApplySurveillance()
}
package main
import (
"testing"
)
func TestService(t *testing.T) {
db := stubDB{}
apiClient := stubAPIClient{}
service := NewService(db, apiClient)
for i := 0; i < 1000; i++ {
err := service.process()
if err != nil {
t.Fatal("expected no error, got one:", err)
}
}
}
type stubDB struct {
capturedStmt string
err error
}
func (s stubDB) Query(statement string) error {
s.capturedStmt = statement
return s.err
}
type stubAPIClient struct {
capturedUser User
err error
}
func (s stubAPIClient) SendSurveillanceMetrics(user User) error {
s.capturedUser = user
return s.err
}
// Should be in a package 'infra/db'
package main
type RealUserRepository struct {
db DB
}
func NewRealUserRepository(db DB) *RealUserRepository {
return &RealUserRepository{
db: db,
}
}
func (repo *RealUserRepository) GetAll() ([]User, error) {
// should in reality do something like:
// records := repo.db.Query("select id, email from users where surveillance = true")
// then convert 'records' to a slice of User
repo.db.Query("select id, email from users where surveillance = true")
return []User{
{"user1", "sam.e@example.org"},
{"user2", "andre.v@example.org"},
}, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment