opcotech/elemo

View on GitHub
docs/ADRs/0002.entity-and-model-constructors.md

Summary

Maintainability
Test Coverage
# Entity and Model Constructors

| author       | created at | updated at | status   |
|:-------------|:-----------|------------|:---------|
| @gabor-boros | 2023-03-16 | -          | accepted |

## Abstract

When extending the code as time goes by, it is important to keep constructors'
usage and functionality consistent, so we know what to expect from them without
having to dig deep in the code.

This ADR describes what should we assume when working with constructors, i.e.
what must be done **at least** in the constructor, and what should be done
after a constructor call.

## Decision

First of all, not every object needs a constructor. If the object has no
nillable fields, nor any other fields that need to be initialized, then there
is no need for a constructor.

If a constructor is needed, then it should be used to initialize the object's
required or default fields. The constructor should not be used to initialize
optional fields, as they can be initialized later, and it is not always
possible to initialize them in the constructor.

Also, the constructor should not be used to perform any other actions, such as
calling other methods, or performing any other operations, except for returning
errors in case of an invalid input.

One exception to the above rule is when the constructor is used to initialize
a service or repository, since we are going to use configurators for these
constructors as their arguments. The configurators will be used to initialize
the services and repositories, and they will be called in the constructor.

The example below shows how a constructor should be used to initialize a
`User` object. As you can see, the constructor is used to initialize the
required fields, and the optional fields are initialized later.

**Example:**

_The ID field is not initialized in the constructor, because it is a field
that should be generated by the database._

```go
type User struct {
    ID        string
    Name      string
    Email     string
    Password  string
    CreatedAt time.Time
    UpdatedAt time.Time
}

func NewUser(name, email, password string) (*User, error) {
    if name == "" {
        return nil, errors.New("name is required")
    }

    if email == "" {
        return nil, errors.New("email is required")
    }

    if password == "" {
        return nil, errors.New("password is required")
    }

    return &User{
        Name:     name,
        Email:    email,
        Password: password,
    }, nil
}

func caller() {
    user := NewUser("Bob", "bob@example.com", "secret")
    user.CreatedAt = time.Now().UTC()
}
```

## Consequences

The consequences of this decision are that we will have a consistent way of
working with constructors, and we will know what to expect from them.

The downside of this decision is that there is chance we may not call a
constructor, and we are initializing the object's fields manually, which can be
error-prone.

## References

None.