Closed blaggacao closed 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
)
Updated current UX above, works now: https://github.com/xoe-labs/go-generators/blob/master/ddd-domain-gen/tests/account_gen.go — feedback welcome!
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:
New
method for a struct, you can't validate other requirements that "not empty". And your business logic will probably be more complex than that.nil
is not enough. What if the passed holder
in your example is not nil
, but a pointer to an empty string? From domain point of view, this is not a valid input.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.
Thank you for the input, I will process it over the weekend.
As for metaprogramming approaches on ddd, I also found:
https://github.com/phelmkamp/magnum — an immutable enum generator (yeay!)
https://github.com/phelmkamp/metatag — a more flexible implementation of getters + a whole bunch of other boilerplate. The slice-wrapper and filter directives might be interesting when working with plural type read model version of a domain.
also worth noting: https://github.com/cheekybits/genny — useful if business logic requires fleshing out a custom type system of some sort.
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.
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.
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.
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.
Thank you! The implementation so far indeed reflects my ignorance (so far). 😉
How I'm going to incorporate your feedback:
reflect.IsZero()
, for generated code reflect
is probably ok.phelmkap/metatag
instead. — maybe even incorporate the "safe" parts of it for dddMight you have another look? I just implemented your feedback faithfully.
→ https://github.com/xoe-labs/go-generators/tree/master/ddd-domain-gen/test-domain
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
}
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.
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.
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).
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