266 lines
6.4 KiB
Markdown
266 lines
6.4 KiB
Markdown
# Domain Rules (DOM-01..09)
|
|
|
|
## DOM-01: Private Entity Fields (CRITICAL)
|
|
|
|
ALL entity struct fields MUST be unexported (lowercase). Entities are "types with behavior," not data bags.
|
|
|
|
**Check:** Scan all structs in `domain/` for exported fields. Any uppercase field name is a violation.
|
|
|
|
**Correct:**
|
|
```go
|
|
type Hour struct {
|
|
hour time.Time
|
|
availability Availability
|
|
}
|
|
```
|
|
|
|
**Wrong:**
|
|
```go
|
|
type Hour struct {
|
|
Hour time.Time // VIOLATION: exported field
|
|
Availability Availability // VIOLATION: exported field
|
|
}
|
|
```
|
|
|
|
**Exception:** DB model structs in `adapters/` MAY have exported fields for serialization tags.
|
|
|
|
---
|
|
|
|
## DOM-02: Factory Constructors (WARNING)
|
|
|
|
Entities MUST be created through factory constructors, never by direct struct literal.
|
|
|
|
Pattern: `func New{Type}(args...) (*Type, error)`
|
|
|
|
The constructor:
|
|
- Validates all invariants
|
|
- Returns an error if validation fails
|
|
- Returns a pointer to the new entity
|
|
|
|
**Reference:**
|
|
```go
|
|
func NewAvailableHour(hour time.Time) (*Hour, error) {
|
|
if err := validateTime(hour); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Hour{hour: hour, availability: Available}, nil
|
|
}
|
|
|
|
func NewTraining(uuid, userUUID, userName string, trainingTime time.Time) (*Training, error) {
|
|
if uuid == "" {
|
|
return nil, errors.New("empty training uuid")
|
|
}
|
|
if userUUID == "" {
|
|
return nil, errors.New("empty training user uuid")
|
|
}
|
|
// ... validate all fields
|
|
return &Training{uuid: uuid, userUUID: userUUID, userName: userName, time: trainingTime}, nil
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DOM-03: MustNew Panic Constructors (INFO)
|
|
|
|
For use in tests and initialization code, provide `MustNew{Type}` that panics on error.
|
|
|
|
```go
|
|
func MustNewFactory(fc FactoryConfig) Factory {
|
|
f, err := NewFactory(fc)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return f
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DOM-04: UnmarshalFromDatabase (WARNING)
|
|
|
|
Entities MUST provide an `Unmarshal{Type}FromDatabase` function for reconstruction from persistence. This function:
|
|
- Bypasses normal validation (data was already valid when stored)
|
|
- Accepts all fields needed to reconstruct full state
|
|
- Is used ONLY by repository adapters
|
|
|
|
**Reference:**
|
|
```go
|
|
func UnmarshalHourFromDatabase(hour time.Time, availability Availability) *Hour {
|
|
return &Hour{hour: hour, availability: availability}
|
|
}
|
|
|
|
func UnmarshalTrainingFromDatabase(
|
|
uuid, userUUID, userName string,
|
|
trainingTime time.Time,
|
|
notes string,
|
|
canceled bool,
|
|
proposedNewTime time.Time,
|
|
moveProposedBy UserType,
|
|
) (*Training, error) {
|
|
return &Training{
|
|
uuid: uuid, userUUID: userUUID, userName: userName,
|
|
time: trainingTime, notes: notes, canceled: canceled,
|
|
proposedNewTime: proposedNewTime, moveProposedBy: moveProposedBy,
|
|
}, nil
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DOM-05: Value Objects as Structs (CRITICAL)
|
|
|
|
Value objects MUST be structs wrapping a private field, NOT raw strings, ints, or type aliases.
|
|
|
|
This ensures they cannot be constructed with arbitrary values — only through validated constructors or predefined constants.
|
|
|
|
**Correct:**
|
|
```go
|
|
type Availability struct {
|
|
a string // private — cannot be set directly
|
|
}
|
|
|
|
var (
|
|
Available = Availability{"available"}
|
|
NotAvailable = Availability{"not_available"}
|
|
TrainingScheduled = Availability{"training_scheduled"}
|
|
)
|
|
|
|
type UserType struct {
|
|
s string
|
|
}
|
|
|
|
var (
|
|
Trainer = UserType{"trainer"}
|
|
Attendee = UserType{"attendee"}
|
|
)
|
|
```
|
|
|
|
**Wrong:**
|
|
```go
|
|
type Availability string // VIOLATION: can be set to any string
|
|
|
|
const (
|
|
Available Availability = "available"
|
|
NotAvailable Availability = "not_available"
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## DOM-06: IsZero Method (WARNING)
|
|
|
|
Value objects and factory structs SHOULD implement `IsZero() bool` to check for zero-value state.
|
|
|
|
```go
|
|
func (a Availability) IsZero() bool {
|
|
return a == Availability{}
|
|
}
|
|
|
|
func (f Factory) IsZero() bool {
|
|
return f == Factory{}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DOM-07: Behavior Methods Use Domain Language (CRITICAL)
|
|
|
|
Entity methods MUST use domain-specific language, NOT generic CRUD terms.
|
|
|
|
| Forbidden | Use Instead |
|
|
|-----------|------------|
|
|
| `SetStatus`, `Update` | `ScheduleTraining`, `CancelTraining`, `MakeAvailable` |
|
|
| `Create` | `Schedule`, `Register`, `Place`, `Submit` |
|
|
| `Delete` | `Cancel`, `Archive`, `Revoke` |
|
|
| `Get` | Use query noun phrases |
|
|
|
|
**Reference:**
|
|
```go
|
|
func (h *Hour) ScheduleTraining() error {
|
|
if !h.IsAvailable() {
|
|
return ErrHourNotAvailable
|
|
}
|
|
h.availability = TrainingScheduled
|
|
return nil
|
|
}
|
|
|
|
func (h *Hour) CancelTraining() error { ... }
|
|
func (h *Hour) MakeAvailable() error { ... }
|
|
func (h *Hour) MakeNotAvailable() error { ... }
|
|
|
|
func (t *Training) ProposeReschedule(newTime time.Time, proposedBy UserType) error { ... }
|
|
func (t *Training) ApproveReschedule(approvedBy UserType) error { ... }
|
|
func (t *Training) RejectReschedule() error { ... }
|
|
```
|
|
|
|
---
|
|
|
|
## DOM-08: String Constructors Validate Input (WARNING)
|
|
|
|
When a value object can be constructed from a string, use `New{Type}FromString` with validation.
|
|
|
|
```go
|
|
func NewAvailabilityFromString(availabilityStr string) (Availability, error) {
|
|
switch availabilityStr {
|
|
case "available":
|
|
return Available, nil
|
|
case "not_available":
|
|
return NotAvailable, nil
|
|
case "training_scheduled":
|
|
return TrainingScheduled, nil
|
|
default:
|
|
return Availability{}, fmt.Errorf("unknown availability: %s", availabilityStr)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## DOM-09: Factory Struct for Complex Creation (INFO)
|
|
|
|
When entity creation requires configuration or external dependencies, use a Factory struct pattern.
|
|
|
|
```go
|
|
type FactoryConfig struct {
|
|
MaxWeeksInTheFutureToSet int
|
|
MinUtcHour int
|
|
MaxUtcHour int
|
|
}
|
|
|
|
func (c FactoryConfig) Validate() error {
|
|
var errs []error
|
|
if c.MaxWeeksInTheFutureToSet <= 0 {
|
|
errs = append(errs, errors.New("MaxWeeksInTheFutureToSet must be > 0"))
|
|
}
|
|
// ... more validations
|
|
return multierr.Combine(errs...)
|
|
}
|
|
|
|
type Factory struct {
|
|
fc FactoryConfig
|
|
}
|
|
|
|
func NewFactory(fc FactoryConfig) (Factory, error) {
|
|
if err := fc.Validate(); err != nil {
|
|
return Factory{}, err
|
|
}
|
|
return Factory{fc: fc}, nil
|
|
}
|
|
|
|
func MustNewFactory(fc FactoryConfig) Factory {
|
|
f, err := NewFactory(fc)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (f Factory) IsZero() bool {
|
|
return f == Factory{}
|
|
}
|
|
|
|
func (f Factory) NewAvailableHour(hour time.Time) (*Hour, error) {
|
|
// uses f.fc for validation bounds
|
|
}
|
|
```
|