ThreeDotsLabs / wild-workouts-go-ddd-example

Go DDD example application. Complete project to show how to apply DDD, Clean Architecture, and CQRS by practical refactoring.
https://threedots.tech
MIT License
5.14k stars 472 forks source link

[ANNOUNCEMENT]: Implementing a ddd generator #11

Closed blaggacao closed 3 years ago

blaggacao commented 3 years ago
outdated https://pkg.go.dev/github.com/mizkei/accessor ```golang // go:generate accessor -output livecall_getters.go -type=Livecall ``` (without `-setter` as we want to have more control over the setters in the domain)

https://github.com/xoe-labs/ddd-gen

blaggacao commented 3 years ago

I'm working on a ddd-generator based on this repository.

https://github.com/xoe-labs/go-generators/tree/master/ddd-domain-gen (at the time of writing, it just compiles, nothing works, yet)https://github.com/xoe-labs/go-generators/blob/master/ddd-domain-gen/tests/account_gen.go

@roblaszczak @m110 Do you have any early comments on this PoC UX?

$ go run main.go -h
Generates idiomatic go code for a DDD domain based on struct field annotations.

  Available Annotations:
    Key "gen"
      getter              - generate default getter: no special domain (e.g. access) logic for reads
    Key "ddd"
      private             - this is private state, it can only be initialized directly from the repository
      required'error msg' - if not present in the constructor, an error with the provided message will be returned

  Expected Folder Structure:
    ./domain
    ├── livecall
    │   ├── livecall.go
    │   └── livecall_gen.go // generated by this tool
    ├── party
    │   ├── party.go
    │   └── party_gen.go // generated by this tool
    └── ...

Usage:
  ddd-domain-gen [flags]

Examples:
  Command:
    //go:generate go run github.com/xoe-labs/go-generators/ddd-domain-gen --type YOURTYPE

  Code:
    type Account struct {
        uuid    *string `gen:"getter" ddd:"required'field uuid is missing'"`
        holder  *string `gen:"getter"`
        balance *int64  `ddd:"private"` // read via domain logic: don't generate default getter
    }

    Required fields must be pointers for validation to work. So just use pointers everywhere.

Flags:
      --config string   config file (default is $HOME/.ddd-domain-gen.yaml)
  -h, --help            help for ddd-domain-gen
  -t, --type string     The source type for which to generate the code

In order to keep the code generation simple (aka v == nil checks for validation), all structs should use pointer types and at least must use pointer types for validated fields.


assumes the following package structure since constructors have generic names (New, MustNew & UnmarshalFromRepository):

./domain
├── livecall
│   ├── livecall.go
│   └── livecall_test.go
├── party
│   ├── party.go
│   └── party_test.go
└── repository.go

And it's probably a good idea to expose on the application, (etc.) layer which parts of the domain are used where by:

import (
    ./domain/livecall
    ./domain/party
)
blaggacao commented 3 years ago

Updated current UX above, works now: https://github.com/xoe-labs/go-generators/blob/master/ddd-domain-gen/tests/account_gen.go — feedback welcome!

m110 commented 3 years ago

Hey @blaggacao.

It's an interesting idea! However, right now I would say it's mostly checking a struct for nil values.

A couple of points from my side:

I see your point, and I think it's a nice idea to have code-generated Unmarshal function, as this is easy to predict most of the time. However, you can only go so far with validation and it will be hard to cover all possible requirements with struct tags.

blaggacao commented 3 years ago

Thank you for the input, I will process it over the weekend.

As for metaprogramming approaches on ddd, I also found:

m110 commented 3 years ago

Thanks for sharing. :) I like magnum, I think this is the perfect case for code generation. It's great it generates the All() function for an enum.

As for getters and setters, I still think it's better to write them manually, as this is the most important code in your application, and you shouldn't need "setters" anyway, but a specialised domain methods instead.

Consider the methods of Hour: https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/blob/master/internal/trainer/domain/hour/availability.go#L51-L97

There are seven methods working with one field. You could replace them with GetAvailability and SetAvailability, but where would the logic live then? The point is to make the domain model reflect the actual business language and behavior. It's more verbose than getters and setters, but that's the point.

blaggacao commented 3 years ago

Hm, I was well aware that using any single Set... would be a pretty bad idea for ddd, as you state.

A thought that Get... could also be a pretty bad idea, is only taking form. Here, I have a question though about how domain and readmodel play together. Unfortunatly I'm not yet able to formulate that question. I'll have a closer look on read models first.

blaggacao commented 3 years ago

A faint glimpse of a thought as I'm having it suggests the only getter method in need to be accessed from outside would be a stringer for logging purpose. — I'm definitely going to investigate more on this.

m110 commented 3 years ago

Well, it depends on the domain model. There's usually nothing wrong in exposing getters, but if you begin with the assumption that every field needs to be exposed, you might be tempted to add some logic outside of the domain. Instead, you could start with nothing exposed and add getters for what's really needed.

Getters are not a big deal IMO, but the way you implemented this is more serious, because you return pointers to the fields. This means someone could get the value, modify it, and it would change the inner state of your domain object. So when you use getters, avoid returning pointers.

blaggacao commented 3 years ago

Thank you! The implementation so far indeed reflects my ignorance (so far). 😉

blaggacao commented 3 years ago

How I'm going to incorporate your feedback:

blaggacao commented 3 years ago

Might you have another look? I just implemented your feedback faithfully.

https://github.com/xoe-labs/go-generators/tree/master/ddd-domain-gen/test-domain

blaggacao commented 3 years ago

Here is how the magnum works, the field apparently should be rather called string (or use metatag's stringer):

// GENERATED BY magnum, DO NOT EDIT

package party

// Local returns the <no value> PartyType.
func Local() PartyType {
    return PartyType{
        s: "local",
    }
}

// Remote returns the <no value> PartyType.
func Remote() PartyType {
    return PartyType{
        s: "remote",
    }
}

// S returns the PartyType's s.
func (p PartyType) S() string {
    return p.s
}

// MarshalText encodes the receiver into textual form.
func (p PartyType) MarshalText() (text []byte, err error) {
    return []byte(p.String()), nil
}

// UnmarshalText decodes the receiver from its textual form.
func (p *PartyType) UnmarshalText(text []byte) error {
    v, err := NewPartyType(string(text))
    if err != nil {
        return err
    }
    *p = v
    return nil
}
blaggacao commented 3 years ago

Similar to what phelmkamp/metatag does — I figured, I need to implement a variant of constructors if our domain aggregate in question is a slice wrapper, for example in order to implement uniqueness validations across the slice.

blaggacao commented 3 years ago

Interesting project: https://github.com/aloder/tojen — generates jennifer code from a real file. Since jennifer code is a bit tedious to write at first, this gives an excellent boilerplate for getting started with code generation.

blaggacao commented 3 years ago

I'm closing this, since it probably now is "announced". I\m going to continue to work on this and will try to make it a perfect companion for your blog post series. Just today I pushed a complete command generator (with generic error handling and implementation stubs).