├── .gitignore ├── README.md ├── cmd └── main.go ├── docs ├── aggregate.md ├── domain-event.md ├── domain-service.md ├── entity.md ├── factory.md ├── images │ ├── aggregate │ │ ├── boundary.png │ │ ├── boundary2.png │ │ ├── business-invariants.png │ │ └── intro.jpeg │ ├── domain-event │ │ └── intro.jpeg │ ├── domain-service │ │ └── intro.jpeg │ ├── entity │ │ └── intro.jpeg │ ├── factory │ │ └── intro.jpeg │ ├── module │ │ ├── diagram.png │ │ └── intro.jpeg │ ├── repository-pattern-for-gorm │ │ ├── intro.jpeg │ │ └── uml.png │ ├── repository │ │ └── intro.jpeg │ ├── specification │ │ └── intro.jpeg │ └── value-object │ │ └── intro.jpeg ├── module.md ├── repository-pattern-for-gorm.md ├── repository.md ├── specification.md └── value-object.md ├── domain ├── bankAccount │ ├── entity │ │ └── entity.go │ └── repository │ │ └── bank_account_repository.go ├── bonus │ ├── entity │ │ └── bonus.go │ └── repository │ │ └── bonus_repository.go ├── email │ └── entity │ │ └── email.go ├── exchangeRate │ └── repository │ │ └── exchange_rate_repository.go ├── order │ ├── entity │ │ └── entity.go │ └── repository │ │ └── order_repository.go ├── services │ ├── account.go │ └── order.go └── userAccount │ └── entity │ └── entity.go ├── events ├── email_events.go ├── event.go ├── event_publisher.go └── sqs.go ├── go.mod ├── go.sum ├── gorm-generics ├── repository.go ├── simple_example.go └── specification.go ├── infrastructure ├── bankAccount │ ├── dto │ │ └── dto.go │ └── repository │ │ └── bank_account_repository.go ├── bonus │ └── repository │ │ └── bonus_repository.go └── exchangeRate │ └── repository │ └── exchange_rate_repository.go ├── pkg ├── access │ ├── access.go │ ├── domain │ │ ├── model │ │ │ └── access_model.go │ │ ├── repository │ │ │ └── access_repository.go │ │ └── service │ │ │ └── access_service.go │ └── infrastructure │ │ ├── database │ │ └── access_database.go │ │ └── fake │ │ └── access_fake.go ├── bankAccount │ └── domain │ │ └── model │ │ ├── bank_account.go │ │ ├── currency.go │ │ └── customer_account.go ├── billing │ └── billing.go ├── client │ ├── domain │ │ ├── model │ │ │ ├── address.go │ │ │ ├── company.go │ │ │ ├── customer.go │ │ │ ├── customer_account.go │ │ │ └── person.go │ │ └── repository │ │ │ └── customer_repository.go │ └── infrastructure │ │ ├── customer.go │ │ ├── dto │ │ ├── company.go │ │ ├── customer.go │ │ └── person.go │ │ ├── redis_API_customer.go │ │ └── redis_customer.go ├── investment │ ├── domain │ │ └── model │ │ │ └── investment.go │ └── infrastructure │ │ └── investment.go ├── loan │ └── domain │ │ └── model │ │ └── loan.go └── product │ ├── domain │ └── model │ │ └── product.go │ └── infrastructure │ ├── create │ └── product.go │ └── product.go ├── services ├── account_service.go ├── bank_service.go ├── casino_service.go ├── order_service.go └── transaction_service.go └── value-objects └── value_objects.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDD на практике в Golang 2 | 3 | Russian translation of article series: "Practical DDD in Golang". 4 | 5 | 1. [Practical DDD in Golang: Value Object](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 6 | 2. [Practical DDD in Golang: Entity](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 7 | 3. [Practical DDD in Golang: Domain Service](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 8 | 4. [Practical DDD in Golang: Domain Event](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 9 | 5. [Practical DDD in Golang: Module](https://levelup.gitconnected.com/practical-ddd-in-golang-module-51edf4c319ec) 10 | 6. [Practical DDD in Golang: Aggregate](https://levelup.gitconnected.com/practical-ddd-in-golang-aggregate-de13f561e629) 11 | 7. [Practical DDD in Golang: Factory](https://levelup.gitconnected.com/practical-ddd-in-golang-factory-5ba135df6362) 12 | 8. [Practical DDD in Golang: Repository](https://levelup.gitconnected.com/practical-ddd-in-golang-repository-d308c9d79ba7) 13 | 9. [Practical DDD in Golang: Specification](https://levelup.gitconnected.com/practical-ddd-in-golang-specification-6523d14438e6) 14 | 10. [The Power of Generics in Go: The Repository pattern for GORM](https://hindenbug.io/the-power-of-generics-in-go-the-repository-pattern-for-gorm-7f8891df0934) 15 | 16 | Перевод статей по практическому применению предметно-ориентированного проектирования (DDD) в Golang 17 | 18 | 1. [DDD на практике в Golang: Объект-значение](docs/value-object.md) 19 | 2. [DDD на практике в Golang: Сущности](docs/entity.md) 20 | 3. [DDD на практике в Golang: Сервисы предметной области](docs/domain-service.md) 21 | 4. [DDD на практике в Golang: Событие предметной области](docs/domain-event.md) 22 | 5. [DDD на практике в Golang: Модуль](docs/module.md) 23 | 6. [DDD на практике в Golang: Агрегат](docs/aggregate.md) 24 | 7. [DDD на практике в Golang: Фабрика](docs/factory.md) 25 | 8. [DDD на практике в Golang: Репозиторий](docs/repository.md) 26 | 9. [DDD на практике в Golang: Спецификация](docs/specification.md) 27 | 10. [Преимущества дженериков (Generics) в Go: шаблон Репозиторий для GORM](docs/repository-pattern-for-gorm.md) -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/product/domain/model" 6 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/product/infrastructure/create" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | func main() { 11 | spec := create.NewAndSpecification( 12 | create.NewHasAtLeast(10), 13 | create.FunctionSpecification(create.IsPlastic), 14 | create.FunctionSpecification(create.IsDeliverable), 15 | ) 16 | 17 | fmt.Printf("%+v", spec.Create(model.Product{ 18 | ID: uuid.New(), 19 | })) 20 | // выводит: {ID:befaf2b9-73cd-44cf-95f1-5fba087e46d9 Material:plastic IsDeliverable:true Quantity:10} 21 | } 22 | -------------------------------------------------------------------------------- /docs/aggregate.md: -------------------------------------------------------------------------------- 1 | # DDD на практике в Golang: Агрегат 2 | 3 | ![intro](images/aggregate/intro.jpeg) 4 | *Фото [Raphaël Biscaldi](https://unsplash.com/@les_photos_de_raph) из [Unsplash](https://unsplash.com/)* 5 | 6 | Я потратил годы на понимание и практику DDD подхода. Большинство принципов было 7 | легко понять и реализовать в коде. Тем не менее, один из них привлек мое особое 8 | внимание. 9 | 10 | Я должен сказать, что агрегат является наиболее важным шаблоном в DDD и 11 | вероятно вcё тактическое предметно-ориентированное проектирование не имеет 12 | смысла без него. Он нужен для объединения бизнес-логики. 13 | 14 | При чтении, вы увидите, что Агрегат больше похож на набор шаблонов, но это 15 | заблуждение. Агрегат — это ключевая точка уровня предметной области. Без него 16 | нет причин использовать DDD. 17 | 18 | > Другие статьи из DDD цикла: 19 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 20 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 21 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 22 | > 4. [DDD на практике в Golang: Событие предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 23 | > 5. [DDD на практике в Golang: Модуль](https://levelup.gitconnected.com/practical-ddd-in-golang-module-51edf4c319ec) 24 | 25 | ## Бизнес инварианты 26 | 27 | В реальном мире некоторые правила гибки. Например, если вы берёте кредит в банке, 28 | вам необходимо со временем выплачивать проценты. Общая сумма процентов регулируется 29 | в зависимости от вашего инвестиционного капитала и периода, который вы потратите на 30 | выплату долга. 31 | 32 | В некоторых случаях банк может предоставить вам льготный период. Или предложение 33 | по кредиту будет более выгодным из-за вашей лояльности в прошлом. Или предоставить 34 | вам уникальное одноразовое предложение, или заставить вас оформить ипотечный 35 | кредит на дом. 36 | 37 | Все эти гибкие правила из реального мира, в DDD мы реализуем с помощью шаблона 38 | Policy (я расскажу о нём в следующих статьях). Они зависят от многих конкретных 39 | случаев и для реализации требуют более сложных структур кода. 40 | 41 | > В реальном мире существуют незыблемые правила. Как бы мы не пытались, мы не 42 | > можем изменить ни их, ни то, как они используются в нашей бизнес-логике. Эти 43 | > правила должны применяться всякий раз, когда объекты переходят из одного 44 | > состояния в другое. Их называют бизнес-инвариантами. 45 | 46 | Например, никому не должно быть разрешено удалять учётную запись клиента в банке, 47 | если на каком-либо из банковских счетов, связанных с клиентом, есть деньги или 48 | задолженность. 49 | 50 | Во многих банках один клиент может иметь несколько банковских счетов в одной и 51 | той же валюте. Но в некоторых из них клиенту не разрешается иметь валюту в иностранной 52 | валюте или иметь несколько счетов в одной и той же. 53 | 54 | Когда возникают такие бизнес-правила, они становятся бизнес-инвариантами. Они 55 | существуют с момента создания объекта до его удаления. Их нарушение означает 56 | нарушение назначения всего приложения. 57 | 58 | ```go 59 | // Сущность 60 | type Currency struct { 61 | id uuid.UUID 62 | // 63 | // какие-то поля 64 | // 65 | } 66 | 67 | func (c Currency) Equal(other Currency) bool { 68 | return c.id == other.id 69 | } 70 | 71 | // Сущность 72 | type BankAccount struct { 73 | id uuid.UUID 74 | iban string 75 | amount int 76 | currency Currency 77 | } 78 | 79 | func NewBankAccount(currency Currency) BankAccount { 80 | return BankAccount{ 81 | // 82 | // определяем поля 83 | // 84 | } 85 | } 86 | 87 | func (ba BankAccount) HasMoney() bool { 88 | return ba.amount > 0 89 | } 90 | 91 | func (ba BankAccount) InDebt() bool { 92 | return ba.amount < 0 93 | } 94 | 95 | func (ba BankAccount) IsForCurrency(currency Currency) bool { 96 | return ba.currency.Equal(currency) 97 | } 98 | 99 | type BankAccounts []BankAccount 100 | 101 | func (bas BankAccounts) HasMoney() bool { 102 | for _, ba := range bas { 103 | if ba.HasMoney() { 104 | return true 105 | } 106 | } 107 | 108 | return false 109 | } 110 | 111 | func (bas BankAccounts) InDebt() bool { 112 | for _, ba := range bas { 113 | if ba.InDebt() { 114 | return true 115 | } 116 | } 117 | 118 | return false 119 | } 120 | 121 | func (bas BankAccounts) HasCurrency(currency Currency) bool { 122 | for _, ba := range bas { 123 | if ba.IsForCurrency(currency) { 124 | return true 125 | } 126 | } 127 | 128 | return false 129 | } 130 | 131 | 132 | // Сущность и агрегат 133 | type CustomerAccount struct { 134 | id uuid.UUID 135 | isDeleted bool 136 | // 137 | // какие-то поля 138 | // 139 | accounts BankAccounts 140 | // 141 | // какие-то поля 142 | // 143 | } 144 | 145 | func (ca *CustomerAccount) MarkAsDeleted() error { 146 | if ca.accounts.HasMoney() { 147 | return errors.New("there are still money on bank account") 148 | } 149 | if ca.accounts.InDebt() { 150 | return errors.New("bank account is in debt") 151 | } 152 | 153 | ca.isDeleted = true 154 | 155 | return nil 156 | } 157 | 158 | func (ca *CustomerAccount) CreateAccountForCurrency(currency Currency) error { 159 | if ca.accounts.HasCurrency(currency) { 160 | return errors.New("there is already bank account for that currency") 161 | } 162 | ca.accounts = append(ca.accounts, NewBankAccount(currency)) 163 | 164 | return nil 165 | } 166 | ``` 167 | *Пример бизнес-инвариантов* 168 | 169 | В вышеприведенном примере мы видим некую конструкцию кода в Go, 170 | `CustomerAccount`, которая является Сущностью и Агрегатом. Помимо этого созданы 171 | Сущности `BankAccount` и `Currency`. 172 | 173 | По отдельности у всех этих трех сущностей есть свои собственные бизнес-правила. 174 | Некоторые из них гибкие, некоторые — инварианты. Тем не менее, когда они 175 | взаимодействуют, какие-то инварианты влияют на все. Для этого и создаётся 176 | агрегат. 177 | 178 | У нас есть логика создания `BankAccount`, которая зависит от всех `BankAccounts` 179 | конкретного `CustomerAccount`. В этом случае у одного Клиента не может быть 180 | несколько банковских счетов с одной и той же валютой. 181 | 182 | Кроме того, мы не можем удалить `CustomerAccount`, если все подключенные к нему 183 | `BankAccount` не имеют ноль на счету. На них не должно быть денег и 184 | задолженностей. 185 | 186 | ![business-invariants](images/aggregate/business-invariants.png) 187 | *Область применения бизнес-инвариантов* 188 | 189 | На диаграмме выше показан кластер из трёх уже обсуждавшихся сущностей. Все они 190 | вместе связаны с бизнес-инвариантами, которые гарантируют, что `Aggregate` 191 | всегда находится в правильном состоянии. 192 | 193 | Если какая-либо другая Сущность или Объект-значение принадлежат тем же 194 | бизнес-инвариантам, то эти новые объекты становятся частью того же Агрегата. 195 | 196 | Если в Агрегате у нас нет хотя бы одного инварианта, который связывает один объект 197 | с остальными, то этот объект не принадлежит этому агрегату. 198 | 199 | ## Граница 200 | 201 | Я много раз использовал DDD, и возникал вопрос, как определить границу Агрегата. 202 | Этот вопрос всегда возникает при добавлении новой Сущности или Объекта-значения 203 | в приложение. 204 | 205 | Пока что понятно, что Агрегат - это не просто набор объектов. Это понятие 206 | предметной области. Его члены определяют логический кластер. Без группировки мы не 207 | можем гарантировать, что они будут находиться в правильном состоянии. 208 | 209 | ```go 210 | type Person struct { 211 | id uuid.UUID 212 | // 213 | // какие-то поля 214 | // 215 | birthday time.Time 216 | } 217 | 218 | type Company struct { 219 | id uuid.UUID 220 | // 221 | // какие-то поля 222 | // 223 | isLiquid bool 224 | } 225 | 226 | type Customer struct { 227 | id uuid.UUID 228 | person *Person 229 | company *Company 230 | // 231 | // какие-то поля 232 | // 233 | } 234 | 235 | func (c *Customer) IsLegal() bool { 236 | if c.person != nil { 237 | return c.person.birthday.AddDate(18, 0, 0).Before(time.Now()) 238 | } else { 239 | return c.company.isLiquid 240 | } 241 | } 242 | ``` 243 | *Агрегат Клиент* 244 | 245 | В вышеприведенном фрагменте кода показан Агрегат Клиент (`Customer`). Не только 246 | здесь, но и во многих приложениях, у вас будут Сущности с названием Клиент 247 | (`Customer`) и почти всегда это будут Агрегаты. 248 | 249 | Здесь у нас задано несколько бизнес-инвариантов, которые определяют легальность 250 | конкретного Клиента (`Customer`), в зависимости от того говорим ли мы о 251 | человеке (`Person`) или компании (`Company`). Бизнес-инвариантов может быть 252 | даже больше, но пока достаточно одного. 253 | 254 | Поскольку мы имеем дело с приложением для банка, проблема заключается в том, 255 | принадлежат ли `CustomerAccount` и `Customer` к одному и тому же Агрегату. Между 256 | ними существует связь и некоторые бизнес-правила связывают их, но являются ли 257 | они инвариантами? 258 | 259 | ![boundary](images/aggregate/boundary.png) 260 | *Новые Сущности на уровне предметной области* 261 | 262 | У одного Клиента (`Customer`) может быть множество `CustomerAccounts` (или ни 263 | одного). И мы видим, что существуют некие бизнес-инварианты для объектов рядом с 264 | `Customer` и объектов рядом c `CustomerAccounts`. 265 | 266 | Исходя из точного определения инвариантов, если мы не можем найти ничего, 267 | что связывает вместе `Customer` и `CustomerAccount`, то мы должны разделить их 268 | на агрегаты. И любой другой кластер, который мы добавим на рисунок должен 269 | обрабатываться таким же образом — имеют ли он общие инварианты с уже 270 | существующими агрегатами? 271 | 272 | ![boundary2](images/aggregate/boundary2.png) 273 | *Несколько связанных агрегатов* 274 | 275 | Хорошей практикой является делать агрегаты как можно меньше. Члены агрегата 276 | сохраняются вместе в хранилище (например, базе данных) и добавление слишком 277 | большого количества таблиц в одну транзакцию не является хорошей практикой. 278 | 279 | Здесь мы уже видим, что должны определить репозиторий на уровне агрегата и 280 | сохранять всех её членов только через этот репозиторий, как в примере ниже. 281 | 282 | ```go 283 | type Customer struct { 284 | id uuid.UUID 285 | person *Person 286 | company *Company 287 | // 288 | // какие-то поля 289 | // 290 | } 291 | type CustomerRepository interface { 292 | Search(ctx context.Context, specification CustomerSpecification) ([]model.Customer, error) 293 | Create(ctx context.Context, customer model.Customer) (*model.Customer, error) 294 | UpdatePerson(ctx context.Context, customer model.Customer) (*model.Customer, error) 295 | UpdateCompany(ctx context.Context, customer model.Customer) (*model.Customer, error) 296 | // 297 | // и много других методов 298 | // 299 | } 300 | ``` 301 | *Пример репозитория для всего агрегата* 302 | 303 | Мы можем определить `Person` и `Company` как Сущности `Entities` (или Объекты-значения 304 | `Value Objects`), но даже если у них есть свой идентификатор, мы должны обновить 305 | из `Customer`, используя `CustomerRepository`. 306 | 307 | Работая напрямую с `Person` или `Company` или сохраняя их без `Customer` и 308 | других объектов может нарушить бизнес-инварианты. Мы хотим убедиться, что транзакции 309 | выполняются вместе или, если необходимо, откатить все изменения. 310 | 311 | Кроме сохранения, удаление Агрегата должно происходить одновременно. Это означает, 312 | что удаляя Сущность Клиент (`Customer`) мы должны удалить также Сущности 313 | `Person` и `Company`. У них нет причин существовать отдельно. 314 | 315 | Как видите, Агрегат не должен быть слишком маленьким или слишком большим. Он 316 | должен быть точно ограничен бизнес-инвариантами. Все, что находится внутри 317 | этой границы, мы должны использовать вместе, а все, что находится за этой 318 | границей, принадлежит другим Агрегатам. 319 | 320 | ## Связи 321 | 322 | Как было видно ранее в статье, между Агрегатами существуют связи. Они всегда 323 | должны быть прописаны в коде, но как можно проще. 324 | 325 | Чтобы избежать сложных связей, прежде всего нужно избегать ссылок на Агрегаты, 326 | а использовать идентификаторы для связей — как во фрагменте кода, показанном 327 | ниже. 328 | 329 | ```go 330 | type CustomerAccount struct { 331 | id uuid.UUID 332 | // 333 | // какие-то поля 334 | // 335 | customer Customer // неправильный способ - используется ссылка 336 | // 337 | // какие-то поля 338 | // 339 | } 340 | 341 | type CustomerAccount struct { 342 | id uuid.UUID 343 | // 344 | // какие-то поля 345 | // 346 | customerID uuid.UUID // правильный способ - используется идентификатор 347 | // 348 | // какие-то поля 349 | // 350 | } 351 | ``` 352 | *Избегаем ссылок, используя идентификаторы* 353 | 354 | Другая проблема может быть связана с направлением связей. Наилучший сценарий — 355 | это когда существует однонаправленная связь между ними, и мы избегаем любой 356 | двунаправленной. 357 | 358 | Это непростой процесс для принятия решения, и он зависит от вариантов 359 | использования в нашем ограниченном контексте. Если мы пишем программное 360 | обеспечение для банкомата, где пользователь взаимодействует с `CustomerAccount` 361 | с помощью дебетовой карты, тогда мы иногда будем получать доступ к `Customer`, 362 | указав его идентификатор в `CustomerAccount`. 363 | 364 | В другом случае нашим Ограниченным контекстом может быть приложение, которое 365 | управляет всеми `CustomerAccounts` одного `Customer`. Пользователи могут 366 | авторизоваться и управлять всеми банковскими счетами (`CustomerAccounts`). В 367 | этом случае Клиент (`Customer`) должен хранить список идентификаторов, 368 | связанных с банковскими счетами (`CustomerAccounts`). 369 | 370 | ## Корневой агрегат 371 | 372 | Все агрегаты в этой статье имеют те же имена, что и некоторые сущности, 373 | например, Сущность и Агрегат `Customer`. Эти уникальные сущности — Корневые 374 | агрегаты (`Aggregate Roots`) и основные объекты внутри Агрегатов. 375 | 376 | Корневой агрегат (`Aggregate Root`) — это шлюз для доступа ко всем другим 377 | Сущностям, Объектам-значениям и Коллекциям внутри них. Мы не должны изменять 378 | члены Агрегата напрямую, а только через Корневой Агрегат (`Aggregate Root`). 379 | 380 | Корневой агрегат предоставляет методы, которые описывают его поведение. Он 381 | должен определять способы доступа к атрибутам или объектам внутри него, а также 382 | позволять изменять эти данные. Даже когда Корневой агрегат возвращает объект, 383 | он должен возвращать только его копию. 384 | 385 | ```go 386 | func (ca *CustomerAccount) GetIBANForCurrency(currency Currency) (string, error) { 387 | for _, account := range ca.accounts { 388 | if account.IsForCurrency(currency) { 389 | return account.iban, nil 390 | } 391 | } 392 | return "", errors.New("this account does not support this currency") 393 | } 394 | 395 | func (ca *CustomerAccount) MarkAsDeleted() error { 396 | if ca.accounts.HasMoney() { 397 | return errors.New("there are still money on bank account") 398 | } 399 | if ca.accounts.InDebt() { 400 | return errors.New("bank account is in debt") 401 | } 402 | 403 | ca.isDeleted = true 404 | 405 | return nil 406 | } 407 | 408 | func (ca *CustomerAccount) CreateAccountForCurrency(currency Currency) error { 409 | if ca.accounts.HasCurrency(currency) { 410 | return errors.New("there is already bank account for that currency") 411 | } 412 | ca.accounts = append(ca.accounts, NewBankAccount(currency)) 413 | 414 | return nil 415 | } 416 | 417 | func (ca *CustomerAccount) AddMoney(amount int, currency Currency) error { 418 | if ca.isDeleted { 419 | return errors.New("account is deleted") 420 | } 421 | if ca.isLocked { 422 | return errors.New("account is locked") 423 | } 424 | 425 | return ca.accounts.AddMoney(amount, currency) 426 | } 427 | ``` 428 | *Пример методов, описывающих поведение Агрегата* 429 | 430 | Поскольку Агрегат состоит из множества Сущностей и Объектов-значений, внутри него 431 | существуют различные идентификаторы. В таких случаях различают два типа 432 | идентификаторов. 433 | 434 | Корневой Агрегат имеет глобальный идентификатор. Этот идентификатор уникален 435 | глобально и в приложении нет ещё одной Сущности с таким же идентификатором. Мы 436 | можем ссылаться на идентификатор Корневого Агрегата извне Агрегата. 437 | 438 | Все остальные Сущности внутри Агрегатов имеют локальные идентификаторы. Такие 439 | идентификаторы уникальны только внутри Агрегатов, но вне их могут повторяться. 440 | Только Агрегат содержит информацию о локальных идентификаторах, и мы не должны 441 | ссылаться на них вне Агрегата. 442 | 443 | ```go 444 | type Person struct { 445 | id uuid.UUID // локальный идентификатор 446 | // 447 | // какие-то поля 448 | // 449 | birthday time.Time 450 | } 451 | 452 | type Company struct { 453 | id uuid.UUID // локальный идентификатор 454 | // 455 | // какие-то поля 456 | // 457 | isLiquid bool 458 | } 459 | 460 | type CustomerAccount struct { 461 | id uuid.UUID // глобальный идентификатор 462 | person *Person 463 | company *Company 464 | // 465 | // какие-то поля 466 | // 467 | } 468 | ``` 469 | *Глобальные и локальные идентификаторы* 470 | 471 | ## Заключение 472 | 473 | Агрегат — это понятие предметной области, задаваемое бизнес-инвариантами. 474 | Бизнес-инварианты определяют правила, которые должны выполняться в любой момент 475 | приложения. Они представляют собой границу Агрегата. 476 | 477 | Агрегаты должны сохраняться и удаляться вместе. Корневой агрегат — это шлюз, 478 | через который осуществляется доступ к другим членам Агрегата. Доступ к ним 479 | возможен только через Корневые Агрегаты. 480 | 481 | > Другие статьи из DDD цикла: 482 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 483 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 484 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 485 | > 4. [DDD на практике в Golang: Событие предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 486 | > 5. [DDD на практике в Golang: Модуль](https://levelup.gitconnected.com/practical-ddd-in-golang-module-51edf4c319ec) 487 | 488 | ## Полезные ссылки на источники: 489 | 490 | * [https://martinfowler.com/](https://martinfowler.com/) 491 | * [https://www.domainlanguage.com/](https://www.domainlanguage.com/) -------------------------------------------------------------------------------- /docs/domain-event.md: -------------------------------------------------------------------------------- 1 | # DDD на практике в Golang: Событие предметной области 2 | 3 | ![intro](images/domain-event/intro.jpeg) 4 | *Фото [Anthony DELANOIX](https://unsplash.com/@anthonydelanoix) из [Unsplash](https://unsplash.com/)* 5 | 6 | Во многих случаях Сущности — наилучший способ описать что-либо при 7 | предметно-ориентированном проектировании. Вместе с объектами-значениями они 8 | предоставляют наиболее полную картину рассматриваемой предметной области. 9 | 10 | Иногда отличный способ описать рассматриваемую предметную область — использовать 11 | события, происходящие в ней. На самом деле я всё чаще пытаюсь определить события, 12 | а затем Сущности, связанные с ними. 13 | 14 | Хотя Эрик Эванс не рассмотрел шаблон Событие предметной области (`Domain Event`) 15 | в первом издании своей книги, сегодня сложно представить уровень предметной 16 | области без использования событий. 17 | 18 | Шаблон Событие предметной области описывает возникающие события в нашем коде. 19 | Мы можем использовать его для представления любого явления из реального мира, 20 | которое имеет отношение к нашей бизнес-логике. Сегодня всё в деловом мире связано 21 | с какими-то событиями. 22 | 23 | > Другие статьи из DDD цикла: 24 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 25 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 26 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 27 | 28 | ## Им может быть что угодно 29 | 30 | Событиями предметной области может быть что угодно, но они должны удовлетворять 31 | некоторым правилам. Во-первых, они неизменяемы. Чтобы следовать этому правилу 32 | я всегда использую приватные поля внутри структуры `Event`, даже если я не большой 33 | поклонник приватных полей и геттеров в Go. По крайней мере, у событий не так много 34 | геттеров. 35 | 36 | Одно конкретное событие может произойти только один раз. Это означает, что мы 37 | можем только один раз создать Сущность Заказ (`Order`) с неким идентификатором, 38 | поэтому только один раз наш код может инициировать событие (`Event`), описывающее 39 | создание этого Заказа. 40 | 41 | Любое другое Событие для этого Заказа будет иметь другой тип. Любое другое, описывающее 42 | создание, Событие будет относиться к другому Заказу. 43 | 44 | Каждое Событие практически описывает то, что уже произошло. Оно представляет 45 | прошлое. Это означает, что мы запускаем событие `OrderCreated`, когда уже создали 46 | `Order`, а не до этого. 47 | 48 | ```go 49 | // Интерфейс Event для описания События предметной области 50 | type Event interface { 51 | Name() string 52 | } 53 | 54 | // Событие GeneralError 55 | type GeneralError string 56 | 57 | func NewGeneralError(err error) Event { 58 | return GeneralError(err.Error()) 59 | } 60 | 61 | func (e GeneralError) Name() string { 62 | return "event.general.error" 63 | } 64 | 65 | // Интерфейс OrderEvent для описания События предметной области, связанных с Заказом 66 | type OrderEvent interface { 67 | Event 68 | OrderID() uuid.UUID 69 | } 70 | 71 | // Событие OrderDispatched 72 | type OrderDispatched struct { 73 | orderID uuid.UUID 74 | } 75 | 76 | func (e OrderDispatched) Name() string { 77 | return "event.order.dispatched" 78 | } 79 | 80 | func (e OrderDispatched) OrderID() uuid.UUID { 81 | return e.orderID 82 | } 83 | 84 | // Событие OrderDelivered 85 | type OrderDelivered struct { 86 | orderID uuid.UUID 87 | } 88 | 89 | func (e OrderDelivered) Name() string { 90 | return "event.order.delivery.success" 91 | } 92 | 93 | func (e OrderDelivered) OrderID() uuid.UUID { 94 | return e.orderID 95 | } 96 | 97 | // Событие OrderDeliveryFailed 98 | type OrderDeliveryFailed struct { 99 | orderID uuid.UUID 100 | } 101 | 102 | func (e OrderDeliveryFailed) Name() string { 103 | return "event.order.delivery.failed" 104 | } 105 | 106 | func (e OrderDeliveryFailed) OrderID() uuid.UUID { 107 | return e.orderID 108 | } 109 | ``` 110 | *Простые События* 111 | 112 | В приведённом выше примере кода показаны простые События предметной области. 113 | Этот код — один из миллиардов реализующих их на Go. В некоторых случаях, как здесь 114 | для `GeneralError`, я использовал простые строки. 115 | 116 | Но иногда я создавал сложные объекты. Или мне приходилось расширить основной 117 | интерфейс `Event` каким-то более специфическим, чтобы добавить дополнительные 118 | методы, например, как в случае с `OrderEvent`. 119 | 120 | Для События предметной области, поскольку оно является интерфейсом, не нужно 121 | реализовывать какие-либо методы. Им может быть что угодно. Как я уже говорил, 122 | иногда я использую строки, но достаточно и чего-либо другого. Для обобщения 123 | время от времени я все же объявляю интерфейс `Event`. 124 | 125 | ## Старый друг 126 | 127 | Событие предметной области как шаблон, не является чем-то новым, а 128 | является всего лишь другим представлением шаблона Наблюдатель (`Observer`). 129 | Шаблон Наблюдатель включает Издателя (`Publisher`), Подписчика (`Subscriber`) 130 | как основных исполнителей и конечно же Событие (`Event`). 131 | 132 | Событие предметной области использует ту же логику. Подписчик (`Subscriber`) 133 | или Обработчик Cобытий (`Event Handler`) — это структура, реагирующая на 134 | конкретное событие домена, на которое подписана. Издатель (`Publisher`) - это 135 | структура, уведомляющая все Обработчики Событий (`Event Handlers`) о том, что 136 | какое-то событие произошло. 137 | 138 | Издатель — это точка входа для запуска любого События. Он содержит все 139 | Обработчики Событий и предоставляет простой интерфейс для любого сервиса 140 | предметной области (`Domain Service`), фабрики (`Factory`) или других объектов, 141 | которые хотят опубликовать какое-либо событие. 142 | 143 | ```go 144 | // Интерфейс EventHandler, описывающий любой объект, который должен быть 145 | // уведомлен о каком-либо Event 146 | type EventHandler interface { 147 | Notify(event Event) 148 | } 149 | 150 | // EventPublisher - основная структура, уведомляющая все EventHandler 151 | type EventPublisher struct { 152 | handlers map[string][]EventHandler 153 | } 154 | 155 | // Метод Subscribe подписывает EventHandler на определённое событие (Event) 156 | func (e *EventPublisher) Subscribe(handler EventHandler, events ...Event) { 157 | for _, event := range events { 158 | handlers := e.handlers[event.Name()] 159 | handlers = append(handlers, handler) 160 | e.handlers[event.Name()] = handlers 161 | } 162 | } 163 | 164 | // Метод Notify уведомляет подписанный EventHandler о том, что произошло определенное событие (Event) 165 | func (e *EventPublisher) Notify(event Event) { 166 | for _, handler := range e.handlers[event.Name()] { 167 | handler.Notify(event) 168 | } 169 | } 170 | ``` 171 | *Пример с `EventHandler` и `EventPublisher`* 172 | 173 | Приведенный выше фрагмент кода показывает остальную часть шаблона Событие 174 | предметной области. Интерфейс `EventHandler` представляет собой любую структуру, 175 | которая должна реагировать на какое-либо событие. У него есть только один 176 | метод `Notify`, который ожидает событие в качестве аргумента. 177 | 178 | Структура `EventPublisher` более сложная. Она предоставляет общий метод `Notify`, 179 | который отвечает за уведомление всех Обработчиков Событий, подписанных на него. 180 | Ещё один метод `Subscribe` позволяет любому `EventHandler` подписаться на любое 181 | событие. 182 | 183 | Структура `EventPublisher` может быть менее сложной. Вместо того, чтобы давать 184 | возможность `EventHandler` подписаться на конкретное событие `Event`, используя 185 | map, у него может быть простой массив с `EventHandler`. Он будет уведомлять все 186 | обработчики о любом событии. 187 | 188 | в общем случае мы должны публиковать События предметной области синхронно на 189 | уровне предметной области. Но иногда по какой-то причине я могу запускать их 190 | асинхронно. Для этой цели я использую [Goroutine](https://tour.golang.org/concurrency/1). 191 | 192 | ```go 193 | type Event interface { 194 | Name() string 195 | IsAsynchronous() bool 196 | } 197 | 198 | type EventPublisher struct { 199 | handlers map[string][]EventHandler 200 | } 201 | 202 | func (e *EventPublisher) Notify(event Event) { 203 | if event.IsAsynchronous() { 204 | go e.notify(event) // запускаем код в отдельной горутине 205 | } 206 | 207 | e.notify(event) // синхронный вызов 208 | } 209 | 210 | func (e *EventPublisher) notify(event Event) { 211 | for _, handler := range e.handlers[event.Name()] { 212 | handler.Notify(event) 213 | } 214 | } 215 | ``` 216 | *Запускаем События асинхронно* 217 | 218 | В приведенном выше примере показан один из вариантов асинхронной публикации 219 | событий. Чтобы реализовать оба подхода, я часто определяю метод внутри 220 | интерфейса `Event`, который позже предоставляет мне информацию должен ли я 221 | запускать событие синхронно или нет. 222 | 223 | ## Создание 224 | 225 | Моя самая большая дилемма заключалась в том, где правильное место для 226 | создания события. И, честно говоря, я задавал их везде. Единственное правило, 227 | которое у меня было, заключалось в том, что объекты, отслеживающие состояния, 228 | не могли уведомлять `EventPublisher`. 229 | 230 | Сущности (`Entity`), Объекты-значения (`Value Objects`) и Агрегаты 231 | (`Aggregates`) (которые мы рассмотрим в следующей статье) являются объектами, 232 | отслеживающими состояния. С этой точки зрения они не должны содержать внутри 233 | себя `EventPublisher`, и передача его в качестве аргумента их методам 234 | я всегда считал безобразным кодом. 235 | 236 | Кроме того, я не использую объекты, отслеживающие состояния, в качестве 237 | обработчиков событий (`EventHandlers`). Если мне нужно было бы что-то сделать с 238 | какой-либо Сущностью (`Entity`), когда происходит конкретное Событие 239 | (`Event`), я бы создал `EventHandler`, содержащий репозиторий (`Repository`). 240 | Из репозитория можно получить Сущность, которую следует модифицировать. 241 | 242 | Тем не менее создание объектов `Event` внутри какого-либо метода Агрегата 243 | (`Aggregate`) - это нормально. Иногда я инициирую их внутри метода Сущности 244 | (`Entity`) и возвращаю как результат. Затем я используя структуры, не 245 | хранящие состояния, например, Сервисы предметной области (`Domain Service`) 246 | или Фабрики (`Factory`) для уведомления `EventPublisher`. 247 | 248 | ```go 249 | type Order struct { 250 | id uuid.UUID 251 | // 252 | // какие-то поля 253 | // 254 | isDispatched bool 255 | deliverAddress value_objects.Address 256 | } 257 | 258 | func (o Order) ID() uuid.UUID { 259 | return o.id 260 | } 261 | 262 | func (o Order) ChangeAddress(address value_objects.Address) events.Event { 263 | if o.isDispatched { 264 | return events.NewDeliveryAddressChangeFailed(o.ID()) 265 | } 266 | // 267 | // какой-то код 268 | // 269 | return events.NewDeliveryAddressChanged(o.ID()) 270 | } 271 | 272 | type OrderService struct { 273 | repository repository.OrderRepository 274 | publisher events.EventPublisher 275 | } 276 | 277 | func (s *OrderService) Create(order entity.Order) (*entity.Order, error) { 278 | result, err := s.repository.Create(order) 279 | if err != nil { 280 | return nil, err 281 | } 282 | // 283 | // обновляем адрес в базе данных 284 | // 285 | s.publisher.Notify(events.NewOrderCreated(result.ID())) 286 | 287 | return result, nil 288 | } 289 | 290 | func (s *OrderService) ChangeAddress(order entity.Order, address value_objects.Address) { 291 | evt := order.ChangeAddress(address) 292 | 293 | s.publisher.Notify(evt) // публикуем события только внутри объект, не хранящих состояние 294 | } 295 | ``` 296 | *Создание Событий* 297 | 298 | В приведенном выше примере Агрегат `Order` содержит метод для обновления 299 | адресов доставки. Результатом работы этого метода может быть Событие (`Event`). 300 | Это означает, что `Order` может создавать некоторые события, но не более. 301 | 302 | С другой стороны, `OrderService` может как создавать События, так и публиковать 303 | их. Он также может инициировать события, которые получает от `Order`, при обновлении 304 | адреса доставки. Это возможно, поскольку он содержит `EventPublisher`. 305 | 306 | ## События на других уровнях 307 | 308 | Мы можем прослушивать события на других уровнях, например, прикладных операций, 309 | представления или инфраструктуры. Мы также можем определить отдельные События, 310 | которые будут относиться только к этим уровням. В таких случаях мы не говорим о 311 | Событиях предметной области. 312 | 313 | Простым примером являются события на уровне прикладных операций. После создания 314 | Заказа (`Order`) в большинстве случаев мы должны отправить клиенту электронное 315 | письмо (`Email`). Хотя это может выглядеть как бизнес-правило, отправка 316 | электронных писем всегда зависит от приложения. 317 | 318 | В приведенном ниже примере показан простой код с `EmailEvent`. Как вы наверное 319 | догадались электронное письмо (`Email`) может иметь различные состояния 320 | и переход от одного к другому всегда выполняется во время некоторых событий 321 | (`Events`). 322 | 323 | ```go 324 | // инфраструктурный уровень 325 | type SQSService struct { 326 | svc *sqs.SQS 327 | publisher *EventPublisher 328 | stopChannel chan bool 329 | } 330 | 331 | // Run запускает прослушивание SQS сообщений 332 | func (s *SQSService) Run(event Event) { 333 | eventChan := make(chan Event) 334 | 335 | MessageLoop: 336 | for { 337 | s.listen(eventChan) 338 | 339 | select { 340 | case event := <-eventChan: 341 | s.publisher.Nofity(event) 342 | case <-s.stopChannel: 343 | break MessageLoop 344 | } 345 | } 346 | 347 | close(eventChan) 348 | close(s.stopChannel) 349 | } 350 | 351 | // Stop останавливает прослушивание SQS сообщений 352 | func (s *SQSService) Stop() { 353 | s.stopChannel <- true 354 | } 355 | 356 | func (s *SQSService) listen(eventChan chan Event) { 357 | go func() { 358 | message, err := s.svc.ReceiveMessage(&sqs.ReceiveMessageInput{ 359 | // 360 | // какой-то код 361 | // 362 | }) 363 | 364 | var event Event 365 | if err != nil { 366 | log.Print(err) 367 | event = NewGeneralError(err) 368 | return 369 | } else { 370 | // 371 | // извлечь сообщение 372 | // 373 | } 374 | 375 | eventChan <- event 376 | }() 377 | } 378 | ``` 379 | *Пример с Событиями прикладных операций* 380 | 381 | Иногда мы хотим инициировать событие предметной области вне нашего [Ограниченного 382 | контекста](https://martinfowler.com/bliki/BoundedContext.html). Эти события 383 | предметной области являются внутренними событиями для нашего Ограниченного 384 | контекста, но они являются внешними для других. 385 | 386 | Хотя эта тема относится больше к стратегическому предметно-ориентированному 387 | проектированию, я коснусь её здесь. Чтобы создать Событие вне нашего Микросервиса, 388 | мы можем использовать какой-то сервис обмена сообщениями, например, 389 | [SQS](https://aws.amazon.com/sqs/). 390 | 391 | ```go 392 | // инфраструктурный уровень 393 | import ( 394 | "encoding/json" 395 | "log" 396 | 397 | // 398 | // какой-то импорт 399 | // 400 | "github.com/aws/aws-sdk-go/aws" 401 | "github.com/aws/aws-sdk-go/service/sqs" 402 | ) 403 | 404 | // EventSQSHandler передаёт внутренние события во внешний мир 405 | type EventSQSHandler struct { 406 | svc *sqs.SQS 407 | } 408 | 409 | // Notify передаёт события через SQS 410 | func (e *EventSQSHandler) Notify(event Event) { 411 | data := map[string]string{ 412 | "event": event.Name(), 413 | } 414 | 415 | body, err := json.Marshal(data) 416 | if err != nil { 417 | log.Fatal(err) 418 | } 419 | 420 | _, err = e.svc.SendMessage(&sqs.SendMessageInput{ 421 | MessageBody: aws.String(string(body)), 422 | QueueUrl: &e.svc.Endpoint, 423 | }) 424 | if err != nil { 425 | log.Fatal(err) 426 | } 427 | } 428 | ``` 429 | *Передаём внутренние События во внешний мир* 430 | 431 | В приведенном выше фрагменте кода есть `EventSQSHandler`, простая структура 432 | в инфраструктурном уровне, которая отправляет сообщение в очередь SQS всякий раз, 433 | когда происходит какое-либо событие. Она публикует только названия событий 434 | без каких-либо конкретных деталей. 435 | 436 | Публикуя внутренние События во внешний мир, мы также можем прослушивать внешние 437 | События и сопоставлять их с внутренними. Для этого я всегда создаю какой-то 438 | Сервис в инфраструктурном уровне, который прослушивает события извне. 439 | 440 | ```go 441 | // инфраструктурный уровень 442 | type SQSService struct { 443 | svs *sqs.SQS 444 | publisher *EventPublisher 445 | stopChannel chan bool 446 | } 447 | 448 | // Run запускает прослушивание SQS сообщений 449 | func (s *SQSService) Run(event Event) { 450 | eventChan := make(chan Event) 451 | 452 | MessageLoop: 453 | for { 454 | s.listen(eventChan) 455 | 456 | select { 457 | case event := <- eventChan: 458 | s.publisher.Notify(event) 459 | case <-s.stopChannel: 460 | break MessageLoop 461 | } 462 | } 463 | 464 | close(eventChan) 465 | close(s.stopChannel) 466 | } 467 | 468 | // Stop останавливает прослушивание SQS сообщений 469 | func (s *SQSService) Stop() { 470 | s.stopChannel <- true 471 | } 472 | 473 | func (s *SQSService) listen(eventChan chan Event) { 474 | go func() { 475 | message, err := s.svc.ReceiveMessage(&sqs.ReceiveMessageInput{ 476 | // 477 | // какой-то код 478 | // 479 | }) 480 | 481 | var event Event 482 | if err != nil { 483 | log.Print(err) 484 | event = NewGeneralError(err) 485 | return 486 | } else { 487 | // 488 | // извлечь сообщение 489 | // 490 | } 491 | 492 | eventChan <- event 493 | }() 494 | } 495 | ``` 496 | *Прослушиваем внешние События* 497 | 498 | В приведенном выше примере показан SQSService внутри инфраструктурного уровня. 499 | Этот Сервис прослушивает SQS сообщения и сопоставляет их с внутренними 500 | событиями, если это возможно. 501 | 502 | Я нечасто использовал этот подход, но в некоторых случаях он того стоил. Например, 503 | если несколько микросервисов должны отреагировать на создание Заказа (`Order`) 504 | или когда регистрируется Клиент (`Customer`). 505 | 506 | ## Заключение 507 | 508 | События предметной области — это неотъемлемая часть нашей логики предметной 509 | области. Сегодня все в деловом мире привязано к определенным событиям, поэтому 510 | описание нашей модели предметной области с помощью событий является хорошей 511 | практикой. 512 | 513 | Шаблон Событие предметной области — это просто реализация шаблона Наблюдатель. 514 | Он может быть создан внутри многих объектов, но должен инициироваться только из 515 | объектов, не хранящих состояние. Другие уровни также могут использовать 516 | события предметной области или свои собственные. 517 | 518 | > Другие статьи из DDD цикла: 519 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 520 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 521 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 522 | 523 | ## Полезные ссылки на источники: 524 | 525 | * [https://martinfowler.com/](https://martinfowler.com/) 526 | * [https://www.domainlanguage.com/](https://www.domainlanguage.com/) -------------------------------------------------------------------------------- /docs/domain-service.md: -------------------------------------------------------------------------------- 1 | # DDD на практике в Golang: Сервисы предметной области 2 | 3 | ![intro](images/domain-service/intro.jpeg) 4 | *Фото [Nathan Dumlao](https://unsplash.com/@nate_dumlao) из [Unsplash](https://unsplash.com/)* 5 | 6 | После того как мы обсудили Сущности и Объекты-значения, я представлю в этой 7 | статье третий из группы шаблонов моделирования предметной области. Он называется 8 | Сервис. 9 | 10 | Сервис, вероятно, является наиболее часто неправильно используемым шаблоном DDD. 11 | Непонимание для чего предназначен Сервис предметной области возникает из-за его 12 | использования в различных веб-фреймворках. В большинстве фреймворков Сервис 13 | делает все. 14 | 15 | Там в нём хранится бизнес-логика. Он создаёт компоненты пользовательского 16 | интерфейса, например, поля формы. Он работает с сессиями и обрабатывает HTTP 17 | запросы. Иногда он просто играет роль огромного класса с "вспомогательными 18 | функциями". Иногда содержит код, который мог бы иметь простейший 19 | объект-значение. Периодически выполняет миграции в базе данных. 20 | 21 | Практически ничто из приведенного выше не должно находиться в Сервисе предметной 22 | области. В этой статье я постараюсь лучше объяснить его назначение и 23 | использование. 24 | 25 | > Другие статьи из DDD цикла: 26 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 27 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 28 | 29 | ## Он содержит логику работы 30 | В Сервисе описывается логика работы [рассматриваемой предметной области](https://www.definitions.net/definition/problem+domain). 31 | В нём реализованы решения для бизнес-инвариантов, которые слишком сложны, чтобы 32 | их хранить внутри одной сущности или объекта-значения. 33 | 34 | Иногда определенная логика работы требует взаимодействия с несколькими Сущностями 35 | или Объектами-значениями. В таких случаях тяжело определить к какой Сущности она 36 | относится. В таком случае следует использовать Сервис предметной области. 37 | 38 | > Сервисы предметной области не работают с сессиями или запросами. Они ничего не 39 | > знают о компонентах пользовательского интерфейса. Не выполняют миграции базы 40 | > данных. Не проверяет вводимые пользователем данные. Сервисы предметной области 41 | > отвечают только за бизнес-логику. 42 | 43 | ```go 44 | type ExchangeRateService interface { 45 | IsConversionPossible (from domain.Currency, to domain.Currency) bool 46 | Convert(to domain.Currency, from value_objects.Money) (value_objects.Money, error) 47 | } 48 | 49 | type DefaultExchangeRateService struct { 50 | repository repository.ExchangeRateRepository 51 | } 52 | 53 | func NewExchangeRateService(repository repository.ExchangeRateRepository) ExchangeRateService { 54 | return &DefaultExchangeRateService{ 55 | repository: repository, 56 | } 57 | } 58 | 59 | func (s *DefaultExchangeRateService) IsConversionPossible(from domain.Currency, to domain.Currency) bool { 60 | var result bool 61 | // 62 | // какой-то код 63 | // 64 | return result 65 | } 66 | 67 | func (s *DefaultExchangeRateService) Convert(to domain.Currency, from value_objects.Money) (value_objects.Money, error) { 68 | var result value_objects.Money 69 | // 70 | // какой-то код 71 | // 72 | return result, nil 73 | } 74 | ``` 75 | *Пример Сервиса предметной области* 76 | 77 | В приведенном выше примере рассматривается `ExchangeRateService`. Каждый раз 78 | когда я создаю некую структуру без состояния, которую я должен буду внедрить 79 | в другой объект, я определяю интерфейс. Это поможет позже при unit тестировании. 80 | 81 | Этот сервис отвечает за всю бизнес-логику обмена валюты. Он содержит 82 | `ExchangeRateRepository` для получения всех курсов, поэтому может преобразовать 83 | сумму в любой валюте. 84 | 85 | ```go 86 | type CasinoService struct { 87 | bonusRepository repository.BonusRepository 88 | accountService services.AccountService 89 | // 90 | // какие-нибудь другие поля 91 | // 92 | } 93 | 94 | func (s *CasinoService) Bet(account domain.Account, money value_objects.Money) error { 95 | bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money) 96 | if err != nil { 97 | return err 98 | } 99 | // 100 | // какой-то код 101 | // 102 | for _, bonus := range bonuses { 103 | err = bonus.Apply(&account) 104 | if err != nil { 105 | return err 106 | } 107 | } 108 | // 109 | // какой-то код 110 | // 111 | err = s.accountService.Update(account) 112 | if err != nil { 113 | return err 114 | } 115 | return nil 116 | } 117 | ``` 118 | *Случай со сложной логикой работы* 119 | 120 | Как я уже говорил, Сервис предметной области содержит бизнес-инварианты, которые 121 | слишком сложны для хранения в одной Сущности или Объекте-значении. В приведенном 122 | выше примере, `CasinoService` хранит сложную логику применения бонусов 123 | (`Bonuses`) всякий раз, когда с какой-то учетной записи (`Account`) делается 124 | новая ставка (`Bet`). 125 | 126 | Вместо того, чтобы создавать связи между Сущностями `Account` и `Bonus` или, 127 | что ещё хуже, передавать требуемые репозитории или сервисы в методы Сущности, 128 | мы должны создать Сервис предметной области. Он будет хранить всю бизнес-логику 129 | применения Бонусов (`Bonuses`) к любой необходимой учётной записи (`Account`). 130 | 131 | ## Он представляет собой контракт 132 | 133 | Иногда наш ограниченный контекст зависит от других. Классическим примером может 134 | быть кластер микросервисов, где один из них обращается ко второму через REST 135 | API. 136 | 137 | В большинстве случаев данные, полученные от внешнего API, играют решающую роль в 138 | работе первоначального ограниченного контекста. Таким образом, внутри нашего уровня 139 | предметной области мы должны иметь доступ к этим данным. 140 | 141 | > Мы всегда должны отделять нашу предметную область от технических деталей. Наличие 142 | > внешнего API или соединения с базой данных внутри бизнес-логики — это признак 143 | > кода с запашком. 144 | 145 | Здесь на помощь приходит Сервис предметной области. На уровне предметной области 146 | я всегда предоставляю интерфейс Сервиса как контракт для внешних интеграций. Затем 147 | мы можем внедрить этот интерфейс по всей нашей бизнес-логике, но реализация 148 | будет находиться на инфраструктурном уровне. 149 | 150 | ```go 151 | // уровень предметной области 152 | type AccountService interface { 153 | Update(account entity.Account) error 154 | } 155 | // инфраструктурный уровень 156 | type AccountAPIService struct { 157 | client *http.Client 158 | } 159 | 160 | func NewAccountService(client *http.Client) services.AccountService { 161 | return &AccountAPIService{ 162 | client: client, 163 | } 164 | } 165 | 166 | func (s *AccountAPIService) Update(account domain.Account) error { 167 | var request *http.Request 168 | // 169 | // какой-то код 170 | // 171 | response, err := s.client.Do(request) 172 | if err != nil { 173 | return err 174 | } 175 | // 176 | // какой-то код 177 | // 178 | fmt.Printf("Response code: %d", response.StatusCode) 179 | return nil 180 | } 181 | ``` 182 | *Сервис предметной области как контракт* 183 | 184 | В приведенном выше примере я определил интерфейс `AccountService` на уровне 185 | предметной области. Он представляет собой контракт, который могут использовать 186 | другие Сервисы предметной области. Интерфейс реализован в виде 187 | `AccountAPIService`. 188 | 189 | `AccountAPIService` отправляет HTTP-запросы во внешнюю [CRM-систему](https://financesonline.com/what-are-examples-of-crm-different-tool-types-you-should-know-about/) или нашему 190 | внутреннему микросервису, предназначенному только для работы с учётными записями (`Accounts`). 191 | Таким образом, используя такой подход, мы сможем создать ещё одну реализацию 192 | `AccountService`, которая будет работать с тестовыми учётными записями (`Accounts`) из файла 193 | в изолированной тестовой среде. 194 | 195 | ## Не хранит состояний 196 | 197 | **Сервис предметной области НЕ должен хранить состояния. Он также НЕ должен иметь 198 | полей, которые имеют состояние.** 199 | 200 | Это правило может показаться очевидным, но на самом деле таким не является. В 201 | зависимости от уровня подготовки каждого конкретного разработчика, некоторые 202 | их них имеют опыт веб-разработки с языками, которые запускают изолированные 203 | процессы для каждого запроса. 204 | 205 | В таких случаях неважно хранит Сервис состояние или нет. Но при работе с Go, вы 206 | вероятно будете использовать один экземпляр Сервиса предметной области для всего 207 | приложения. Вы наверно представляете, что может произойти, если множество различных 208 | клиентов обратятся к одному и тому же значению в памяти. 209 | 210 | ```go 211 | // Сущность хранит состояние 212 | type Account struct { 213 | ID uint 214 | Person Person 215 | Wallets []Wallet 216 | } 217 | 218 | // Объект-значение хранит состояние 219 | type Money struct { 220 | Amount int 221 | Currency Currency 222 | } 223 | 224 | // Сервис предметной области зависит только от других не хранящих состояние конструкций, например: 225 | // сервисов, репозиториев, фабрик, объектов, определяющие настройки приложения 226 | type DefaultExchangeRateService struct { 227 | repository *ExchangeRateRepository 228 | useForceRefresh bool 229 | } 230 | 231 | type CasinoService struct { 232 | bonusRepository BonusRepository 233 | bonusFactory BonusFactory 234 | accountService AccountService 235 | } 236 | ``` 237 | *Сравнение Сервиса предметной области с Сущностью и Объектом-значением* 238 | 239 | Как видно из приведенного выше примера, Сущности и Объект-значение хранят 240 | состояние. Сущность может изменять состояние во время выполнения, а объекты-значения 241 | всегда остаются неизменными. Когда нам нужно изменить объект-значение, мы 242 | создаём новый объект. 243 | 244 | Сервиса предметной области не содержат какие-либо объекты, хранящие состояния. 245 | Они состоят только из других структур без состояния, таких как репозиторий, 246 | другой Сервис, Фабрика, значения настроек. Он может инициализировать создание 247 | состояние или его сохранение, но не хранит его. 248 | 249 | ```go 250 | // неправильно - состояние хранится внутри сервиса 251 | type TransactionService struct { 252 | bonusRepository repository.BonusRepository 253 | result value_objects.Money // поле, которое содержит состояние 254 | } 255 | 256 | func (s *TransactionService) Deposit(account entity.Account, money value_objects.Money) error { 257 | bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money) 258 | if err != nil { 259 | return err 260 | } 261 | fmt.Printf("%v", bonuses) 262 | // 263 | // какой-то код 264 | // 265 | s.result, err = s.result.Add(money) // изменяем состояние сервиса 266 | if err != nil { 267 | return err 268 | } 269 | return nil 270 | } 271 | ``` 272 | *Неправильный подход к хранению состояния внутри Сервиса предметной области* 273 | 274 | В приведенном выше примере `TransactionService` содержит поле, хранящее 275 | состояние в виде объекта-значения `Money`. Каждый раз когда мы хотим положить 276 | деньги на счёт, мы применяем бонусы к зачисляемому значению, `money`, а затем 277 | прибавляем его к `result`, которое является полем внутри Сервиса. 278 | 279 | Такой подход — неправильный. Результат меняется каждый раз, когда кто-то кладёт 280 | деньги на счёт. Это не то, чего мы хотим. Вместо этого мы должны вернуть 281 | вычисления в качестве результата работы метода, как показано в примере ниже. 282 | 283 | ```go 284 | // правильно - состояние передаётся в виде аргумента current 285 | type TransactionService struct { 286 | bonusRepository repository.BonusRepository 287 | } 288 | 289 | func (s *TransactionService) Deposit(current value_objects.Money, account entity.Account, money value_objects.Money) (value_objects.Money, error) { 290 | bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money) 291 | if err != nil { 292 | return value_objects.Money{}, err 293 | } 294 | fmt.Printf("%v", bonuses) 295 | // 296 | // какой-то код 297 | // 298 | return current.Add(money) // возвращаем новое значение, которое представляет новое состояние 299 | } 300 | 301 | func main() { 302 | // 303 | // какой-то код 304 | // 305 | LOOP: 306 | for true { 307 | select { 308 | case deposit := <- moneyChan: 309 | current, err := service.Deposit(current, account, deposit) 310 | if err != nil { 311 | log.Fatal(err) 312 | } 313 | case <-quitChan: 314 | break LOOP 315 | } 316 | } 317 | // 318 | // какой-то код 319 | // 320 | } 321 | ``` 322 | *Gравильный подход - состояние возвращается из Сервиса предметной области* 323 | 324 | Новый `TransactionService` всегда производит вычисления с переданным аргументом 325 | вместо того, чтобы хранить его внутри. Разные пользователи не могут совместно 326 | использовать один и тот же объект в памяти, и Сервис предметной области снова 327 | ведёт себя как единый экземпляр. 328 | 329 | ## Сравнение Сервисов предметной области с другими типами сервисов 330 | 331 | Пока что, должно быть понятно, когда нужно создавать Сервис предметной области. 332 | Но в некоторых случаях неясно является ли Сервис Сервисом предметной области. 333 | Или, если выражаться яснее, к какому уровню принадлежит Сервис? 334 | 335 | Инфраструктурный Сервисы легко распознать. Они всегда содержат технические 336 | детали, интеграцию с базой данных или внешним API. В большинстве случаев это 337 | фактические реализации интерфейсов других слоёв. 338 | 339 | Сервисы уровня представления также легко распознать. Они всегда содержат некую 340 | логику, относящуюся к компонентам пользовательского интерфейса или валидации 341 | пользовательского ввода. Типичным примером являются сервисы для [работы с 342 | формами](https://symfony.com/doc/current/forms.html). 343 | 344 | Проблема возникает, когда нужно отличать Сервисы прикладных операций 345 | (`Application`) и предметной области (`Domain`). Как оказалось, труднее всего 346 | найти отличие между этими двумя типами. 347 | 348 | Исходя из моего опыта, я использовал Сервисы прикладных операций только для 349 | реализации общей логики работы с сессиями или обработки запросов. Алгоритм 350 | авторизации и прав доступа тоже можно разместить на этом уровне. 351 | 352 | ```go 353 | type AccountSessionService struct { 354 | accountService AccountService 355 | } 356 | 357 | func (s *AccountSessionService) GetAccount(session *sessions.Session) (*Account, error) { 358 | value, ok := session.Values["accountID"] 359 | if !ok { 360 | return nil, errors.New("there is no account in session") 361 | } 362 | 363 | id, ok := value.(string) 364 | if !ok { 365 | return nil, errors.New("invalid value for account ID in session") 366 | } 367 | 368 | account, err := s.accountService.ByID(id) 369 | if err != nil { 370 | return nil, err 371 | } 372 | 373 | return account, nil 374 | } 375 | ``` 376 | *Пример Сервиса прикладных операций* 377 | 378 | Во многих случаях Сервис прикладных операций — это обертка Сервиса предметной 379 | области. Я использовал такой подход всякий раз, когда хотел что-то кешировать 380 | внутри сессии и использовать Сервис предметной области в качестве резервного 381 | источника данных. Этот подход показан в приведенном выше примере. 382 | 383 | Здесь AccountSessionService - это Сервис прикладных операций, который обертывает 384 | `AccountService` из уровня предметной области. Он отвечает за извлечения значения 385 | из сессии и затем использует его для поиска `Account` в сервисе 386 | `AccountService`. 387 | 388 | ## Заключение 389 | 390 | Сервис предметной области представляет собой структуру, не хранящую состояния, 391 | реализующую бизнес-логику. Он взаимодействует со многими различными объектами, 392 | такими как Сущность (`Entity`) и Объект-значение (`Value Object`). В сервис 393 | переносится сложная логика работы из них или та логика, которую непонятно куда 394 | лучше поместить. 395 | 396 | Сервис предметной области не имеет ничего общего с сервисами из других уровней, 397 | кроме названия. Он используется только для бизнес-логики и не должен 398 | взаимодействовать с техническими деталями, сессиями, запросами или чем-либо ещё, 399 | специфичным для приложения. 400 | 401 | > Другие статьи из DDD цикла: 402 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 403 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 404 | 405 | ## Полезные ссылки на источники: 406 | 407 | * [https://martinfowler.com/](https://martinfowler.com/) 408 | * [https://www.domainlanguage.com/](https://www.domainlanguage.com/) -------------------------------------------------------------------------------- /docs/factory.md: -------------------------------------------------------------------------------- 1 | # DDD на практике в Golang: Фабрика 2 | 3 | ![intro](images/factory/intro.jpeg) 4 | *Фото [Omar Flores](https://unsplash.com/@__itsflores) из [Unsplash](https://unsplash.com/)* 5 | 6 | Когда я писал заголовок этой статьи, я пытался вспомнить первый шаблон проектирования, 7 | который узнал из [«Банды четырех»](https://springframework.guru/gang-of-four-design-patterns/). 8 | Я думаю это был один из следующих: [Фабричный метод](https://refactoring.guru/design-patterns/factory-method), 9 | [Синглтон](https://refactoring.guru/design-patterns/singleton) или 10 | [Декоратор](https://refactoring.guru/design-patterns/decorator). 11 | 12 | Я уверен у других разработчиков программного обеспечения существует похожая 13 | история. Когда они начали изучать шаблоны проектирования либо фабричный метод 14 | (`Factory Method`), либо [Абстрактная Фабрика](https://refactoring.guru/design-patterns/abstract-factory) 15 | (`Abstract Factory`) были одними из первых трёх, о которых они узнали. 16 | 17 | Сегодня любая производная шаблона Фабрики является неотъемлемой частью 18 | предметно-ориентированного проектирования. И его цель остается прежней даже 19 | спустя многие десятилетия. 20 | 21 | > Другие статьи из DDD цикла: 22 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 23 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 24 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 25 | > 4. [DDD на практике в Golang: Событие предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 26 | > 5. [DDD на практике в Golang: Модуль](https://levelup.gitconnected.com/practical-ddd-in-golang-module-51edf4c319ec) 27 | > 6. [DDD на практике в Golang: Агрегат](https://levelup.gitconnected.com/practical-ddd-in-golang-aggregate-de13f561e629) 28 | 29 | ## Сложная логика при создании 30 | 31 | Мы используем шаблон Фабрика, если логика создания объекта сложна или для изоляции 32 | процесса создания от другой бизнес-логики. В таких случаях гораздо лучше иметь 33 | выделенное место в коде, которое мы можем протестировать отдельно. 34 | 35 | Когда я создаю Фабрику, в большинстве случаев, она является частью уровня 36 | предметной области. Таким образом, я могу использовать её везде в приложении. 37 | Ниже приведён простой пример Фабрики. 38 | 39 | ```go 40 | type Loan struct { 41 | ID uuid.UUID 42 | // 43 | // какие-то поля 44 | // 45 | } 46 | 47 | type LoanFactory interface { 48 | CreateShortTermLoan(specification LoanSpecification) Loan 49 | CreateLongTermLoan(specification LoanSpecification) Loan 50 | } 51 | ``` 52 | *Пример шаблона Фабрика* 53 | 54 | Шаблон Фабрика (`Factory`) тесно связан с шаблоном Спецификация `Specification` 55 | (я расскажу о нём в следующих статьях). Здесь приведён небольшой пример с 56 | `LoanFactory`, `LoanSpecification` и `Loan`. 57 | 58 | `LoanFactory` представляет собой шаблон `Factory` в DDD или более точно 59 | фабричный метод (`Factory Method`). Он отвечает за создание и выдачу новых 60 | экземпляров Ссуды `Loan`, которая может меняться в зависимости от периода 61 | выплаты. 62 | 63 | ## Вариативность 64 | 65 | Как уже говорилось, шаблон Фабрика можно реализовать по-разному. Форма, которая 66 | чаще всего используется, по крайней мере мной, - это Фабричный метод. В этом 67 | случае мы предоставляем некоторые методы создающие нашу структуру. 68 | 69 | ```go 70 | const ( 71 | LongTerm = iota 72 | ShortTerm 73 | ) 74 | 75 | type Loan struct { 76 | ID uuid.UUID 77 | Type int 78 | BankAccountID uuid.UUID 79 | Amount value_objects.Money 80 | RequiredLifeInsurance bool 81 | } 82 | 83 | type LoanFactory struct{} 84 | 85 | func (f *LoanFactory) CreateShortTermLoan(bankAccountID uuid.UUID, amount value_objects.Money) Loan { 86 | return Loan{ 87 | Type: ShortTerm, 88 | BankAccountID: bankAccountID, 89 | Amount: amount, 90 | } 91 | } 92 | 93 | func (f *LoanFactory) CreateLongTermLoan(bankAccountID uuid.UUID, amount value_objects.Money) Loan { 94 | return Loan{ 95 | Type: LongTerm, 96 | BankAccountID: bankAccountID, 97 | Amount: amount, 98 | RequiredLifeInsurance: true, 99 | } 100 | } 101 | ``` 102 | *Пример с фабричным методом* 103 | 104 | В приведенном выше фрагменте кода `LoanFactory` теперь является конкретной 105 | реализацией фабричного метода. Он предоставляет два метода для создания 106 | экземпляров Сущности Ссуда (`Loan`). 107 | 108 | В этом случае мы создаём один и тот же объект, но с различными значениями полей, 109 | в зависимости от того является Ссуда (`Loan`) кратко- или долгосрочной. Разница 110 | между этими двумя случаями может быть ещё более сложной и каждая дополнительная 111 | особенность, которую нужно учесть при создании объекта, оправдывает 112 | существование этого шаблона. 113 | 114 | ```go 115 | type Investment interface { 116 | Amount() value_objects.Money 117 | } 118 | 119 | type EtfInvestment struct { 120 | ID uuid.UUID 121 | EtfID uuid.UUID 122 | InvestedAmount value_objects.Money 123 | BankAccountID uuid.UUID 124 | } 125 | 126 | func (e EtfInvestment) Amount() value_objects.Money { 127 | return e.InvestedAmount 128 | } 129 | 130 | type StockInvestment struct { 131 | ID uuid.UUID 132 | CompanyID uuid.UUID 133 | InvestedAmount value_objects.Money 134 | BankAccountID uuid.UUID 135 | } 136 | 137 | func (s StockInvestment) Amount() value_objects.Money { 138 | return s.InvestedAmount 139 | } 140 | 141 | type InvestmentSpecification interface { 142 | Amount() value_objects.Money 143 | BankAccountID() uuid.UUID 144 | TargetID() uuid.UUID 145 | } 146 | 147 | type InvestmentFactory interface { 148 | Create(specification InvestmentSpecification) Investment 149 | } 150 | 151 | type EtfInvestmentFactory struct{} 152 | 153 | func (f *EtfInvestmentFactory) Create(specification InvestmentSpecification) Investment { 154 | return EtfInvestment{ 155 | EtfID: specification.TargetID(), 156 | InvestedAmount: specification.Amount(), 157 | BankAccountID: specification.BankAccountID(), 158 | } 159 | } 160 | 161 | type StockInvestmentFactory struct{} 162 | 163 | func (f *StockInvestmentFactory) Create(specification InvestmentSpecification) Investment { 164 | return StockInvestment{ 165 | CompanyID: specification.TargetID(), 166 | InvestedAmount: specification.Amount(), 167 | BankAccountID: specification.BankAccountID(), 168 | } 169 | } 170 | ``` 171 | *Пример с Абстрактной Фабрикой* 172 | 173 | В вышеприведенном примере дан фрагмент кода с шаблоном Абстрактная Фабрика. В 174 | этом случае мы хотим создать несколько экземпляров интерфейса `Investment`. 175 | 176 | Поскольку существует несколько реализаций этого интерфейса, сейчас идеальный 177 | момент для добавления шаблона Фабрика. И `EtfInvestmentFactory`, и 178 | `StockInvestmentFactory` создают экземпляры удовлетворяющие интерфейсу 179 | `Investment`. 180 | 181 | В нашем коде мы можем сохранить их в некоторой карте интерфейсов 182 | `InvestmentFactory` и использовать их всякий раз, когда мы хотим создать 183 | `Investment` из любого `BankAccount`. 184 | 185 | Это идеальное место для использования абстрактной фабрики, поскольку мы должны 186 | создавать некие объекты из определенного набора (на самом деле может существовать 187 | ещё больше различных инвестиций). 188 | 189 | ## Преобразование 190 | 191 | Мы можем использовать шаблон Фабрика на других уровнях. По крайней мере я его 192 | использую на инфраструктурном уровне и уровне представления. Там я преобразую 193 | [Объекты для передачи данных](https://martinfowler.com/eaaCatalog/dataTransferObject.html) 194 | (`Data Transfer Objects`) в Сущности и наоборот. 195 | 196 | ```go 197 | // уровень предметной области 198 | type CryptoInvestment struct { 199 | ID uuid.UUID 200 | CryptoCurrencyID uuid.UUID 201 | InvestedMoney value_objects.Money 202 | BankAccountID uuid.UUID 203 | } 204 | 205 | // инфраструктурный уровень 206 | type CryptoInvestmentGorm struct { 207 | ID int `gorm:"primaryKey;column:id"` 208 | UUID string `gorm:"column:uuid"` 209 | CryptoCurrencyID int `gorm:"column:crypto_currency_id"` 210 | CryptoCurrency CryptoCurrencyGorm `gorm:"foreignKey:CryptoCurrencyID"` 211 | InvestedAmount int `gorm:"column:amount"` 212 | InvestedCurrencyID int `gorm:"column:currency_id"` 213 | Currency dto.CurrencyGorm `gorm:"foreignKey:InvestedCurrencyID"` 214 | BankAccountID int `gorm:"column:bank_account_id"` 215 | BankAccount dto.BankAccountGorm `gorm:"foreignKey:BankAccountID"` 216 | } 217 | 218 | type CryptoInvestmentDBFactory struct { 219 | } 220 | 221 | func (f *CryptoInvestmentDBFactory) ToEntity(dto CryptoInvestmentGorm) (model.CryptoInvestment, error) { 222 | id, err := uuid.Parse(dto.UUID) 223 | if err != nil { 224 | return model.CryptoInvestment{}, err 225 | } 226 | 227 | cryptoId, err := uuid.Parse(dto.CryptoCurrency.UUID) 228 | if err != nil { 229 | return model.CryptoInvestment{}, err 230 | } 231 | 232 | currencyId, err := uuid.Parse(dto.Currency.UUID) 233 | if err != nil { 234 | return model.CryptoInvestment{}, err 235 | } 236 | 237 | accountId, err := uuid.Parse(dto.BankAccount.UUID) 238 | if err != nil { 239 | return model.CryptoInvestment{}, err 240 | } 241 | 242 | return model.CryptoInvestment{ 243 | ID: id, 244 | CryptoCurrencyID: cryptoId, 245 | InvestedMoney: value_objects.NewMoney(dto.InvestedAmount, currencyId), 246 | BankAccountID: accountId, 247 | }, nil 248 | } 249 | ``` 250 | *Пример преобразования* 251 | 252 | `CryptoInvestmentDBFactory` - это фабрика внутри инфраструктурного уровня, 253 | используемая для реконструкции объекта `CryptoInvestment`. Здесь показан 254 | только метод преобразования DTO в Сущность, но эта же Фабрика может иметь 255 | метод преобразования Сущности (`Entity`) в DTO. 256 | 257 | Поскольку `CryptoInvestmentDBFactory` использует структуру как для инфраструктуры 258 | (`CryptoInvestmentGorm`), так и для предметной области (`CryptoInvestment`), 259 | она должна находиться внутри инфраструктурного уровня, поскольку у нас не может 260 | быть никаких зависимостей от других уровней внутри уровня предметной области. 261 | 262 | Я всегда любил использовать UUID внутри бизнес-логики и выдавать только UUID в 263 | качестве ответа API. Но поскольку база данных плохо работает со строками или 264 | двоичными данными в качестве первичных ключей, Фабрика кажется подходящим 265 | местом для выполнения этого преобразования. 266 | 267 | ## Заключение 268 | 269 | Шаблон Фабрика (`Factory`) — это принцип, уходящий корнями в старые шаблоны из 270 | «Банды четырех». Мы можем реализовать его в виде Абстрактной Фабрики или 271 | фабричного метода. 272 | 273 | Мы используем его в тех случаях, когда хотим отделить логику создания от другой 274 | бизнес-логики. Мы также можем применять его для преобразования наших Сущностей 275 | в DTO и наоборот. 276 | 277 | > Другие статьи из DDD цикла: 278 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 279 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 280 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 281 | > 4. [DDD на практике в Golang: Событие предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 282 | > 5. [DDD на практике в Golang: Модуль](https://levelup.gitconnected.com/practical-ddd-in-golang-module-51edf4c319ec) 283 | > 6. [DDD на практике в Golang: Агрегат](https://levelup.gitconnected.com/practical-ddd-in-golang-aggregate-de13f561e629) 284 | 285 | ## Полезные ссылки на источники: 286 | 287 | * [https://martinfowler.com/](https://martinfowler.com/) 288 | * [https://www.domainlanguage.com/](https://www.domainlanguage.com/) -------------------------------------------------------------------------------- /docs/images/aggregate/boundary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/aggregate/boundary.png -------------------------------------------------------------------------------- /docs/images/aggregate/boundary2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/aggregate/boundary2.png -------------------------------------------------------------------------------- /docs/images/aggregate/business-invariants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/aggregate/business-invariants.png -------------------------------------------------------------------------------- /docs/images/aggregate/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/aggregate/intro.jpeg -------------------------------------------------------------------------------- /docs/images/domain-event/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/domain-event/intro.jpeg -------------------------------------------------------------------------------- /docs/images/domain-service/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/domain-service/intro.jpeg -------------------------------------------------------------------------------- /docs/images/entity/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/entity/intro.jpeg -------------------------------------------------------------------------------- /docs/images/factory/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/factory/intro.jpeg -------------------------------------------------------------------------------- /docs/images/module/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/module/diagram.png -------------------------------------------------------------------------------- /docs/images/module/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/module/intro.jpeg -------------------------------------------------------------------------------- /docs/images/repository-pattern-for-gorm/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/repository-pattern-for-gorm/intro.jpeg -------------------------------------------------------------------------------- /docs/images/repository-pattern-for-gorm/uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/repository-pattern-for-gorm/uml.png -------------------------------------------------------------------------------- /docs/images/repository/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/repository/intro.jpeg -------------------------------------------------------------------------------- /docs/images/specification/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/specification/intro.jpeg -------------------------------------------------------------------------------- /docs/images/value-object/intro.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaksimDzhangirov/practicalDDD/de90450b0da354e3a9a3f7397b09d09ff31f3e7c/docs/images/value-object/intro.jpeg -------------------------------------------------------------------------------- /docs/module.md: -------------------------------------------------------------------------------- 1 | # DDD на практике в Golang: Модуль 2 | 3 | ![intro](images/module/intro.jpeg) 4 | *Фото [Amos Bar-Zeev](https://unsplash.com/@amosbarzeev) из [Unsplash](https://unsplash.com/)* 5 | 6 | На первый взгляд Модуль не похож на шаблон, по крайней мере, на то, что мы 7 | считаем шаблоном при разработке программного обеспечения. С этим можно 8 | согласиться, поскольку кто-то больше воспринимает модуль как часть структуры 9 | проекта, а не шаблон. 10 | 11 | Дополнительные проблемы возникают, когда мы рассматриваем Go модули. Они включают 12 | в себя коллекции Go пакетов, используют систему версий и релизов. Мы используем 13 | эти модули для управления зависимостями в Go. 14 | 15 | Итак, если Go модули и пакеты влияют на структуру проекта, кажется, что они 16 | должны как-то быть связаны с шаблоном Модуль (`Module`) в DDD. В действительности 17 | так и есть. 18 | 19 | > Другие статьи из DDD цикла: 20 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 21 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 22 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 23 | > 4. [Practical DDD in Golang: Domain Event](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 24 | 25 | ## Структура 26 | 27 | В Go мы используем пакеты для группировки нашего кода. Пакеты соответствуют 28 | структуре папок внутри наших проектов, хотя могут иметь различные названия. 29 | Эти различия возникают потому, что мы можем назвать наш пакет иначе, чем 30 | папку. 31 | 32 | ```go 33 | // папка pkg/access/domain/model 34 | package access_model 35 | 36 | import ( 37 | "github.com/google/uuid" 38 | ) 39 | 40 | type User struct { 41 | ID uuid.UUID 42 | // 43 | // какие-то поля 44 | // 45 | } 46 | 47 | // папка pkg/access/domain/service 48 | package access_service 49 | 50 | import ( 51 | "project/pkg/access/domain/model" 52 | ) 53 | 54 | type UserService interface { 55 | Create(user access_model.User) error 56 | // 57 | // какие-то методы 58 | // 59 | } 60 | ``` 61 | *Разница между названием папки и названием пакета* 62 | 63 | В вышеприведенном примере видны небольшие отличия в именовании папок и пакетов. 64 | Иногда, если у меня много пакетов с моделями, я добавляю им префиксы моих DDD 65 | модулей, чтобы было легче ссылаться на несколько моделей в одном файле. 66 | 67 | Теперь мы скорее всего уже имеете некоторое представление о том, что являлось бы 68 | DDD модулем в предыдущем примере. Там Модуль (`Module`) - это папка access со 69 | всеми его дочерними пакетами. 70 | 71 | ```shell 72 | project 73 | |--cmd 74 | |--main.go 75 | |--internal 76 | |--module1 77 | |--infrastructure 78 | |--presentation 79 | |--application 80 | |--domain 81 | |--service 82 | |--factory 83 | |--repository 84 | |--model 85 | |--module1.go 86 | |--module2 87 | |--... 88 | |--... 89 | |--pkg 90 | |--module3 91 | |--... 92 | |--module4 93 | |--... 94 | |--... 95 | |--go.mod 96 | |--... 97 | ``` 98 | 99 | Структура папок из вышеприведенной схемы является моей любимой структурой 100 | проекта, реализующего предметно-ориентированное проектирование в Go. Иногда 101 | я вношу изменения в некоторые папки, но всегда стараюсь сохранять DDD модули 102 | в одной и той же форме. 103 | 104 | В моих проектах каждый модуль имеет максимум четыре базовых пакета: 105 | `infrastructure`, `presentation`, `application` и `domain`. Как видите, мне 106 | нравится следовать принципам [многоуровневой архитектуры](https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html). 107 | 108 | Здесь я поместил пакет `infrastructure` в самом вверху. Это связано с тем, что 109 | следуя [принципу инверсии зависимостей](https://stackify.com/dependency-inversion-principle/) [Дяди Боба](https://twitter.com/unclebobmartin) 110 | мои низкоуровневые сервисы из инфраструктурного уровня реализуют высокоуровневые 111 | интерфейсы других уровней. 112 | 113 | При таком подходе я определяю `Port` как интерфейс `UserRepository` 114 | на уровне предметной области. Фактическая реализация находится на 115 | инфраструктурном уровне и ей могут соответствовать несколько адаптеров, 116 | например, `UserDBRepository` или `UserFakeRepository`. 117 | 118 | ```go 119 | // папка pkg/access/domain/repository 120 | package access_repository 121 | 122 | import access_model "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/domain/model" 123 | 124 | type UserRepository interface { 125 | Create(user access_model.User) error 126 | } 127 | 128 | package database 129 | 130 | type UserDBRepository struct { 131 | // 132 | // какие-то поля 133 | // 134 | } 135 | 136 | func (r *UserDBRepository) Create(user access_model.User) error { 137 | // 138 | // какие-то код 139 | // 140 | return nil 141 | } 142 | 143 | package fake 144 | 145 | type UserFakeRepository struct { 146 | // 147 | // какие-то поля 148 | // 149 | } 150 | 151 | func (r *UserFakeRepository) Create(user access_model.User) error { 152 | // 153 | // какие-то код 154 | // 155 | return nil 156 | } 157 | ``` 158 | *Порты и Адаптеры* 159 | 160 | Понятия "Портов" и "Адаптеров" не является чем-то новым и относятся к принципам 161 | [гексагональной архитектуры](https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c) (`Hexagonal Architecture`). 162 | Это второй принцип, который я использую при разработке своих DDD модулей и для 163 | меня он является решающим. 164 | 165 | Возвращаясь к структуре пакетов внутри модуля, каждый уровень знает всё о 166 | нижестоящих, и никто ничего не знает о них вне модуля. Таким образом, уровень 167 | `infrastructure` может зависеть от всех уровней, а уровень `domain` не зависит 168 | ни от одного. 169 | 170 | Сразу за инфраструктурным уровнем находится уровень представления. Мы также 171 | могли называть это уровень `interface`, но это зарезервированное слово в Go, 172 | поэтому будем использовать `presentation`. Наконец между уровнями `presentation` 173 | и `domain` находится `application`. 174 | 175 | Преимущество использования таких уровней в Go заключается в том, что это 176 | помогает нам избежать циклических зависимостей, которые не позволяют скомпилировать 177 | наш код. Используя эти уровни и направление зависимостей, мы можем избавиться от 178 | болезненного рефакторинга кода. 179 | 180 | Наконец, наверное вы заметили, что некоторые папки (или пакеты) внутри уровня 181 | `domain` называются: `model`, `service` и т.д. Я иногда добавляю их, чтобы мои 182 | пакеты были как можно проще. 183 | 184 | Иногда я использую эту внутреннюю подструктуру, чтобы избежать циклических 185 | зависимостей в Go. Я предпочитаю, когда папка `model` находится в самом низу, а 186 | `service` вверху, но каждый может выбирать как ему нравится. 187 | 188 | ## Логический кластер 189 | 190 | DDD модуль — это не просто группа файлов и папок расположенных вместе. Код внутри 191 | этих файлов и папок должен представлять некую связанную конструкцию. И кроме того, 192 | два разных Модуля (`Modules`) должны быть слабо связаны, с минимальными 193 | зависимостями между ними. 194 | 195 | ``` 196 | project 197 | |--... 198 | |--pkg 199 | |--access 200 | |--infrastructure 201 | |--... 202 | |--presentation 203 | |--... 204 | |--application 205 | |--service 206 | |--authorization.go 207 | |--registration.go 208 | |--domain 209 | |--repository 210 | |--user.go 211 | |--group.go 212 | |--role.go 213 | |--model 214 | |--user.go 215 | |--group.go 216 | |--role.go 217 | |--access.go 218 | |--shopping 219 | |--infrastructure 220 | |--... 221 | |--presentation 222 | |--... 223 | |--application 224 | |--service 225 | |--session_basket.go 226 | |--domain 227 | |--service 228 | |--shopping.go 229 | |--factory 230 | |--basket.go 231 | |--repository 232 | |--order.go 233 | |--model 234 | |--order.go 235 | |--basket.go 236 | |--shopping.go 237 | |--customer 238 | |--infrastructure 239 | |--... 240 | |--presentation 241 | |--... 242 | |--application 243 | |--... 244 | |--domain 245 | |--repository 246 | |--customer.go 247 | |--address.go 248 | |--model 249 | |--customer.go 250 | |--address.go 251 | |--customer.go 252 | |--... 253 | |--... 254 | ``` 255 | 256 | Вышеприведенная структура — это простой пример использования DDD модулей. Здесь 257 | у нас три модуля (их может быть и больше), названных `access`, `shopping` и 258 | `customer`. 259 | 260 | Модуль `access` связан с процессом авторизации и регистрации. Он содержит всю 261 | логику работы с пользователем (`User`) во время одной сессии. Кроме того в нём 262 | хранятся права доступа каждого из них и он решает — могут ли они получить доступ 263 | к конкретному объекту. 264 | 265 | Модуль `customer` содержит информацию о Клиентах `Customers` и их адресах 266 | `Addresses`. Хотя они могут быть похожи на `User`, он представляет Сущность, 267 | осуществляющую Заказы (`Orders`), тогда как `User` - это Сущность для работы с 268 | сессиями. Кроме того у одного и того же пользователя может быть множество 269 | адресов доставки, как мы уже видели на различных платформах. 270 | 271 | Наконец, модуль `shopping` представляет собой кластер, хранящий всю логику, 272 | включая создание корзины, хранение её в сессии, а также дальнейшее создание 273 | заказов. Этот модуль `shopping` выглядит более сложным, чем два других, и, 274 | действительно, зависит от них. 275 | 276 | Как и в случае со слоями мы также должны отслеживать зависимости между модулями 277 | и следить за тем, чтобы они были однонаправленными. Иначе компилятор выдаст 278 | ошибку. 279 | 280 | ![diagram](images/module/diagram.png) 281 | *Диаграмма зависимостей модулей* 282 | 283 | Как показано на рисунке выше, Модуль `shopping` использует Модуль `customer`, 284 | чтобы узнать, кому принадлежит заказ. Из него он может извлечь адрес (`Address`) 285 | доставки. Он также зависит от модуля `access`, для проверки прав доступа к 286 | определенным корзинам (`Baskets`) и её составляющим (`Items`). 287 | 288 | Модуль `customer` зависит только от модуля `access`. Он обеспечивает связь с 289 | `User` в сессии, а также содержит список Клиентов (`Customers`), позволяя 290 | определить кому отправить Заказ (`Order`). 291 | 292 | `customer` и `shopping` могут вместе определять один [ограниченный контекст](https://martinfowler.com/bliki/BoundedContext.html). 293 | Отдельный модуль необязательно должен представлять один ограниченный контекст. 294 | Я люблю разбивать один ограниченный контекст на несколько Модулей. 295 | 296 | Модуль `access` выглядит как кандидат для другого ограниченного контекста и в 297 | будущем мы можем подумать о его размещении в другом месте. Позднее другие 298 | ограниченные контексты могут зависеть от ограниченного контекста `access`. 299 | 300 | Хотя может показаться, что `shopping` и `customer` можно объединить, в нашем 301 | приложении мы решили разделить их. Причина в том, что Клиент (`Customer`) может осуществлять 302 | другие различные операции, независимые от Заказов (`Orders`). 303 | 304 | Мы можем менять адреса (`Addresses`), просматривать нашу историю покупок, отслеживать доставку, 305 | связываться со службой поддержки. Изменения в реквизитах клиента не должны влиять 306 | на те, что указаны в Заказе (`Order`). Также изменения адреса в каком-то Заказе 307 | (`Order`) не влияют на Клиента (`Customer`). Мы можем независимо работать с ними. 308 | 309 | ## Наименование 310 | 311 | Может показаться странным говорить о наименовании, но к сожалению это не так. 312 | Основываясь на моём опыте, я видел ужасные названия для DDD модулей и сам создавал 313 | даже ещё хуже. 314 | 315 | ``` 316 | project 317 | |--... 318 | |--pkg 319 | |--shoppingAndCustomer 320 | |--... 321 | |--utils 322 | |--... 323 | |--events 324 | |--... 325 | |--strategy 326 | |--... 327 | |--... 328 | |--... 329 | ``` 330 | 331 | В приведенном выше примере много плохих наименований. Я всегда избегаю использования 332 | слова "and" в названиях модуля, например, как здесь, `shoppingAndCustomer`. 333 | Если невозможно не использовать слова "and", я имею дело с двумя отдельными 334 | Модулями. 335 | 336 | Слово "utils" - худшее название при разработке программного обеспечения. Я не могу 337 | его терпеть ни в названии структуры, ни в названии файла, ни в названии функции, 338 | ни в названии пакета, ни в названии модуля. Имя "Сборщик мусора" вероятно подходит 339 | наилучшим образом, поскольку лучше всего описывает весь код, хранящийся в 340 | модуле `utils`. 341 | 342 | Также бесполезно иметь модуль, который бы содержал мелкие детали со всего 343 | приложения. Таким примером может быть Модуль `events` - он содержит События 344 | предметной области всего приложения. 345 | 346 | Также не рекомендуется называть Модуль как какой-то шаблон проектирования, 347 | например, Модуль `strategy`. Вероятно нам следует использовать шаблон Стратегия 348 | во многих местах нашего приложения, поэтому нет смысла создавать несколько 349 | Модулей `strategy`. 350 | 351 | Наши модули должны заимствовать названия из реального мира. Название должно 352 | быть частью [Единого Языка](https://martinfowler.com/bliki/UbiquitousLanguage.html), 353 | каким-то термином, который относится к реальному миру и разработке программного 354 | обеспечения, описывая одно и то же. Это должно быть уникальное имя для данного 355 | кластера бизнес-логики. 356 | 357 | ## Внедрение зависимостей 358 | 359 | Возможно вы заметили, что первая структура содержала отдельные Go файлы в корне 360 | каждого DDD Модуля. Я всегда называю их `module.go` или по названию Модуля. 361 | 362 | В этих файлах я определяю зависимости в моём Модуле и различных Адаптерах для 363 | своих Портов, если они есть. Во многих случаях я пишу простые Go контейнеры, 364 | в которых хранятся объекты, используемые в приложении. 365 | 366 | ```go 367 | package access 368 | 369 | type AccessModule struct { 370 | repository access_repository.UserRepository 371 | service access_service.UserService 372 | } 373 | 374 | func NewAccessModule(useDatabase bool) *AccessModule { 375 | var repository access_repository.UserRepository 376 | if useDatabase { 377 | repository = &database.UserDBRepository{} 378 | } else { 379 | repository = &fake.UserFakeRepository{} 380 | } 381 | var service access_service.UserService 382 | // 383 | // какой-то код 384 | // 385 | return &AccessModule{ 386 | repository: repository, 387 | service: service, 388 | } 389 | } 390 | 391 | func (m *AccessModule) GetRepository() access_repository.UserRepository { 392 | return m.repository 393 | } 394 | 395 | func (m *AccessModule) GetService() access_service.UserService { 396 | return m.service 397 | } 398 | ``` 399 | *Простой Модуль с зависимостями* 400 | 401 | В вышеприведенном примере я создал структуру `AccessModule`. Во время 402 | инициализации она принимает на вход конфигурацию, которая определяет должен ли 403 | он использовать `UserRepository`, работающий с базой данных или с какой-то 404 | её имитацией. Позже все другие модули могут использовать этот контейнер для 405 | получения своих зависимостей. 406 | 407 | Мы также можем реализовать внедрение зависимостей в Go, используя один из множества 408 | имеющихся у нас фреймворков. Одна из наиболее часто используемых — это библиотека 409 | [Wire](https://github.com/google/wire), но я сам предпочитаю [Dingo](https://github.com/i-love-flamingo/dingo). 410 | 411 | Библиотека Dingo использует рефлексии, что является болезненной темой для многих 412 | Go разработчиков. Хотя [я не поклонник рефлексий в Go](https://medium.com/codex/what-is-the-right-amount-of-reflection-in-golang-2c35d4fca68b), 413 | по моему опыту, Dingo оказалась простым и стабильным решением, предоставляющим 414 | множество различных функций. 415 | 416 | ```go 417 | package example 418 | 419 | import "flamingo.me/dingo" 420 | 421 | type BillingModule struct {} 422 | 423 | func (module *BillingModule) Configure(injector *dingo.Injector) { 424 | // Эта команда сообщает Dingo, что всякий раз, когда она видит зависимость от TransactionLog 425 | // она должна удовлетворить её, используя DatabaseTransactionLog. 426 | injector.Bind(new(TransactionLog)).To(DatabaseTransactionLog{}) 427 | 428 | // По аналогии такая запись сообщает Dingo, что когда используется CreditCardProcessor в зависимости, 429 | // она должна быть удовлетворена с помощью PaypalCreditCardProcessor. 430 | injector.Bind(new(CreditCardProcessor)).To(PaypalCreditCardProcessor{}) 431 | } 432 | ``` 433 | 434 | ## Заключение 435 | 436 | DDD модуль — это логический кластер для нашего кода. Он объединяет множество 437 | структур в связанную группу, которая разделяет некие бизнес-правила. Внутри 438 | модуля мы можем создавать разные уровни. 439 | 440 | И уровни, и модули должны поддерживать однонаправленную связь, чтобы избежать 441 | циклических зависимостей. Модули должны иметь имени, представляющие терминологию 442 | из реального мира. 443 | 444 | > Другие статьи из DDD цикла: 445 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 446 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 447 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 448 | > 4. [Practical DDD in Golang: Domain Event](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 449 | 450 | 451 | ## Полезные ссылки на источники: 452 | 453 | * [https://martinfowler.com/](https://martinfowler.com/) 454 | * [https://www.domainlanguage.com/](https://www.domainlanguage.com/) -------------------------------------------------------------------------------- /docs/repository-pattern-for-gorm.md: -------------------------------------------------------------------------------- 1 | # Преимущества дженериков (Generics) в Go: шаблон Репозиторий для GORM 2 | 3 | Можем ли мы ожидать, появление в Go полноценной структуры ORM и DBAL, такой 4 | как Doctrine? 5 | 6 | ![intro](images/repository-pattern-for-gorm/intro.jpeg) 7 | *Фото [Rajan Alwan](https://unsplash.com/@rajanalwan) из [Unsplash](https://unsplash.com/)* 8 | 9 | После месяцев и лет дискуссий, реализаций и проверки работоспособности мы 10 | наконец достигли точки, когда произошла [революция](https://levelup.gitconnected.com/generics-in-go-viva-la-revolution-e27898bf5495) 11 | в выбранном нами языке программирования. Вышла новая версия Golang [1.18](https://go.dev/blog/go1.18). 12 | 13 | > Мы уже знали, что это будет радикальным изменением в кодовой базе Go, 14 | > ещё до окончательного релиза Дженериков (Generics). 15 | 16 | В течение многих лет мы использовали генераторы кода в Go всякий раз, когда 17 | хотели обеспечить некоторую универсальность и абстракцию. Познание 18 | концепции *"The Golang Way"* для многих из нас не было легким, но несмотря 19 | на это привело к большим успехам. Это стоило того. 20 | 21 | Теперь за столом появились новые игроки. Возникло много новых [пакетов](https://cs.opensource.google/go/x/exp/+/master:slices/slices.go), 22 | давшие нам некоторое представление о том, как мы можем обогатить 23 | экосистему Go повторно используемым кодом, который облегчил бы жизнь всем нам. 24 | И что-то подобное послужило моим вдохновением, которое привело меня к 25 | небольшой **проверки работоспособности** концепции на основе [библиотеки GORM](https://gorm.io/index.html). 26 | А теперь давайте посмотрим на неё. 27 | 28 | ## Исходный код 29 | 30 | *Когда я писал эту статью она основывалась на [Git репозитории на GitHub](https://github.com/Ompluscator/gorm-generics). Код 31 | (библиотека на Go) был проверки работоспособности концепции и я намеревался 32 | работать над ним дальше. Тем не менее он не был готов к использованию в 33 | продакшене (и я точно не планировал оказывать какую-либо продакшен 34 | поддержку на тот момент).* 35 | 36 | Вы можете увидеть текущий функционал по [ссылке](https://github.com/Ompluscator/gorm-generics#example), 37 | простейший пример использования показан во фрагменте кода ниже: 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "context" 44 | "fmt" 45 | 46 | "gorm.io/gorm" 47 | 48 | "github.com/ompluscator/gorm-generics" 49 | // какие-то импорты 50 | ) 51 | 52 | // Product - сущность предметной области 53 | type Product struct { 54 | // какие-то поля 55 | } 56 | 57 | // ProductGorm - это DTO для сопоставления сущности Product с базой данных 58 | type ProductGorm struct { 59 | // какие-то поля 60 | } 61 | 62 | // ToEntity соответствует интерфейсу gorm_generics.GormModel 63 | func (g ProductGorm) ToEntity() Product { 64 | return Product{ 65 | // какие-то поля 66 | } 67 | } 68 | 69 | // FromEntity соответствует интерфейсу gorm_generics.GormModel 70 | func (g ProductGorm) FromEntity(product Product) interface{} { 71 | return ProductGorm{ 72 | // какие-то поля 73 | } 74 | } 75 | 76 | func main() { 77 | db, err := gorm.Open( /* строка подключения к БД */ ) 78 | // обработка ошибки 79 | 80 | err = db.AutoMigrate(ProductGorm{}) 81 | // обработка ошибки 82 | 83 | // инициализируем новый репозиторий, передавая 84 | // GORM модель и сущность как тип 85 | repository := gorm_generics.NewRepository[ProductGorm, Product](db) 86 | 87 | ctx := context.Background() 88 | 89 | // создаём новую сущность 90 | product := Product{ 91 | // какие-то поля 92 | } 93 | 94 | // посылаем новую сущность в репозиторий для сохранения 95 | err = repository.Insert(ctx, &product) 96 | // обработка ошибки 97 | 98 | fmt.Println(product) 99 | // Выводит: 100 | // {1 product1 100 true} 101 | 102 | single, err := repository.FindByID(ctx, product.ID) 103 | // обработка ошибки 104 | 105 | fmt.Println(single) 106 | // Выводит: 107 | // {1 product1 100 true} 108 | } 109 | ``` 110 | Фрагмент кода с практическим примером из библиотеки, проверяющей 111 | работоспособность концепции 112 | 113 | ## Почему я выбрал ORM в качестве проверки работоспособности концепции 114 | 115 | Как разработчик программного обеспечения с опытом, работавший со 116 | старомодными объектно-ориентированными языками программирования, такими как 117 | Java, C# и PHP, одним из первых моих запросов в Google был какой-нибудь 118 | подходящий ORM для Golang. Я был глуп и наивен, но ожидал, что найду её. 119 | 120 | Дело не в том, что я не могу жить без ORM. Мне не особо нравится, как в коде 121 | выглядят чистые MySQL запросы. Вся эта конкатенация строк на мой взгляд 122 | выглядит уродливо. 123 | 124 | С другой стороны, мне всегда нравится сразу же приступать к написанию 125 | бизнес-логики, и я почти не трачу время на размышления об используемом 126 | хранилище. Иногда во время внедрения я меняю своё мнение и перехожу к 127 | другому типу хранилища. И именно в этом случае ORM облегчают переход. 128 | 129 | Короче говоря, ORM позволяют: 130 | 131 | 1. сделать код чище 132 | 2. добавляют гибкости при выборе типы используемого хранилища 133 | 3. позволяют сфокусироваться на бизнес-логике, а не на технических деталях 134 | 135 | В Golang существуют различные [решения](https://github.com/d-tsuji/awesome-go-orms) 136 | для ORM, и я использовал большинство из них. Неудивительно, что GORM — это 137 | то, которое я использовал чаще всего, поскольку оно покрывает большую часть 138 | необходимого функционала. Да, в нем отсутствуют некоторые известные шаблоны, 139 | такие как [Identity Map](https://www.martinfowler.com/eaaCatalog/identityMap.html), 140 | [Unit of Work](https://martinfowler.com/eaaCatalog/unitOfWork.html) и 141 | [Lazy Load](https://www.martinfowler.com/eaaCatalog/lazyLoad.html), но я 142 | мог бы жить и без них. 143 | 144 | Но мне не хватало шаблона [Репозиторий](https://martinfowler.com/eaaCatalog/repository.html), 145 | поскольку время от времени я сталкивался с дублированием похожих или 146 | идентичных блоков кода (и я ненавижу повторяться). 147 | 148 | [DDD на практике в Golang: Репозиторий](repository.md) 149 | 150 | Для этой цели я иногда использовал библиотеку [GNORM](https://gnorm.org/), 151 | шаблонизатор которой позволял мне создавать структуры Репозиториев. Хотя мне 152 | нравилась идея, которую предоставляет GNORM (хороший пример Golang Way!), 153 | постоянные обновления шаблонов для добавления нового функционала в 154 | репозиторий - это не очень хорошо. 155 | 156 | Я попытался создать свою реализацию, которая основывается на рефлексии и 157 | поделиться ей с сообществом открытого исходного кода. Это было моей большой 158 | ошибкой. Она работала, но поддерживать библиотеку было очень сложно и 159 | производительность была далека от хорошей. В конце концов я удалил Git 160 | репозиторий с GitHub. 161 | 162 | И в тот самый момент, когда я уже отчаялся реализовать этот ORM апгрейд 163 | в Go, появились дженерики. *О, Боже. О, Боже!* Я сражу же вернулся к 164 | написанию кода. 165 | 166 | ## Реализация 167 | 168 | У меня есть опыт в предметно-ориентированном проектировании. Это значит, что 169 | мне нравится отделять уровень предметной области от инфраструктурного. Некоторые 170 | ORM рассматривают шаблон Entity больше как [Шлюз записи данных](https://www.martinfowler.com/eaaCatalog/rowDataGateway.html) или 171 | [Active Record](https://www.martinfowler.com/eaaCatalog/activeRecord.html). Но поскольку как следует из названия он ссылается на 172 | шаблон Entity из DDD, происходит потеря при переходе и мы пытаемся хранить 173 | бизнес-логику и технические детали в одном классе. И создаём монстра. 174 | 175 | [DDD на практике в Golang: Сущности](entity.md) 176 | 177 | > *Шаблон Entity не имеет ничего общего с сопоставлением схемы таблиц 178 | > базы данных. Это никак не связано с используемым хранилищем.* 179 | 180 | Итак, я всегда использую Entity на уровне предметной области и [Data 181 | Transfer Object](https://www.martinfowler.com/eaaCatalog/dataTransferObject.html) 182 | в инфраструктурном. Сигнатуры моих Репозиториев всегда поддерживают только 183 | Entity, но внутри они используют DTO для сопоставления данных с БД и из неё, 184 | а также для извлечения и сохранения их в Entity. Это нужно, чтобы 185 | гарантировать нам работающий [предохранительный уровень](https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer). 186 | 187 | В этом случае я могу выделить три интерфейса и структуры (как видите на 188 | диаграмме ниже): 189 | 190 | 1. `Entity` хранит бизнес-логики на уровне предметной области. 191 | 2. `GormModel` как DTO используется для отображения данных из Entity в базу 192 | данных 193 | 3. `GormRepository` - оркестратор для запросов и сохранения данных 194 | 195 | ![](images/repository-pattern-for-gorm/uml.png) 196 | UML диаграмма представляющая обобщенный краткий обзор реализации 197 | 198 | Две основные составляющие, `GormModel` и `GormRepository`, предполагают, 199 | что обобщенные типы определяют сигнатуру их методов. Использование дженериков 200 | позволяет нам определить `GormRepository` как структуру и обобщить реализацию: 201 | 202 | ```go 203 | func (r *GormRepository[M, E]) Insert(ctx context.Context, entity *E) error { 204 | // отображаем данные из Entity в DTO 205 | var start M 206 | model := start.FromEntity(*entity).(M) 207 | 208 | // создаём новую запись в базе данных 209 | err := r.db.WithContext(ctx).Create(&model).Error 210 | // обработка ошибки 211 | 212 | // отображаем новую запись из базы в Entity 213 | *entity = model.ToEntity() 214 | return nil 215 | } 216 | 217 | func (r *GormRepository[M, E]) FindByID(ctx context.Context, id uint) (E, error) { 218 | // извлекаем запись по id из базы данных 219 | var model M 220 | err := r.db.WithContext(ctx).First(&model, id).Error 221 | // обработка ошибки 222 | 223 | // отображаем запись в Entity 224 | return model.ToEntity(), nil 225 | } 226 | 227 | func (r *GormRepository[M, E]) Find(ctx context.Context, specification Specification) ([]E, error) { 228 | // получаем записи по некоторому критерию 229 | var models []M 230 | err := r.db.WithContext(ctx).Where(specification.GetQuery(), specification.GetValues()...).Find(&models).Error 231 | // обработка ошибки 232 | 233 | // отображаем все записи в Entities 234 | result := make([]E, 0, len(models)) 235 | for _, row := range models { 236 | result = append(result, row.ToEntity()) 237 | } 238 | 239 | return result, nil 240 | } 241 | ``` 242 | Реализация GormRepository 243 | 244 | Я не планировал добавлять более-менее сложные фичи вроде [предзагрузки](https://gorm.io/docs/preload.html), 245 | [джойнов](https://gorm.io/docs/query.html#Joins) и даже [LIMIT и OFFSET](https://gorm.io/docs/query.html#Limit-amp-Offset) 246 | для этого проверки работоспособности концепции. Идея заключалась в том, чтобы 247 | проверить простоту реализации дженериков в Go с помощью библиотеки GORM. 248 | 249 | В этом фрагменте кода мы видим, что структура GormRepository поддерживает 250 | вставку новых записей, а также поиск по идентификатору или запрос с 251 | использованием спецификации. Шаблон «Спецификация» — это еще один шаблон 252 | предметно-ориентированного проектирования, который мы можем использовать для 253 | многих целей, в том числе и запроса данных из хранилища. 254 | 255 | [DDD на практике в Golang: Спецификация](specification.md) 256 | 257 | Проверка работоспособности концепции, представленная здесь, определяет 258 | интерфейс Specification, который обеспечивает реализацию условия `WHERE` и 259 | используемых внутри него значений. Это требует определенного использования 260 | дженериков для операторов сравнения и это возможный предшественник для 261 | будущего [объекта запроса](https://martinfowler.com/eaaCatalog/queryObject.html): 262 | 263 | ```go 264 | type Specification interface { 265 | GetQuery() string 266 | GetValues() []any 267 | } 268 | 269 | // joinSpecification - это действующая реализация интерфейса Specification 270 | // Она используется для операторов AND и OR 271 | type joinSpecification struct { 272 | specifications []Specification 273 | separator string 274 | } 275 | 276 | // GetQuery объединяет все подзапросы 277 | func (s joinSpecification) GetQuery() string { 278 | queries := make([]string, 0, len(s.specifications)) 279 | 280 | for _, spec := range s.specifications { 281 | queries = append(queries, spec.GetQuery()) 282 | } 283 | 284 | return strings.Join(queries, fmt.Sprintf(" %s ", s.separator)) 285 | } 286 | 287 | // GetValues объединяет все подзначения 288 | func (s joinSpecification) GetValues() []any { 289 | values := make([]any, 0) 290 | 291 | for _, spec := range s.specifications { 292 | values = append(values, spec.GetValues()...) 293 | } 294 | 295 | return values 296 | } 297 | 298 | // And передаёт AND оператор в виде Specification 299 | func And(specifications ...Specification) Specification { 300 | return joinSpecification{ 301 | specifications: specifications, 302 | separator: "AND", 303 | } 304 | } 305 | 306 | // notSpecification отрицает под Specification 307 | type notSpecification struct { 308 | Specification 309 | } 310 | 311 | // GetQuery отрицает подзапрос 312 | func (s notSpecification) GetQuery() string { 313 | return fmt.Sprintf(" NOT (%s)", s.Specification.GetQuery()) 314 | } 315 | 316 | // Not передаёт NOT оператор в виде Specification 317 | func Not(specification Specification) Specification { 318 | return notSpecification{ 319 | specification, 320 | } 321 | } 322 | 323 | // binaryOperatorSpecification определяет бинарный оператор как Specification 324 | // Он используется для операторов =, >, <, >=, <=. 325 | type binaryOperatorSpecification[T any] struct { 326 | field string 327 | operator string 328 | value T 329 | } 330 | 331 | // GetQuery создаёт запрос для бинарного оператора 332 | func (s binaryOperatorSpecification[T]) GetQuery() string { 333 | return fmt.Sprintf("%s %s ?", s.field, s.operator) 334 | } 335 | 336 | // GetValues возвращает значение для бинарного оператора 337 | func (s binaryOperatorSpecification[T]) GetValues() []any { 338 | return []any{s.value} 339 | } 340 | 341 | // Equal передаёт оператор равенства в виде Specification 342 | func Equal[T any](field string, value T) Specification { 343 | return binaryOperatorSpecification[T]{ 344 | field: field, 345 | operator: "=", 346 | value: value, 347 | } 348 | } 349 | ``` 350 | Пример реализации Спецификации 351 | 352 | Часть пакета «Спецификация» позволяет указать в Репозитории свой собственный, 353 | пользовательский критерий и получить данные, соответствующие ему. Его 354 | использование позволяет комбинировать критерии, отрицать их и в дальнейшем 355 | расширять. 356 | 357 | ## Результат 358 | 359 | Эта реализация, наконец, воплощает в жизнь основную цель этой проверки 360 | работоспособности концепции, которая заключается в предоставлении 361 | обобщенного интерфейса для запроса записей из базы данных: 362 | 363 | ```go 364 | err := repository.Insert(ctx, &Product{ 365 | Name: "product2", 366 | Weight: 50, 367 | IsAvailable: true, 368 | }) 369 | // обработка ошибки 370 | 371 | err = repository.Insert(ctx, &Product{ 372 | Name: "product3", 373 | Weight: 250, 374 | IsAvailable: false, 375 | }) 376 | // обработка ошибки 377 | 378 | many, err := repository.Find(ctx, gorm_generics.And( 379 | gorm_generics.GreaterOrEqual("weight", 90), 380 | gorm_generics.Equal("is_available", true)), 381 | ) 382 | // обработка ошибки 383 | 384 | fmt.Println(many) 385 | // Выводит: 386 | // [{1 product1 100 true}] 387 | ``` 388 | Основная цель проверки работоспособности концепции 389 | 390 | Что касается моих стремлений, приведенный выше фрагмент кода позволяет 391 | быстро и элегантно извлекать данные в чистом и удобочитаемом виде. 392 | И не влияя (сильно) на производительность. 393 | 394 | ## Вывод 395 | 396 | Первое знакомство с дженериками после официального релиза Go 1.18 стало 397 | глотком свежего воздуха. В последнее время мне не хватало новых вызовов и эта 398 | возможность для новых идей даже больше, чем мне было необходимо. 399 | 400 | Кроме того, мне нужно было продолжать вести блог после долгого перерыва. 401 | Мне так приятно снова публично выражать свое мнение, и я с нетерпением жду 402 | от вас обратную связь, которые вы, ребята, можете дать. -------------------------------------------------------------------------------- /docs/specification.md: -------------------------------------------------------------------------------- 1 | # DDD на практике в Golang: Спецификация 2 | 3 | ![intro](images/specification/intro.jpeg) 4 | *Фото [Esteban Castle](https://unsplash.com/@estebancastle) из [Unsplash](https://unsplash.com/)* 5 | 6 | Существует не так много алгоритмических структур, которые я реализую с 7 | удовольствием. Первой такой стало упрощенное ORM в Go, когда у нас его не было. 8 | 9 | С другой стороны я много лет использовал ORM. В какой-то момент, когда вы 10 | зависите от ORM, возникает неизбежная необходимость применения 11 | [QueryBuilder](https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/query-builder.html). 12 | Вот где используется шаблон Спецификация (`Specification`). 13 | 14 | Сложно найти какой-либо шаблон, который мы использовали бы так же часто, как и 15 | Спецификацию, но не произнося его название. Я думаю, что сложнее только написать 16 | приложение без использования этого шаблона. 17 | 18 | Шаблон Спецификация имеет множество различных применений. Мы можем использовать 19 | его для запросов, создания или валидации. Мы можем написать универсальный код, 20 | выполняющий всю эту работу, или написать свою реализацию для каждого случая. 21 | 22 | > Другие статьи из DDD цикла: 23 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 24 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 25 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 26 | > 4. [DDD на практике в Golang: Событие предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 27 | > 5. [DDD на практике в Golang: Модуль](https://levelup.gitconnected.com/practical-ddd-in-golang-module-51edf4c319ec) 28 | > 6. [DDD на практике в Golang: Агрегат](https://levelup.gitconnected.com/practical-ddd-in-golang-aggregate-de13f561e629) 29 | > 7. [DDD на практике в Golang: Фабрика](https://levelup.gitconnected.com/practical-ddd-in-golang-factory-5ba135df6362) 30 | > 8. [DDD на практике в Golang: Репозиторий](https://levelup.gitconnected.com/practical-ddd-in-golang-repository-d308c9d79ba7) 31 | 32 | ## Для валидации 33 | 34 | Первый вариант использования шаблона Спецификация — это валидация. В первую 35 | очередь мы проверяем данные в формах, но это происходит на уровне представления. 36 | Иногда мы выполняем её во время создания, например, для Объектов-значений. 37 | 38 | На уровне предметной области мы можем использовать Спецификации для проверки 39 | состояний Сущности и фильтрации их из коллекции. Итак, валидация на уровне 40 | предметной области уже имеет более широкое применение, чем только проверка 41 | пользовательского ввода. 42 | 43 | ```go 44 | type MaterialType = string 45 | 46 | const Plastic = "plastic" 47 | 48 | type Product struct { 49 | ID uuid.UUID 50 | Material MaterialType 51 | IsDeliverable bool 52 | Quantity int 53 | } 54 | 55 | type ProductSpecification interface { 56 | IsValid(product Product) bool 57 | } 58 | 59 | type AndSpecification struct { 60 | specifications []ProductSpecification 61 | } 62 | 63 | func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification { 64 | return AndSpecification{ 65 | specifications: specifications, 66 | } 67 | } 68 | 69 | func (s AndSpecification) IsValid(product Product) bool { 70 | for _, specification := range s.specifications { 71 | if !specification.IsValid(product) { 72 | return false 73 | } 74 | } 75 | 76 | return true 77 | } 78 | 79 | type HasAtLeast struct { 80 | pieces int 81 | } 82 | 83 | func NewHasAtLeast(pieces int) ProductSpecification { 84 | return HasAtLeast{ 85 | pieces: pieces, 86 | } 87 | } 88 | 89 | func (h HasAtLeast) IsValid(product Product) bool { 90 | return product.Quantity >= h.pieces 91 | } 92 | 93 | func IsPlastic(product Product) bool { 94 | return product.Material == Plastic 95 | } 96 | 97 | func IsDeliverable(product Product) bool { 98 | return product.IsDeliverable 99 | } 100 | 101 | type FunctionSpecification func(product Product) bool 102 | 103 | func (fs FunctionSpecification) IsValid(product Product) bool { 104 | return fs(product) 105 | } 106 | 107 | func main() { 108 | spec := model.NewAndSpecification( 109 | model.NewHasAtLeast(10), 110 | model.FunctionSpecification(model.IsPlastic), 111 | model.FunctionSpecification(model.IsDeliverable), 112 | ) 113 | 114 | fmt.Println(spec.IsValid(model.Product{})) 115 | // выводит: false 116 | 117 | fmt.Println(spec.IsValid(model.Product{ 118 | Material: model.Plastic, 119 | IsDeliverable: true, 120 | Quantity: 50, 121 | })) 122 | // выводит: true 123 | } 124 | ``` 125 | *Использование Спецификации для валидации данных* 126 | 127 | В вышеприведенном примере задан интерфейс `ProductSpecification`. Он определяет 128 | только один метод IsValid, который ожидает экземпляры `Product` и в результате 129 | возвращает логическое значение, если `Product` соответствует правилам проверки. 130 | 131 | Простая реализация этого интерфейса - `HasAtLeast`, который проверяет минимальное 132 | количество продукта. Более интересными валидаторами являются две функции: 133 | `IsPlastic` и `IsDeliverable`. 134 | 135 | Мы можем обернуть эти функции особым типом `FunctionSpecification`. Этот тип 136 | использует функцию с такой же сигнатурой как у двух упомянутых выше. Кроме того, 137 | он предоставляет методы, соответствующие интерфейсу `ProductSpecification`. 138 | 139 | Этот пример показывает особенность Go, где мы можем определить функцию как 140 | тип и добавить к нему метод, чтобы он мог неявно реализовать некоторый 141 | интерфейс. В нашем случае создаётся метод `IsValid`, который выполняет 142 | встроенную функцию. 143 | 144 | Кроме того, существует также одна не похожая на другие спецификация 145 | `AndSpecification`. Такая структура позволяет нам использовать объект, который 146 | реализует интерфейс `ProductSpecification` и объединяет все входящие в неё 147 | Спецификации, используя логическое "И". 148 | 149 | ```go 150 | type OrSpecification struct { 151 | specifications []ProductSpecification 152 | } 153 | 154 | func NewOrSpecification(specifications ...ProductSpecification) ProductSpecification { 155 | return OrSpecification{ 156 | specifications: specifications, 157 | } 158 | } 159 | 160 | func (s OrSpecification) IsValid(product Product) bool { 161 | for _, specification := range s.specifications { 162 | if specification.IsValid(product) { 163 | return true 164 | } 165 | } 166 | 167 | return false 168 | } 169 | 170 | type NotSpecification struct { 171 | specification ProductSpecification 172 | } 173 | 174 | func NewNotSpecification(specification ProductSpecification) ProductSpecification { 175 | return NotSpecification{ 176 | specification: specification, 177 | } 178 | } 179 | 180 | func (s NotSpecification) IsValid(product Product) bool { 181 | return !s.specification.IsValid(product) 182 | } 183 | ``` 184 | *Дополнительные Спецификации* 185 | 186 | В вышеприведенном фрагменте кода описаны две дополнительные Спецификации. 187 | Одна из них `OrSpecification`. Она, как и `AndSpecification`, объединяет все 188 | входящие в неё Спецификации. Просто в данном случае используется логическое 189 | "ИЛИ" вместо "И". 190 | 191 | Последняя - `NotSpecification`, логически инвертирует результат переданной 192 | Спецификации. `NotSpecification` можно было бы задать с помощью 193 | `FunctionSpecification`, но я не хотел её слишком усложнять. 194 | 195 | ## Для запросов 196 | 197 | Я уже упоминал в этой статье о применении шаблона Спецификация как части ORM. 198 | Во многих случаях вам не нужно будет реализовывать Спецификации для этого 199 | варианта использования, по крайней мере, если вы применяете какую-либо ORM. 200 | 201 | Отличную реализацию Спецификации в виде предикатов я нашёл в библиотеке [Ent](https://entgo.io/) 202 | от Facebook. С того момента я не писал спецификации для запросов. 203 | 204 | Тем не менее, когда вы обнаружите, что ваш запрос для Репозитория на уровне 205 | предметной области может быть слишком сложным, вам понадобятся дополнительные 206 | способы фильтрации желаемых объектов. Реализация может выглядеть как показано в 207 | примере ниже. 208 | 209 | ```go 210 | type ProductSpecification interface { 211 | Query() string 212 | Value() []interface{} 213 | } 214 | 215 | type AndSpecification struct { 216 | specifications []ProductSpecification 217 | } 218 | 219 | func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification { 220 | return AndSpecification{ 221 | specifications: specifications, 222 | } 223 | } 224 | 225 | func (s AndSpecification) Query() string { 226 | var queries []string 227 | for _, specification := range s.specifications { 228 | queries = append(queries, specification.Query()) 229 | } 230 | 231 | query := strings.Join(queries, " AND ") 232 | 233 | return fmt.Sprintf("(%s)", query) 234 | } 235 | 236 | func (s AndSpecification) Value() []interface{} { 237 | var values []interface{} 238 | for _, specification := range s.specifications { 239 | values = append(values, specification.Value()...) 240 | } 241 | return values 242 | } 243 | 244 | type OrSpecification struct { 245 | specifications []ProductSpecification 246 | } 247 | 248 | func NewOrSpecification(specifications ...ProductSpecification) ProductSpecification { 249 | return OrSpecification{ 250 | specifications: specifications, 251 | } 252 | } 253 | 254 | func (s OrSpecification) Query() string { 255 | var queries []string 256 | for _, specification := range s.specifications { 257 | queries = append(queries, specification.Query()) 258 | } 259 | 260 | query := strings.Join(queries, " OR ") 261 | 262 | return fmt.Sprintf("(%s)", query) 263 | } 264 | 265 | func (s OrSpecification) Value() []interface{} { 266 | var values []interface{} 267 | for _, specification := range s.specifications { 268 | values = append(values, specification.Value()...) 269 | } 270 | return values 271 | } 272 | 273 | type HasAtLeast struct { 274 | pieces int 275 | } 276 | 277 | func NewHasAtLeast(pieces int) ProductSpecification { 278 | return HasAtLeast{ 279 | pieces: pieces, 280 | } 281 | } 282 | 283 | func (h HasAtLeast) Query() string { 284 | return "quantity >= ?" 285 | } 286 | 287 | func (h HasAtLeast) Value() []interface{} { 288 | return []interface{}{h.pieces} 289 | } 290 | 291 | func IsPlastic() string { 292 | return "material = 'plastic'" 293 | } 294 | 295 | func IsDeliverable() string { 296 | return "deliverable = 1" 297 | } 298 | 299 | type FunctionSpecification func() string 300 | 301 | func (fs FunctionSpecification) Query() string { 302 | return fs() 303 | } 304 | 305 | func (fs FunctionSpecification) Value() []interface{} { 306 | return nil 307 | } 308 | 309 | func main() { 310 | 311 | spec := infrastructure.NewOrSpecification( 312 | infrastructure.NewAndSpecification( 313 | infrastructure.NewHasAtLeast(10), 314 | infrastructure.FunctionSpecification(infrastructure.IsPlastic), 315 | infrastructure.FunctionSpecification(infrastructure.IsDeliverable), 316 | ), 317 | infrastructure.NewAndSpecification( 318 | infrastructure.NewHasAtLeast(100), 319 | infrastructure.FunctionSpecification(infrastructure.IsPlastic), 320 | ), 321 | ) 322 | 323 | fmt.Println(spec.Query()) 324 | // выводит: ((quantity >= ? AND material = 'plastic' AND deliverable = 1) OR (quantity >= ? AND material = 'plastic')) 325 | 326 | fmt.Println(spec.Value()) 327 | // выводит: [10 100] 328 | } 329 | ``` 330 | *Пример выполнения запроса* 331 | 332 | В новой реализации интерфейс `ProductSpecification` предоставляет два метода: 333 | `Query` и `Values`. Мы используем из для получения строки запроса для конкретной 334 | Спецификации и значений, которые она может содержать. 335 | 336 | Опять же мы видим дополнительные спецификации, `AndSpecification` и 337 | `OrSpecification`. В этом случае они объединяют все входящие запросы в 338 | зависимости от оператора ("AND" или "OR") и все значения. 339 | 340 | Наличие такой Спецификации на уровне домена вызывает вопросы. Как видите из 341 | выходных данных Спецификации предоставляют синтаксис, схожий с SQL, в котором 342 | слишком много технических деталей. 343 | 344 | В этом случае решением, вероятно, будет определить интерфейсы на уровне 345 | предметной области и фактические реализации на инфраструктурном уровне. 346 | 347 | Или модифицировать код так, чтобы спецификация содержала информацию об имени поля, 348 | операции и значении. Затем мы создадим некий сопоставитель на инфраструктурном 349 | уровне, который сможет преобразовать такую Спецификацию в SQL запрос. 350 | 351 | ## Для создания 352 | 353 | Один из простейших вариантов использования Спецификации — создание сложного 354 | объекта, значения которого каждый раз сильно отличаются. В таких случаях мы 355 | можем комбинировать его с шаблоном Фабрика (`Factory`) или использовать внутри Сервиса 356 | предметной области (`Domain Service`). 357 | 358 | ```go 359 | type ProductSpecification interface { 360 | Create(product model.Product) model.Product 361 | } 362 | 363 | type AndSpecification struct { 364 | specifications []ProductSpecification 365 | } 366 | 367 | func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification { 368 | return AndSpecification{ 369 | specifications: specifications, 370 | } 371 | } 372 | 373 | func (s AndSpecification) Create(product model.Product) model.Product { 374 | for _, specification := range s.specifications { 375 | product = specification.Create(product) 376 | } 377 | return product 378 | } 379 | 380 | type HasAtLeast struct { 381 | pieces int 382 | } 383 | 384 | func NewHasAtLeast(pieces int) ProductSpecification { 385 | return HasAtLeast{ 386 | pieces: pieces, 387 | } 388 | } 389 | 390 | func (h HasAtLeast) Create(product model.Product) model.Product { 391 | product.Quantity = h.pieces 392 | return product 393 | } 394 | 395 | func IsPlastic(product model.Product) model.Product { 396 | product.Material = model.Plastic 397 | return product 398 | } 399 | 400 | func IsDeliverable(product model.Product) model.Product { 401 | product.IsDeliverable = true 402 | return product 403 | } 404 | 405 | type FunctionSpecification func(product model.Product) model.Product 406 | 407 | func (fs FunctionSpecification) Create(product model.Product) model.Product { 408 | return fs(product) 409 | } 410 | 411 | func main() { 412 | spec := create.NewAndSpecification( 413 | create.NewHasAtLeast(10), 414 | create.FunctionSpecification(create.IsPlastic), 415 | create.FunctionSpecification(create.IsDeliverable), 416 | ) 417 | 418 | fmt.Printf("%+v", spec.Create(model.Product{ 419 | ID: uuid.New(), 420 | })) 421 | // выводит: {ID:befaf2b9-73cd-44cf-95f1-5fba087e46d9 Material:plastic IsDeliverable:true Quantity:10} 422 | } 423 | ``` 424 | *Пример использования для создания объектов* 425 | 426 | В этом примере показан третий вариант использования Спецификации. В этом случае 427 | `ProductSpecification` поддерживает единственный метод, `Create`, который ожидает 428 | `Product`, модифицирует его и возвращает обратно. 429 | 430 | Опять, `AndSpecification` позволяет применить изменения, определённые в 431 | нескольких спецификациях, но здесь нет `OrSpecification`. Мне не удалось найти 432 | реальный вариант его использования или алгоритм, когда бы он понадобился, при 433 | создании объекта. 434 | 435 | Даже хотя здесь он не приводится мы можем создать `NotSpecification`, который 436 | будет работать с определенными типами данных, например, логическими. Тем не 437 | менее, для данного примера, я не придумал как его можно было бы использовать. 438 | 439 | ## Заключение 440 | 441 | Спецификация — это шаблон, который мы используем везде, во многих разных 442 | случаях. В настоящее время не просто обеспечить валидацию на уровне предметной 443 | области без использования спецификации. 444 | 445 | Спецификацию мы также можем использовать для запросов объектов из 446 | соответствующего хранилища. Сегодня они являются частью ORM. Третий вариант 447 | использования — создание сложных экземпляров, где мы можем комбинировать его 448 | с шаблоном Фабрика (`Factory`). 449 | 450 | > Другие статьи из DDD цикла: 451 | > 1. [DDD на практике в Golang: Объект-значение](https://levelup.gitconnected.com/practical-ddd-in-golang-value-object-4fc97bcad70) 452 | > 2. [DDD на практике в Golang: Сущности](https://levelup.gitconnected.com/practical-ddd-in-golang-entity-40d32bdad2a3) 453 | > 3. [DDD на практике в Golang: Сервисы предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-service-4418a1650274) 454 | > 4. [DDD на практике в Golang: Событие предметной области](https://levelup.gitconnected.com/practical-ddd-in-golang-domain-event-de02ad492989) 455 | > 5. [DDD на практике в Golang: Модуль](https://levelup.gitconnected.com/practical-ddd-in-golang-module-51edf4c319ec) 456 | > 6. [DDD на практике в Golang: Агрегат](https://levelup.gitconnected.com/practical-ddd-in-golang-aggregate-de13f561e629) 457 | > 7. [DDD на практике в Golang: Фабрика](https://levelup.gitconnected.com/practical-ddd-in-golang-factory-5ba135df6362) 458 | > 8. [DDD на практике в Golang: Репозиторий](https://levelup.gitconnected.com/practical-ddd-in-golang-repository-d308c9d79ba7) 459 | 460 | ## Полезные ссылки на источники: 461 | 462 | * [https://martinfowler.com/](https://martinfowler.com/) 463 | * [https://www.domainlanguage.com/](https://www.domainlanguage.com/) -------------------------------------------------------------------------------- /docs/value-object.md: -------------------------------------------------------------------------------- 1 | # DDD на практике в Golang: Объект-значение 2 | 3 | Давайте начнём наш обзор практического использования предметно-ориентированного 4 | проектирования в Golang с наиболее важного шаблона — Объекта-значения. 5 | 6 | ![intro](images/value-object/intro.jpeg) 7 | *Фото [Jason Leung](https://unsplash.com/@ninjason) из [Unsplash](https://unsplash.com/)* 8 | 9 | Утверждение о том, что какой-то шаблон является наиболее важным, может показаться 10 | преувеличенным, но я бы даже не стал спорить с ним. Впервые об [Объекте-значении](https://martinfowler.com/bliki/ValueObject.html) 11 | я узнал из ["Большой Красной Книги"](https://www.amazon.de/-/en/Martin-Fowler/dp/0321127420) 12 | [Мартина Фаулера](https://twitter.com/martinfowler) (Martin Fowler). На тот момент это выглядело довольно просто и 13 | не очень интересно. В следующий раз я прочитал об этом в ["Большой синей книге"](https://www.amazon.de/-/en/Eric-J-Evans/dp/0321125215) 14 | [Эрика Эванса](https://twitter.com/ericevans0?lang=en) (Eric Evans). С этого момента 15 | шаблон начал приобретать все больший и больший смысл, и вскоре я уже не мог 16 | представить как писать свой код, не используя практически везде 17 | Объекты-значения. 18 | 19 | ## Просто, но элегантно 20 | 21 | Объект-значение — на первый взгляд довольно простой шаблон. Он группирует несколько 22 | атрибутов как единое целое, добавляя к ним определённое поведение. Это единое целое 23 | представляет собой определенную качественную или количественную величину, 24 | которая существует в реальном мире, и его можно связать с другим более 25 | сложным объектом. Оно обладает определенным значением или характеристикой. Примером 26 | может быть цвет или деньги (подтип Объекта-значения), номер телефона или любой 27 | другой небольшой объект, представляющий собой какое-либо значение, как во 28 | фрагменте кода ниже. 29 | 30 | ```go 31 | type Currency struct { 32 | ID uuid.UUID 33 | Code string 34 | HTML int 35 | } 36 | 37 | type Money struct { 38 | Value float64 39 | Currency Currency 40 | } 41 | 42 | func (m Money) ToHTML() string { 43 | return fmt.Sprintf(`%.2f %d`, m.Value, m.Currency.HTML) 44 | } 45 | 46 | type Salutation string 47 | 48 | func (s Salutation) IsPerson() bool { 49 | return s != "company" 50 | } 51 | 52 | type Color struct { 53 | Red byte 54 | Green byte 55 | Blue byte 56 | } 57 | 58 | func (c Color) ToCSS() string { 59 | return fmt.Sprintf(`rgb(%d, %d, %d`, c.Red, c.Green, c.Blue) 60 | } 61 | 62 | type Address struct { 63 | Street string 64 | Number int 65 | Suffix string 66 | Postcode int 67 | } 68 | 69 | type Phone struct { 70 | CountryPrefix string 71 | AreaCode string 72 | Number string 73 | } 74 | ``` 75 | 76 | В Golang Объекты-значения могут быть представлены в виде создаваемых пользователем 77 | структур или путём расширения какого-либо примитивного типа. В обоих случаях идея 78 | состоит в обеспечении дополнительного поведения, уникального для этого отдельного 79 | значения или группы значений. Во многих случаях Объект-значение может предоставлять 80 | определенные методы для форматирования строк, описывающих как значения должны себя 81 | вести при JSON кодировании или декодировании. Тем не менее, основная цель этих методов 82 | должна заключаться в поддержке бизнес-инвариантов, связанных с этой характеристикой 83 | или качеством в реальной жизни. 84 | 85 | ## Идентификация и равенство 86 | 87 | Объект-значение не имеет никаких идентификационных данных и это его критическое 88 | отличие от шаблона [Сущность](https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences/) (`Entity`). 89 | Шаблон Сущность имеет идентификатор, определяющий его уникальность. Если две 90 | Сущности имеют одинаковый идентификатор, то мы можем говорить о них как об одном 91 | и том же объекте. У объекта-значения нет такого идентификатора. У него есть только 92 | несколько полей, которые позволяют лучше описать его значение. Чтобы проверить равны 93 | ли два Объекта-значения, нужно проверить на равенство все его поля, как во 94 | фрагменте кода, показанном ниже. 95 | 96 | ```go 97 | // проверяем на равенство Объекты-значения 98 | func (c Color) EqualTo(other Color) bool { 99 | return c.Red == other.Red && c.Green == other.Green && c.Blue == other.Blue 100 | } 101 | 102 | // проверяем на равенство Объекты-значения 103 | func (m Money) EqualTo(other Money) bool { 104 | return m.Value == other.Value && m.Currency.EqualTo(other.Currency) 105 | } 106 | 107 | // проверяем на равенство Сущности 108 | func (c Currency) EqualTo(other Currency) bool { 109 | return c.ID.String() == other.ID.String() 110 | } 111 | ``` 112 | 113 | В приведенном выше примере для структур `Money` и `Color` определены методы 114 | `EqualTo`, которые проверяют на равенство все их поля. С другой стороны, 115 | Currency проверяет на равенство идентификаторы, которым в этом примере является 116 | UUID. 117 | 118 | Как вы возможно заметили, Объект-значение также может ссылаться на некоторую 119 | Сущность, например, `Money` и `Currency` в этом примере. Он также может 120 | содержать другие Объекты-значения (например, структура `Coin` состоит из 121 | `Color` и `Money`) или задаваться в виде среза на коллекцию (`Colors`). 122 | 123 | ```go 124 | type Coin struct { 125 | Value Money 126 | Color Color 127 | } 128 | 129 | type Colors []Color 130 | ``` 131 | 132 | В одном [Ограниченном Контексте](https://martinfowler.com/bliki/BoundedContext.html) у нас 133 | могут быть десятки объектов-значений. Тем не менее, некоторые из них могут 134 | быть Сущностями внутри других Ограниченных Контекстов. Примером может быть 135 | `Currency`. В простом веб-сервисе, где мы хотим отображать определённые суммы 136 | денег, мы можем рассматривать `Currency` как Объект-значение, связанное с 137 | `Money`, которые мы не планируем изменять. С другой стороны в сервисе `Payment` 138 | мы хотим получать обновления в реальном времени с помощью некоторого API 139 | сервиса `Exchange`, где нам нужно будет использовать идентификаторы внутри 140 | модели предметной области. В этом случае мы будем использовать различные 141 | реализации `Currency` на разных сервисах. 142 | 143 | ```go 144 | // Объект-значение в веб-сервисе 145 | type Currency struct { 146 | Code string 147 | HTML int 148 | } 149 | 150 | // Сущность в сервисе Payment 151 | type Currency struct { 152 | ID uuid.UUID 153 | Code string 154 | HTML int 155 | } 156 | ``` 157 | 158 | Шаблон, который мы будем использовать, Объект-значение или Сущность, зависит от 159 | только от того, что этот объект из себя представляет в Ограниченном Контексте. 160 | Если это многократно используемый объект, независимо хранящийся в базе данных, 161 | может изменяться и задействован во многих других объектах или связан с некоторой 162 | внешней Сущностью и его необходимо изменять при изменении внешней Сущности, то 163 | мы говорим о Сущности. Но если объект описывает какое-то значение, принадлежит 164 | определенной Сущности, является простой копией, получаемой из внешнего сервиса, 165 | или не должен существовать независимо в базе данных, тогда это Объект-значение. 166 | 167 | ## Явное описание 168 | 169 | Самая полезная особенность Объекта-значения — это его явное описание. Его проще 170 | понять в случаях, когда исходные типы из Golang (или любого другого языка 171 | программирования) не поддерживают конкретное поведение или поддерживаемое 172 | поведение не является интуитивно понятным. Мы можем работать с клиентами во 173 | многих проектах, и они должны удовлетворять некоторым бизнес-инвариантам, 174 | например, быть совершеннолетними или представлять какое-либо юридическое лицо. 175 | В таких случаях допустимо определять более ясные типы, например, `Birthday` и 176 | `LegalForm`. 177 | 178 | ```go 179 | type Birthday time.Time 180 | 181 | func (b Birthday) IsYoungerThen(other time.Time) bool { 182 | return time.Time(b).After(other) 183 | } 184 | 185 | func (b Birthday) IsAdult() bool { 186 | return time.Time(b).AddDate(18, 0, 0).Before(time.Now()) 187 | } 188 | 189 | const ( 190 | Freelancer = iota 191 | Partnership 192 | LLC 193 | Corporation 194 | ) 195 | 196 | type LegalForm int 197 | 198 | func (s LegalForm) IsIndividual() bool { 199 | return s == Freelancer 200 | } 201 | 202 | func (s LegalForm) HasLimitedResponsibility() bool { 203 | return s == LLC || s == Corporation 204 | } 205 | ``` 206 | 207 | Иногда Объект-значение не нужно явно определять как часть какой-либо другой 208 | Сущности или Объекта-значения. Тем не менее, мы можем определить Объект-значение 209 | в виде вспомогательного объекта, чтобы упростить его дальнейшее использование 210 | в коде. Например, Клиент (`Customer`) может быть физлицом (`Person`) или 211 | компанией (`Company`). В зависимости от типа Клиента меняется логика в 212 | приложении. Одним из лучших решений будет преобразование клиентов, используя 213 | вспомогательные объекты, чтобы с ними было проще работать. 214 | 215 | ```go 216 | type Customer struct { 217 | ID uuid.UUID 218 | Name string 219 | LegalForm LegalForm 220 | Date time.Time 221 | } 222 | 223 | func (c Customer) ToPerson() Person { 224 | return Person{ 225 | FullName: c.Name, 226 | Birthday: Birthday(c.Date), 227 | } 228 | } 229 | 230 | func (c Customer) ToCompany() Company { 231 | return Company{ 232 | Name: c.Name, 233 | CreationDate: c.Date, 234 | } 235 | } 236 | 237 | type Person struct { 238 | FullName string 239 | Birthday Birthday 240 | } 241 | 242 | type Company struct { 243 | Name string 244 | CreationDate time.Time 245 | } 246 | ``` 247 | 248 | Хотя вариант с преобразованием можно использовать в некоторых проектах, в 249 | большинстве случаев это означает, что мы должны добавить эти Объекты-значения 250 | в нашу модель предметной области. Фактически, каждый раз, когда мы замечаем, что 251 | какая-то конкретная группа полей постоянно взаимодействует друг с другом, но она 252 | находится внутри какой-то более крупной группы, то это знак. Мы должны 253 | сгруппировать их в Объект-значение и использовать его таким же образом внутри 254 | нашей большой группы (которая после этого уменьшается). 255 | 256 | ## Неизменяемость 257 | 258 | Объекты-значения неизменяемы. Нет ни единой повода, причины или другого 259 | аргумента для изменения состояния Объекта-значения в течение его жизненного 260 | цикла. Иногда несколько объектов могут содержать один и тот же Объект-значение 261 | (хотя это не идеальное решение). В таких случаях мы определенно не хотим, чтобы 262 | Объекты-значения изменялись где-либо. Итак, всякий раз, когда мы хотим изменить 263 | внутреннее состояние объекта-значения или объединить несколько из них, нам всегда 264 | нужно возвращать новый экземпляр с новым состоянием, как во фрагменте кода ниже. 265 | 266 | ```go 267 | // Неправильно. Состояние изменяется внутри объекта-значения 268 | func (m Money) AddAmount(amount float64) { 269 | m.Value += amount 270 | } 271 | 272 | // Правильно. Возвращаем новый объект-значение с новым состоянием 273 | func (m Money) WithAmount(amount float64) Money { 274 | return Money{ 275 | Value: m.Value + amount, 276 | Currency: m.Currency, 277 | } 278 | } 279 | 280 | // Неправильно. Состояние изменяется внутри объекта-значения 281 | func (m *Money) Deduct(other Money) { 282 | m.Value -= other.Value 283 | } 284 | 285 | // Правильно. Возвращаем новый объект-значение с новым состоянием 286 | func (m Money) DeductedWith(other Money) Money { 287 | return Money{ 288 | Value: m.Value - other.Value, 289 | Currency: m.Currency, 290 | } 291 | } 292 | 293 | // Неправильно. Состояние изменяется внутри объекта-значения 294 | func (c *Color) KeepOnlyGreen() { 295 | c.Red = 0 296 | c.Blue = 0 297 | } 298 | 299 | // Правильно. Возвращаем новый объект-значение с новым состоянием 300 | func (c Color) WithOnlyGreen() Color { 301 | return Color{ 302 | Red: 0, 303 | Green: c.Green, 304 | Blue: 0, 305 | } 306 | } 307 | ``` 308 | 309 | Во всех примерах единственный правильный способ — всегда возвращать новые 310 | экземпляры и оставлять старые нетронутыми. Хорошей практикой в Golang является 311 | всегда передавать в методы значения, а не ссылки на Объекты-значения, чтобы 312 | случайно не изменить внутреннее состояние. 313 | 314 | ```go 315 | func (m Money) Deduct(other Money) (Money, error) { 316 | if !m.Currency.EqualTo(other.Currency) { 317 | return Money{}, errors.New("currencies must be identical") 318 | } 319 | 320 | if other.Value > m.Value { 321 | return Money{}, errors.New("there is not enough amount to deduct") 322 | } 323 | return Money{ 324 | Value: m.Value - other.Value, 325 | Currency: m.Currency, 326 | }, nil 327 | } 328 | ``` 329 | 330 | Неизменяемость означает, что не нужно постоянно проверять правильные ли 331 | значения хранятся в его полях в течение всего жизненного цикла, а только при 332 | создании, как это показано в приведённом выше примере. Когда мы хотим создать 333 | новый Объект-значение, мы всегда должны осуществить валидацию и вернуть ошибки, 334 | если бизнес-инварианты не выполняются. Создавать Объект-значение нужно только 335 | в том случае, если проверка прошла успешна. С этого момента больше валидировать 336 | его не нужно. 337 | 338 | ## Наличие поведения 339 | Объекты-значения позволяют задавать различные варианты поведения. Его основная 340 | цель — предоставить доступный интерфейс. Наличие объекта-значения без методов 341 | заставляет задуматься о целесообразности его существования. Если объект-значение 342 | используется в каком-то конкретном месте кода, то он предоставляет доступ к 343 | огромному числу дополнительных бизнес-инвариантов, намного лучше описывающих 344 | решаемую нами проблему. 345 | 346 | ```go 347 | func (c Color) ToBrighter() Color { 348 | return Color{ 349 | Red: byte(math.Min(255, float64(c.Red+10))), 350 | Green: byte(math.Min(255, float64(c.Green+10))), 351 | Blue: byte(math.Min(255, float64(c.Blue+10))), 352 | } 353 | } 354 | 355 | func (c Color) ToDarker() Color { 356 | return Color{ 357 | Red: byte(math.Max(255, float64(c.Red-10))), 358 | Green: byte(math.Max(255, float64(c.Green-10))), 359 | Blue: byte(math.Max(255, float64(c.Blue-10))), 360 | } 361 | } 362 | 363 | func (c Color) Combine(other Color) Color { 364 | return Color{ 365 | Red: byte(math.Min(255, float64(c.Red+other.Red))), 366 | Green: byte(math.Min(255, float64(c.Green+other.Green))), 367 | Blue: byte(math.Min(255, float64(c.Blue+other.Blue))), 368 | } 369 | } 370 | 371 | func (c Color) IsRed() bool { 372 | return c.Red == 255 && c.Green == 0 && c.Blue == 0 373 | } 374 | 375 | func (c Color) IsYellow() bool { 376 | return c.Red == 255 && c.Green == 255 && c.Blue == 0 377 | } 378 | 379 | func (c Color) IsMagenta() bool { 380 | return c.Red == 255 && c.Green == 0 && c.Blue == 255 381 | } 382 | 383 | func (c Color) ToCSS() string { 384 | return fmt.Sprintf(`rgb(%d, %d, %d`, c.Red, c.Green, c.Blue) 385 | } 386 | ``` 387 | 388 | Декомпозиция всей модели предметной области на небольшие части, такие как 389 | Объекты-значения (и Сущности), делает код понятным и приближённым к бизнес-логике 390 | в реальном мире. Каждый Объект-значение может описывать некоторые небольшие 391 | компоненты и поддерживать различные модели поведения подобно обычным 392 | бизнес-процессам. В конце концов, это значительно упрощает весь процесс unit 393 | тестирования и помогает охватить все пограничные случаи. 394 | 395 | ## Заключение 396 | 397 | В реальном мире мы постоянно сталкиваемся с различными характеристиками, 398 | качественными, количественными величинами. Поскольку программное обеспечение 399 | пытается решить проблемы, существующие в реальном мире, использование таких 400 | показателей неизбежно. В нашей бизнес-логике для задания таких величин могут 401 | использоваться объекты-значения, представленные в этой статье. 402 | 403 | ## Полезные ссылки на источники: 404 | 405 | * [https://martinfowler.com/](https://martinfowler.com/) 406 | * [https://www.domainlanguage.com/](https://www.domainlanguage.com/) -------------------------------------------------------------------------------- /domain/bankAccount/entity/entity.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | // Сущность внутри уровня предметной области 9 | type BankAccount struct { 10 | ID int 11 | IsLocked bool 12 | Wallet Wallet 13 | Person Person 14 | } 15 | 16 | func (ba *BankAccount) Add(other Wallet) error { 17 | if ba.IsLocked { 18 | return errors.New("account is locked") 19 | } 20 | // 21 | // что-то делаем 22 | // 23 | return nil 24 | } 25 | 26 | // правильно - сущность BankAccount проверяет свои собственные инварианты 27 | func (ba *BankAccount) Deduct(other Wallet) error { 28 | if ba.IsLocked { 29 | return errors.New("account is locked") 30 | } 31 | 32 | result, err := ba.Wallet.Deduct(other) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | ba.Wallet = result 38 | 39 | return nil 40 | } 41 | 42 | type Currency struct { 43 | ID uint 44 | Code string 45 | Name string 46 | HtmlCode string 47 | } 48 | 49 | type Person struct { 50 | ID uint 51 | FirstName string 52 | LastName string 53 | DateOfBirth time.Time 54 | } 55 | 56 | type Wallet struct { 57 | Amount int 58 | Currency Currency 59 | } 60 | 61 | func (c Currency) IsEqual(other Currency) bool { 62 | return other.ID == c.ID 63 | } 64 | 65 | // правильно - объект-значение Wallet проверяет свои собственные инварианты 66 | func (w Wallet) Deduct(other Wallet) (Wallet, error) { 67 | if !other.Currency.IsEqual(w.Currency) { 68 | return Wallet{}, errors.New("currencies must be the same") 69 | } 70 | if other.Amount > w.Amount { 71 | return Wallet{}, errors.New("insufficient funds") 72 | } 73 | 74 | return Wallet{ 75 | Amount: w.Amount - other.Amount, 76 | Currency: w.Currency, 77 | }, nil 78 | } 79 | -------------------------------------------------------------------------------- /domain/bankAccount/repository/bank_account_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | domain "github.com/MaksimDzhangirov/PracticalDDD/domain/bankAccount/entity" 6 | ) 7 | 8 | // Интерфейс репозитория внутри уровня предметной области 9 | type BankAccountRepository interface { 10 | Get(ctx context.Context, ID int) (*domain.BankAccount, error) 11 | } 12 | -------------------------------------------------------------------------------- /domain/bonus/entity/bonus.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import domain "github.com/MaksimDzhangirov/PracticalDDD/domain/userAccount/entity" 4 | 5 | type Bonus struct { 6 | Name string 7 | } 8 | 9 | func (b *Bonus) Apply(account *domain.Account) error { 10 | // 11 | // какой-то код 12 | // 13 | 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /domain/bonus/repository/bonus_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/domain/bonus/entity" 5 | domain "github.com/MaksimDzhangirov/PracticalDDD/domain/userAccount/entity" 6 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 7 | ) 8 | 9 | type BonusRepository interface { 10 | FindAllEligibleFor(account domain.Account, money value_objects.Money) ([]entity.Bonus, error) 11 | } -------------------------------------------------------------------------------- /domain/email/entity/email.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "github.com/google/uuid" 4 | 5 | type Email struct { 6 | id uuid.UUID 7 | // 8 | // какие-то поля 9 | // 10 | } 11 | -------------------------------------------------------------------------------- /domain/exchangeRate/repository/exchange_rate_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | type ExchangeRateRepository interface { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /domain/order/entity/entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/events" 5 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type Order struct { 10 | id uuid.UUID 11 | // 12 | // какие-то поля 13 | // 14 | isDispatched bool 15 | deliverAddress value_objects.Address 16 | } 17 | 18 | func (o Order) ID() uuid.UUID { 19 | return o.id 20 | } 21 | 22 | func (o Order) ChangeAddress(address value_objects.Address) events.Event { 23 | if o.isDispatched { 24 | return events.NewDeliveryAddressChangeFailed(o.ID()) 25 | } 26 | // 27 | // какой-то код 28 | // 29 | return events.NewDeliveryAddressChanged(o.ID()) 30 | } -------------------------------------------------------------------------------- /domain/order/repository/order_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "github.com/MaksimDzhangirov/PracticalDDD/domain/order/entity" 4 | 5 | type OrderRepository interface { 6 | Create(order entity.Order) (*entity.Order, error) 7 | } 8 | -------------------------------------------------------------------------------- /domain/services/account.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import "github.com/MaksimDzhangirov/PracticalDDD/domain/userAccount/entity" 4 | 5 | // уровень предметной области 6 | type AccountService interface { 7 | Update(account entity.Account) error 8 | } -------------------------------------------------------------------------------- /domain/services/order.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/domain/order/entity" 5 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 6 | ) 7 | 8 | type OrderService interface { 9 | Create(order entity.Order) (*entity.Order, error) 10 | ChangeAddress(order entity.Order, address value_objects.Address) 11 | } 12 | -------------------------------------------------------------------------------- /domain/userAccount/entity/entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Account struct { 4 | Name string 5 | } 6 | -------------------------------------------------------------------------------- /events/email_events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type EmailEvent interface { 9 | Event 10 | EmailID() uuid.UUID 11 | } 12 | 13 | type EmailSent struct { 14 | emailID uuid.UUID 15 | } 16 | 17 | func (e EmailSent) Name() string { 18 | return "event.email.sent" 19 | } 20 | 21 | func (e EmailSent) EmailID() uuid.UUID { 22 | return e.emailID 23 | } 24 | 25 | type EmailHandler struct { 26 | // 27 | // какие-то поля 28 | // 29 | } 30 | 31 | func (e *EmailHandler) Notify(event Event) { 32 | switch actualEvent := event.(type) { 33 | case EmailSent: 34 | fmt.Println(actualEvent) 35 | // 36 | // что-то делаем 37 | // 38 | default: 39 | return 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /events/event.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "github.com/google/uuid" 4 | 5 | // Интерфейс Event для описания События предметной области 6 | type Event interface { 7 | Name() string 8 | } 9 | 10 | // Событие GeneralError 11 | type GeneralError string 12 | 13 | func NewGeneralError(err error) Event { 14 | return GeneralError(err.Error()) 15 | } 16 | 17 | func (e GeneralError) Name() string { 18 | return "event.general.error" 19 | } 20 | 21 | // Интерфейс OrderEvent для описания События предметной области, связанных с Заказом 22 | type OrderEvent interface { 23 | Event 24 | OrderID() uuid.UUID 25 | } 26 | 27 | // Событие OrderDispatched 28 | type OrderDispatched struct { 29 | orderID uuid.UUID 30 | } 31 | 32 | func (e OrderDispatched) Name() string { 33 | return "event.order.dispatched" 34 | } 35 | 36 | func (e OrderDispatched) OrderID() uuid.UUID { 37 | return e.orderID 38 | } 39 | 40 | // Событие OrderDelivered 41 | type OrderDelivered struct { 42 | orderID uuid.UUID 43 | } 44 | 45 | func (e OrderDelivered) Name() string { 46 | return "event.order.delivery.success" 47 | } 48 | 49 | func (e OrderDelivered) OrderID() uuid.UUID { 50 | return e.orderID 51 | } 52 | 53 | // Событие OrderDeliveryFailed 54 | type OrderDeliveryFailed struct { 55 | orderID uuid.UUID 56 | } 57 | 58 | func (e OrderDeliveryFailed) Name() string { 59 | return "event.order.delivery.failed" 60 | } 61 | 62 | func (e OrderDeliveryFailed) OrderID() uuid.UUID { 63 | return e.orderID 64 | } 65 | 66 | type DeliveryAddressChangeFailed struct { 67 | orderID uuid.UUID 68 | } 69 | 70 | func NewDeliveryAddressChangeFailed(orderID uuid.UUID) DeliveryAddressChangeFailed { 71 | return DeliveryAddressChangeFailed{ 72 | orderID: orderID, 73 | } 74 | } 75 | 76 | func (e DeliveryAddressChangeFailed) Name() string { 77 | return "event.order.delivery-address-change.failed" 78 | } 79 | 80 | func (e DeliveryAddressChangeFailed) OrderID() uuid.UUID { 81 | return e.orderID 82 | } 83 | 84 | type DeliveryAddressChanged struct { 85 | orderID uuid.UUID 86 | } 87 | 88 | func NewDeliveryAddressChanged(orderID uuid.UUID) DeliveryAddressChanged { 89 | return DeliveryAddressChanged{ 90 | orderID: orderID, 91 | } 92 | } 93 | 94 | func (e DeliveryAddressChanged) Name() string { 95 | return "event.order.delivery-address-change.failed" 96 | } 97 | 98 | func (e DeliveryAddressChanged) OrderID() uuid.UUID { 99 | return e.orderID 100 | } 101 | 102 | type OrderCreated struct { 103 | orderID uuid.UUID 104 | } 105 | 106 | func NewOrderCreated(orderID uuid.UUID) OrderCreated { 107 | return OrderCreated{ 108 | orderID: orderID, 109 | } 110 | } 111 | 112 | func (e OrderCreated) Name() string { 113 | return "event.order.dispatched" 114 | } 115 | 116 | func (e OrderCreated) OrderID() uuid.UUID { 117 | return e.orderID 118 | } -------------------------------------------------------------------------------- /events/event_publisher.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // Интерфейс EventHandler, описывающий любой объект, который должен быть 4 | // уведомлен о каком-либо Event 5 | type EventHandler interface { 6 | Notify(event Event) 7 | } 8 | 9 | // EventPublisher - основная структура, уведомляющая все EventHandler 10 | type EventPublisher struct { 11 | handlers map[string][]EventHandler 12 | } 13 | 14 | // Метод Subscribe подписывает EventHandler на определённое событие (Event) 15 | func (e *EventPublisher) Subscribe(handler EventHandler, events ...Event) { 16 | for _, event := range events { 17 | handlers := e.handlers[event.Name()] 18 | handlers = append(handlers, handler) 19 | e.handlers[event.Name()] = handlers 20 | } 21 | } 22 | 23 | // Метод Notify уведомляет подписанный EventHandler о том, что произошло определенное событие (Event) 24 | func (e *EventPublisher) Notify(event Event) { 25 | for _, handler := range e.handlers[event.Name()] { 26 | handler.Notify(event) 27 | } 28 | } -------------------------------------------------------------------------------- /events/sqs.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // инфраструктурный уровень 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | // 10 | // какой-то импорт 11 | // 12 | "github.com/aws/aws-sdk-go/aws" 13 | "github.com/aws/aws-sdk-go/service/sqs" 14 | ) 15 | 16 | // EventSQSHandler передаёт внутренние события во внешний мир 17 | type EventSQSHandler struct { 18 | svc *sqs.SQS 19 | } 20 | 21 | // Notify передаёт события через SQS 22 | func (e *EventSQSHandler) Notify(event Event) { 23 | data := map[string]string{ 24 | "event": event.Name(), 25 | } 26 | 27 | body, err := json.Marshal(data) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | _, err = e.svc.SendMessage(&sqs.SendMessageInput{ 33 | MessageBody: aws.String(string(body)), 34 | QueueUrl: &e.svc.Endpoint, 35 | }) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | } 40 | 41 | // инфраструктурный уровень 42 | type SQSService struct { 43 | svc *sqs.SQS 44 | publisher *EventPublisher 45 | stopChannel chan bool 46 | } 47 | 48 | // Run запускает прослушивание SQS сообщений 49 | func (s *SQSService) Run(event Event) { 50 | eventChan := make(chan Event) 51 | 52 | MessageLoop: 53 | for { 54 | s.listen(eventChan) 55 | 56 | select { 57 | case event := <-eventChan: 58 | s.publisher.Notify(event) 59 | case <-s.stopChannel: 60 | break MessageLoop 61 | } 62 | } 63 | 64 | close(eventChan) 65 | close(s.stopChannel) 66 | } 67 | 68 | // Stop останавливает прослушивание SQS сообщений 69 | func (s *SQSService) Stop() { 70 | s.stopChannel <- true 71 | } 72 | 73 | func (s *SQSService) listen(eventChan chan Event) { 74 | go func() { 75 | message, err := s.svc.ReceiveMessage(&sqs.ReceiveMessageInput{ 76 | // 77 | // какой-то код 78 | // 79 | }) 80 | fmt.Println(message) 81 | 82 | var event Event 83 | if err != nil { 84 | log.Print(err) 85 | event = NewGeneralError(err) 86 | return 87 | } else { 88 | // 89 | // извлечь сообщение 90 | // 91 | } 92 | 93 | eventChan <- event 94 | }() 95 | } 96 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MaksimDzhangirov/PracticalDDD 2 | 3 | go 1.16 4 | 5 | require ( 6 | flamingo.me/dingo v0.2.9 7 | github.com/aws/aws-sdk-go v1.41.6 8 | github.com/go-redis/redis/v8 v8.11.4 // indirect 9 | github.com/google/uuid v1.3.0 10 | gorm.io/gorm v1.21.16 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | flamingo.me/dingo v0.2.9 h1:HL7YV4iv3F6xLcUPvIBEzdkbhBSb6PukkZdoOZ8H+Eo= 2 | flamingo.me/dingo v0.2.9/go.mod h1:NXspAYkbktnP0EKs/27QW6Evija8WZfWGtrMcauOejQ= 3 | github.com/aws/aws-sdk-go v1.41.6 h1:ojO1jWhE3lkJlTFQOq0rlWZ11q18LIdsZNtGJ07FFEA= 4 | github.com/aws/aws-sdk-go v1.41.6/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= 5 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 6 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 12 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 13 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 14 | github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= 15 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 16 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 17 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 19 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 20 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 21 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 22 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 23 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 24 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 25 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 26 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 27 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 28 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 32 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 33 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 34 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 35 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 36 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= 37 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 38 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 39 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 40 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 41 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 42 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 43 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 44 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 45 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 46 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 47 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 48 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 49 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 50 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 54 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 55 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 56 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 57 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 58 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 59 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 60 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 61 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 62 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 63 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 66 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 67 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 68 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 69 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 70 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 73 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 75 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 82 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 84 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 85 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 86 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 87 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 88 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 89 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 90 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 91 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 92 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 94 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 95 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 96 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 97 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 98 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 99 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 100 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 101 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 102 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 103 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 104 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 105 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 106 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 107 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 108 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 109 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 110 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 111 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 112 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 113 | gorm.io/gorm v1.21.16 h1:YBIQLtP5PLfZQz59qfrq7xbrK7KWQ+JsXXCH/THlMqs= 114 | gorm.io/gorm v1.21.16/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 115 | -------------------------------------------------------------------------------- /gorm-generics/repository.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "context" 4 | 5 | func (r *GormRepository[M, E]) Insert(ctx context.Context, entity *E) error { 6 | // отображаем данные из Entity в DTO 7 | var start M 8 | model := start.FromEntity(*entity).(M) 9 | 10 | // создаём новую запись в базе данных 11 | err := r.db.WithContext(ctx).Create(&model).Error 12 | // обработка ошибки 13 | 14 | // отображаем новую запись из базы в Entity 15 | *entity = model.ToEntity() 16 | return nil 17 | } 18 | 19 | func (r *GormRepository[M, E]) FindByID(ctx context.Context, id uint) (E, error) { 20 | // извлекаем запись по id из базы данных 21 | var model M 22 | err := r.db.WithContext(ctx).First(&model, id).Error 23 | // обработка ошибки 24 | 25 | // отображаем запись в Entity 26 | return model.ToEntity(), nil 27 | } 28 | 29 | func (r *GormRepository[M, E]) Find(ctx context.Context, specification Specification) ([]E, error) { 30 | // получаем записи по некоторому критерию 31 | var models []M 32 | err := r.db.WithContext(ctx).Where(specification.GetQuery(), specification.GetValues()...).Find(&models).Error 33 | // обработка ошибки 34 | 35 | // отображаем все записи в Entities 36 | result := make([]E, 0, len(models)) 37 | for _, row := range models { 38 | result = append(result, row.ToEntity()) 39 | } 40 | 41 | return result, nil 42 | } 43 | -------------------------------------------------------------------------------- /gorm-generics/simple_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "gorm.io/gorm" 8 | 9 | "github.com/ompluscator/gorm-generics" 10 | // какие-то импорты 11 | ) 12 | 13 | // Product - сущность предметной области 14 | type Product struct { 15 | // какие-то поля 16 | } 17 | 18 | // ProductGorm - это DTO для сопоставления сущности Product с базой данных 19 | type ProductGorm struct { 20 | // какие-то поля 21 | } 22 | 23 | // ToEntity соответствует интерфейсу gorm_generics.GormModel 24 | func (g ProductGorm) ToEntity() Product { 25 | return Product{ 26 | // какие-то поля 27 | } 28 | } 29 | 30 | // FromEntity соответствует интерфейсу gorm_generics.GormModel 31 | func (g ProductGorm) FromEntity(product Product) interface{} { 32 | return ProductGorm{ 33 | // какие-то поля 34 | } 35 | } 36 | 37 | func main() { 38 | db, err := gorm.Open( /* строка подключения к БД */ ) 39 | // обработка ошибки 40 | 41 | err = db.AutoMigrate(ProductGorm{}) 42 | // обработка ошибки 43 | 44 | // инициализируем новый репозиторий, передавая 45 | // GORM модель и сущность как тип 46 | repository := gorm_generics.NewRepository[ProductGorm, Product](db) 47 | 48 | ctx := context.Background() 49 | 50 | // создаём новую сущность 51 | product := Product{ 52 | // какие-то поля 53 | } 54 | 55 | // посылаем новую сущность в репозиторий для сохранения 56 | err = repository.Insert(ctx, &product) 57 | // обработка ошибки 58 | 59 | fmt.Println(product) 60 | // Выводит: 61 | // {1 product1 100 true} 62 | 63 | single, err := repository.FindByID(ctx, product.ID) 64 | // обработка ошибки 65 | 66 | fmt.Println(single) 67 | // Выводит: 68 | // {1 product1 100 true} 69 | } 70 | -------------------------------------------------------------------------------- /gorm-generics/specification.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Specification interface { 9 | GetQuery() string 10 | GetValues() []any 11 | } 12 | 13 | // joinSpecification - это действующая реализация интерфейса Specification 14 | // Она используется для операторов AND и OR 15 | type joinSpecification struct { 16 | specifications []Specification 17 | separator string 18 | } 19 | 20 | // GetQuery объединяет все подзапросы 21 | func (s joinSpecification) GetQuery() string { 22 | queries := make([]string, 0, len(s.specifications)) 23 | 24 | for _, spec := range s.specifications { 25 | queries = append(queries, spec.GetQuery()) 26 | } 27 | 28 | return strings.Join(queries, fmt.Sprintf(" %s ", s.separator)) 29 | } 30 | 31 | // GetValues объединяет все подзначения 32 | func (s joinSpecification) GetValues() []any { 33 | values := make([]any, 0) 34 | 35 | for _, spec := range s.specifications { 36 | values = append(values, spec.GetValues()...) 37 | } 38 | 39 | return values 40 | } 41 | 42 | // And передаёт AND оператор в виде Specification 43 | func And(specifications ...Specification) Specification { 44 | return joinSpecification{ 45 | specifications: specifications, 46 | separator: "AND", 47 | } 48 | } 49 | 50 | // notSpecification отрицает под Specification 51 | type notSpecification struct { 52 | Specification 53 | } 54 | 55 | // GetQuery отрицает подзапрос 56 | func (s notSpecification) GetQuery() string { 57 | return fmt.Sprintf(" NOT (%s)", s.Specification.GetQuery()) 58 | } 59 | 60 | // Not передаёт NOT оператор в виде Specification 61 | func Not(specification Specification) Specification { 62 | return notSpecification{ 63 | specification, 64 | } 65 | } 66 | 67 | // binaryOperatorSpecification определяет бинарный оператор как Specification 68 | // Он используется для операторов =, >, <, >=, <=. 69 | type binaryOperatorSpecification[T any] struct { 70 | field string 71 | operator string 72 | value T 73 | } 74 | 75 | // GetQuery создаёт запрос для бинарного оператора 76 | func (s binaryOperatorSpecification[T]) GetQuery() string { 77 | return fmt.Sprintf("%s %s ?", s.field, s.operator) 78 | } 79 | 80 | // GetValues возвращает значение для бинарного оператора 81 | func (s binaryOperatorSpecification[T]) GetValues() []any { 82 | return []any{s.value} 83 | } 84 | 85 | // Equal передаёт оператор равенства в виде Specification 86 | func Equal[T any](field string, value T) Specification { 87 | return binaryOperatorSpecification[T]{ 88 | field: field, 89 | operator: "=", 90 | value: value, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /infrastructure/bankAccount/dto/dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import domain "github.com/MaksimDzhangirov/PracticalDDD/domain/bankAccount/entity" 4 | 5 | // DTO внутри инфраструктурного уровня 6 | type BankAccountGorm struct { 7 | ID int `gorm:"primaryKey";column:id` 8 | UUID string `gorm:"column:uuid"` 9 | IsLocked bool `gorm:"column:is_locked"` 10 | Amount int `gorm:"column:amount"` 11 | CurrencyID uint `gorm:"column:currency_id"` 12 | Currency CurrencyGorm `gorm:"foreignKey:CurrencyID"` 13 | PersonID uint `gorm:"column:person_id"` 14 | Person PersonGorm `gorm:"foreignKey:PersonID"` 15 | } 16 | 17 | type CurrencyGorm struct { 18 | UUID string `gorm:"column:uuid"` 19 | } 20 | 21 | type PersonGorm struct { 22 | } 23 | 24 | func (cg *CurrencyGorm) ToEntity() domain.Currency { 25 | return domain.Currency{} 26 | } 27 | 28 | func (pg *PersonGorm) ToEntity() domain.Person { 29 | return domain.Person{} 30 | } 31 | -------------------------------------------------------------------------------- /infrastructure/bankAccount/repository/bank_account_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | domain "github.com/MaksimDzhangirov/PracticalDDD/domain/bankAccount/entity" 6 | dtopackage "github.com/MaksimDzhangirov/PracticalDDD/infrastructure/bankAccount/dto" 7 | ) 8 | 9 | // фактическая реализация репозитория внутри инфраструктурного уровня 10 | type BankAccountRepository struct { 11 | // 12 | // какие-то поля 13 | // 14 | } 15 | 16 | func (r *BankAccountRepository) Get(ctx context.Context, ID uint) (*domain.BankAccount, error) { 17 | var dto dtopackage.BankAccountGorm 18 | // 19 | // какой-то код 20 | // 21 | return &domain.BankAccount{ 22 | ID: dto.ID, 23 | IsLocked: dto.IsLocked, 24 | Wallet: domain.Wallet{ 25 | Amount: dto.Amount, 26 | Currency: dto.Currency.ToEntity(), 27 | }, 28 | Person: dto.Person.ToEntity(), 29 | }, nil 30 | } 31 | -------------------------------------------------------------------------------- /infrastructure/bonus/repository/bonus_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/domain/bonus/entity" 5 | domain "github.com/MaksimDzhangirov/PracticalDDD/domain/userAccount/entity" 6 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 7 | ) 8 | 9 | type BonusRepository struct { 10 | 11 | } 12 | 13 | func (r *BonusRepository) FindAllEligibleFor(account domain.Account, money value_objects.Money) ([]entity.Bonus, error) { 14 | var result []entity.Bonus 15 | // 16 | // какой-то код 17 | // 18 | return result, nil 19 | } -------------------------------------------------------------------------------- /infrastructure/exchangeRate/repository/exchange_rate_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | type ExchangeRateRepository struct { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /pkg/access/access.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import ( 4 | access_repository "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/domain/repository" 5 | access_service "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/domain/service" 6 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/infrastructure/database" 7 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/infrastructure/fake" 8 | ) 9 | 10 | type AccessModule struct { 11 | repository access_repository.UserRepository 12 | service access_service.UserService 13 | } 14 | 15 | func NewAccessModule(useDatabase bool) *AccessModule { 16 | var repository access_repository.UserRepository 17 | if useDatabase { 18 | repository = &database.UserDBRepository{} 19 | } else { 20 | repository = &fake.UserFakeRepository{} 21 | } 22 | var service access_service.UserService 23 | // 24 | // какой-то код 25 | // 26 | return &AccessModule{ 27 | repository: repository, 28 | service: service, 29 | } 30 | } 31 | 32 | func (m *AccessModule) GetRepository() access_repository.UserRepository { 33 | return m.repository 34 | } 35 | 36 | func (m *AccessModule) GetService() access_service.UserService { 37 | return m.service 38 | } -------------------------------------------------------------------------------- /pkg/access/domain/model/access_model.go: -------------------------------------------------------------------------------- 1 | package access_model 2 | 3 | import "github.com/google/uuid" 4 | 5 | type User struct { 6 | ID uuid.UUID 7 | // 8 | // какие-то поля 9 | // 10 | } -------------------------------------------------------------------------------- /pkg/access/domain/repository/access_repository.go: -------------------------------------------------------------------------------- 1 | package access_repository 2 | 3 | import access_model "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/domain/model" 4 | 5 | type UserRepository interface { 6 | Create(user access_model.User) error 7 | } 8 | -------------------------------------------------------------------------------- /pkg/access/domain/service/access_service.go: -------------------------------------------------------------------------------- 1 | package access_service 2 | 3 | import access_model "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/domain/model" 4 | 5 | type UserService interface { 6 | Create(user access_model.User) error 7 | // 8 | // какие-то методы 9 | // 10 | } 11 | -------------------------------------------------------------------------------- /pkg/access/infrastructure/database/access_database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import access_model "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/domain/model" 4 | 5 | type UserDBRepository struct { 6 | // 7 | // какие-то поля 8 | // 9 | } 10 | 11 | func (r *UserDBRepository) Create(user access_model.User) error { 12 | // 13 | // какие-то код 14 | // 15 | return nil 16 | } -------------------------------------------------------------------------------- /pkg/access/infrastructure/fake/access_fake.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import access_model "github.com/MaksimDzhangirov/PracticalDDD/pkg/access/domain/model" 4 | 5 | type UserFakeRepository struct { 6 | // 7 | // какие-то поля 8 | // 9 | } 10 | 11 | func (r *UserFakeRepository) Create(user access_model.User) error { 12 | // 13 | // какие-то код 14 | // 15 | return nil 16 | } -------------------------------------------------------------------------------- /pkg/bankAccount/domain/model/bank_account.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/google/uuid" 4 | 5 | // Сущность 6 | type BankAccount struct { 7 | id uuid.UUID 8 | iban string 9 | amount int 10 | currency Currency 11 | } 12 | 13 | func NewBankAccount(currency Currency) BankAccount { 14 | return BankAccount{ 15 | // 16 | // определяем поля 17 | // 18 | } 19 | } 20 | 21 | func (ba BankAccount) HasMoney() bool { 22 | return ba.amount > 0 23 | } 24 | 25 | func (ba BankAccount) InDebt() bool { 26 | return ba.amount < 0 27 | } 28 | 29 | func (ba BankAccount) IsForCurrency(currency Currency) bool { 30 | return ba.currency.Equal(currency) 31 | } 32 | 33 | type BankAccounts []BankAccount 34 | 35 | func (bas BankAccounts) HasMoney() bool { 36 | for _, ba := range bas { 37 | if ba.HasMoney() { 38 | return true 39 | } 40 | } 41 | 42 | return false 43 | } 44 | 45 | func (bas BankAccounts) InDebt() bool { 46 | for _, ba := range bas { 47 | if ba.InDebt() { 48 | return true 49 | } 50 | } 51 | 52 | return false 53 | } 54 | 55 | func (bas BankAccounts) HasCurrency(currency Currency) bool { 56 | for _, ba := range bas { 57 | if ba.IsForCurrency(currency) { 58 | return true 59 | } 60 | } 61 | 62 | return false 63 | } 64 | 65 | func (bas BankAccounts) AddMoney(amount int, currency Currency) error { 66 | panic("not implemented") 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/bankAccount/domain/model/currency.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/google/uuid" 4 | 5 | // Сущность 6 | type Currency struct { 7 | id uuid.UUID 8 | // 9 | // какие-то поля 10 | // 11 | } 12 | 13 | func (c Currency) Equal(other Currency) bool { 14 | return c.id == other.id 15 | } 16 | -------------------------------------------------------------------------------- /pkg/bankAccount/domain/model/customer_account.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | // Сущность и агрегат 9 | type CustomerAccount struct { 10 | id uuid.UUID 11 | isDeleted bool 12 | isLocked bool 13 | // 14 | // какие-то поля 15 | // 16 | accounts BankAccounts 17 | // 18 | // какие-то поля 19 | // 20 | } 21 | 22 | func (ca *CustomerAccount) GetIBANForCurrency(currency Currency) (string, error) { 23 | for _, account := range ca.accounts { 24 | if account.IsForCurrency(currency) { 25 | return account.iban, nil 26 | } 27 | } 28 | return "", errors.New("this account does not support this currency") 29 | } 30 | 31 | func (ca *CustomerAccount) MarkAsDeleted() error { 32 | if ca.accounts.HasMoney() { 33 | return errors.New("there are still money on bank account") 34 | } 35 | if ca.accounts.InDebt() { 36 | return errors.New("bank account is in debt") 37 | } 38 | 39 | ca.isDeleted = true 40 | 41 | return nil 42 | } 43 | 44 | func (ca *CustomerAccount) CreateAccountForCurrency(currency Currency) error { 45 | if ca.accounts.HasCurrency(currency) { 46 | return errors.New("there is already bank account for that currency") 47 | } 48 | ca.accounts = append(ca.accounts, NewBankAccount(currency)) 49 | 50 | return nil 51 | } 52 | 53 | func (ca *CustomerAccount) AddMoney(amount int, currency Currency) error { 54 | if ca.isDeleted { 55 | return errors.New("account is deleted") 56 | } 57 | if ca.isLocked { 58 | return errors.New("account is locked") 59 | } 60 | 61 | return ca.accounts.AddMoney(amount, currency) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/billing/billing.go: -------------------------------------------------------------------------------- 1 | package billing 2 | 3 | import "flamingo.me/dingo" 4 | 5 | type TransactionLog struct { 6 | 7 | } 8 | 9 | type DatabaseTransactionLog struct { 10 | 11 | } 12 | 13 | type CreditCardProcessor struct { 14 | 15 | } 16 | 17 | type PaypalCreditCardProcessor struct { 18 | 19 | } 20 | 21 | type BillingModule struct {} 22 | 23 | func (module *BillingModule) Configure(injector *dingo.Injector) { 24 | // Эта команда сообщает Dingo, что всякий раз, когда она видит зависимость от TransactionLog 25 | // она должна удовлетворить её, используя DatabaseTransactionLog. 26 | injector.Bind(new(TransactionLog)).To(DatabaseTransactionLog{}) 27 | 28 | // По аналогии такая запись сообщает Dingo, что когда используется CreditCardProcessor в зависимости, 29 | // она должна быть удовлетворена с помощью PaypalCreditCardProcessor. 30 | injector.Bind(new(CreditCardProcessor)).To(PaypalCreditCardProcessor{}) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/client/domain/model/address.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Address struct { 4 | Street string 5 | Number string 6 | Postcode string 7 | City string 8 | } 9 | -------------------------------------------------------------------------------- /pkg/client/domain/model/company.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type Company struct { 6 | Name string 7 | RegistrationNumber string 8 | RegistrationDate time.Time 9 | } 10 | -------------------------------------------------------------------------------- /pkg/client/domain/model/customer.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type Customer struct { 8 | ID uuid.UUID 9 | Person *Person 10 | Company *Company 11 | Address Address 12 | } 13 | -------------------------------------------------------------------------------- /pkg/client/domain/model/customer_account.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/google/uuid" 4 | 5 | type CustomerAccount struct { 6 | id uuid.UUID // глобальный идентификатор 7 | person *Person 8 | company *Company 9 | // 10 | // какие-то поля 11 | // 12 | } -------------------------------------------------------------------------------- /pkg/client/domain/model/person.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Birthday time.Time 8 | 9 | type Person struct { 10 | SSN string 11 | FirstName string 12 | LastName string 13 | Birthday Birthday 14 | } 15 | -------------------------------------------------------------------------------- /pkg/client/domain/repository/customer_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/domain/model" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type CustomerSpecification struct { 10 | } 11 | 12 | type Customers []model.Customer 13 | 14 | type CustomerRepository interface { 15 | GetCustomer(ctx context.Context, ID uuid.UUID) (*model.Customer, error) 16 | Search(ctx context.Context, specification CustomerSpecification) (Customers, int, error) 17 | SaveCustomer(ctx context.Context, customer model.Customer) (*model.Customer, error) 18 | UpdateCustomer(ctx context.Context, customer model.Customer) (*model.Customer, error) 19 | DeleteCustomer(ctx context.Context, ID uuid.UUID) (*model.Customer, error) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/client/infrastructure/customer.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/domain/model" 7 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/infrastructure/dto" 8 | "github.com/google/uuid" 9 | "gorm.io/gorm" 10 | "time" 11 | ) 12 | 13 | // инфраструктурный уровень 14 | 15 | type CustomerRepository struct { 16 | connection *gorm.DB 17 | } 18 | 19 | func (r *CustomerRepository) GetCustomer(ctx context.Context, ID uuid.UUID) (*model.Customer, error) { 20 | var row dto.CustomerGorm 21 | err := r.connection.WithContext(ctx).Where("uuid = ?", ID).First(&row).Error 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | customer, err := row.ToEntity() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &customer, nil 32 | } 33 | 34 | func (r *CustomerRepository) SaveCustomer(ctx context.Context, customer model.Customer) (*model.Customer, error) { 35 | row := NewRow(customer) 36 | err := r.connection.WithContext(ctx).Save(&row).Error 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | customer, err = row.ToEntity() 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &customer, nil 47 | } 48 | 49 | func (r *CustomerRepository) CreateCustomer(ctx context.Context, customer model.Customer) (*model.Customer, error) { 50 | tx := r.connection.Begin() 51 | defer func() { 52 | if r := recover(); r != nil { 53 | tx.Rollback() 54 | } 55 | }() 56 | 57 | if err := tx.Error; err != nil { 58 | return nil, err 59 | } 60 | 61 | // 62 | // какой-то код 63 | // 64 | 65 | var total int64 66 | var err error 67 | if customer.Person != nil { 68 | err = tx.Model(dto.PersonGorm{}).Where("ssn = ?", customer.Person.SSN).Count(&total).Error 69 | } else if customer.Person != nil { 70 | err = tx.Model(dto.CompanyGorm{}).Where("registration_number = ?", customer.Company.RegistrationNumber).Count(&total).Error 71 | } 72 | if err != nil { 73 | tx.Rollback() 74 | return nil, err 75 | } else if total > 0 { 76 | tx.Rollback() 77 | return nil, errors.New("there is already such record in DB") 78 | } 79 | 80 | // 81 | // какой-то код 82 | // 83 | row := NewRow(customer) 84 | err = tx.Save(&row).Error 85 | if err != nil { 86 | tx.Rollback() 87 | return nil, err 88 | } 89 | 90 | err = tx.Commit().Error 91 | if err != nil { 92 | tx.Rollback() 93 | return nil, err 94 | } 95 | 96 | customer, err = row.ToEntity() 97 | if err != nil { 98 | tx.Rollback() 99 | return nil, err 100 | } 101 | 102 | return &customer, nil 103 | } 104 | 105 | // 106 | // другие методы 107 | // 108 | 109 | func NewRow(customer model.Customer) dto.CustomerGorm { 110 | var person *dto.PersonGorm 111 | if customer.Person != nil { 112 | person = &dto.PersonGorm{ 113 | SSN: customer.Person.SSN, 114 | FirstName: customer.Person.FirstName, 115 | LastName: customer.Person.LastName, 116 | Birthday: time.Time(customer.Person.Birthday), 117 | } 118 | } 119 | 120 | var company *dto.CompanyGorm 121 | if customer.Company != nil { 122 | company = &dto.CompanyGorm{ 123 | Name: customer.Company.Name, 124 | RegistrationNumber: customer.Company.RegistrationNumber, 125 | RegistrationDate: customer.Company.RegistrationDate, 126 | } 127 | } 128 | 129 | return dto.CustomerGorm{ 130 | UUID: uuid.NewString(), 131 | Person: person, 132 | Company: company, 133 | Street: customer.Address.Street, 134 | Number: customer.Address.Number, 135 | Postcode: customer.Address.Postcode, 136 | City: customer.Address.City, 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /pkg/client/infrastructure/dto/company.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/domain/model" 5 | "time" 6 | ) 7 | 8 | type CompanyGorm struct { 9 | ID uint `gorm:"primaryKey;column:id"` 10 | Name string `gorm:"column:name"` 11 | RegistrationNumber string `gorm:"column:registration_number"` 12 | RegistrationDate time.Time `gorm:"column:registration_date"` 13 | } 14 | 15 | func (c *CompanyGorm) ToEntity() *model.Company { 16 | if c == nil { 17 | return nil 18 | } 19 | 20 | return &model.Company{ 21 | Name: c.Name, 22 | RegistrationNumber: c.RegistrationNumber, 23 | RegistrationDate: c.RegistrationDate, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/client/infrastructure/dto/customer.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/domain/model" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type CustomerGorm struct { 9 | ID uint `gorm:"primaryKey;column:id"` 10 | UUID string `gorm:"uniqueIndex;column:id"` 11 | PersonID uint `gorm:"column:person_id"` 12 | Person *PersonGorm `gorm:"foreignKey:PersonID"` 13 | CompanyID uint `gorm:"column:company_id"` 14 | Company *CompanyGorm `gorm:"foreignKey:CompanyID"` 15 | Street string `gorm:"column:street"` 16 | Number string `gorm:"column:number"` 17 | Postcode string `gorm:"column:postcode"` 18 | City string `gorm:"column:city"` 19 | } 20 | 21 | func (c CustomerGorm) ToEntity() (model.Customer, error) { 22 | parsed, err := uuid.Parse(c.UUID) 23 | if err != nil { 24 | return model.Customer{}, err 25 | } 26 | 27 | return model.Customer{ 28 | ID: parsed, 29 | Person: c.Person.ToEntity(), 30 | Company: c.Company.ToEntity(), 31 | Address: model.Address{ 32 | Street: c.Street, 33 | Number: c.Number, 34 | Postcode: c.Postcode, 35 | City: c.City, 36 | }, 37 | }, nil 38 | } 39 | 40 | type CustomerJSON struct { 41 | 42 | } 43 | 44 | func (c CustomerJSON) ToEntity() (model.Customer, error) { 45 | panic("not implemented") 46 | return model.Customer{}, nil 47 | } -------------------------------------------------------------------------------- /pkg/client/infrastructure/dto/person.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/domain/model" 5 | "time" 6 | ) 7 | 8 | type PersonGorm struct { 9 | ID uint `gorm:"primaryKey;column:id"` 10 | SSN string `gorm:"uniqueIndex;column:ssn"` 11 | FirstName string `gorm:"column:first_name"` 12 | LastName string `gorm:"column:last_name"` 13 | Birthday time.Time `gorm:"column:birthday"` 14 | } 15 | 16 | func (p *PersonGorm) ToEntity() *model.Person { 17 | if p == nil { 18 | return nil 19 | } 20 | 21 | return &model.Person{ 22 | SSN: p.SSN, 23 | FirstName: p.FirstName, 24 | LastName: p.LastName, 25 | Birthday: model.Birthday(p.Birthday), 26 | } 27 | } -------------------------------------------------------------------------------- /pkg/client/infrastructure/redis_API_customer.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/domain/model" 7 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/infrastructure/dto" 8 | "github.com/google/uuid" 9 | "io/ioutil" 10 | "net/http" 11 | "path" 12 | ) 13 | 14 | // API 15 | 16 | type CustomerRedisAPIRepository struct { 17 | client *http.Client 18 | baseUrl string 19 | } 20 | 21 | func (r *CustomerRedisAPIRepository) GetCustomer(ctx context.Context, ID uuid.UUID) (*model.Customer, error) { 22 | resp, err := r.client.Get(path.Join(r.baseUrl, "users", ID.String())) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | data, err := ioutil.ReadAll(resp.Body) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer resp.Body.Close() 32 | 33 | var row dto.CustomerJSON 34 | err = json.Unmarshal(data, &row) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | customer, err := row.ToEntity() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return &customer, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/client/infrastructure/redis_customer.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/domain/model" 8 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/client/infrastructure/dto" 9 | "github.com/go-redis/redis/v8" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | // Репозиторий для Redis 14 | 15 | type CustomerRedisRepository struct { 16 | client *redis.Client 17 | } 18 | 19 | func (r *CustomerRedisRepository) GetCustomer(ctx context.Context, ID uuid.UUID) (*model.Customer, error) { 20 | data, err := r.client.Get(ctx, fmt.Sprintf("user-%s", ID.String())).Result() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | var row dto.CustomerJSON 26 | err = json.Unmarshal([]byte(data), &row) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | customer, err := row.ToEntity() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &customer, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/investment/domain/model/investment.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type Investment interface { 9 | Amount() value_objects.Money 10 | } 11 | 12 | type EtfInvestment struct { 13 | ID uuid.UUID 14 | EtfID uuid.UUID 15 | InvestedAmount value_objects.Money 16 | BankAccountID uuid.UUID 17 | } 18 | 19 | func (e EtfInvestment) Amount() value_objects.Money { 20 | return e.InvestedAmount 21 | } 22 | 23 | type StockInvestment struct { 24 | ID uuid.UUID 25 | CompanyID uuid.UUID 26 | InvestedAmount value_objects.Money 27 | BankAccountID uuid.UUID 28 | } 29 | 30 | func (s StockInvestment) Amount() value_objects.Money { 31 | return s.InvestedAmount 32 | } 33 | 34 | type CryptoInvestment struct { 35 | ID uuid.UUID 36 | CryptoCurrencyID uuid.UUID 37 | InvestedMoney value_objects.Money 38 | BankAccountID uuid.UUID 39 | } 40 | 41 | func (c CryptoInvestment) Amount() value_objects.Money { 42 | return c.InvestedMoney 43 | } 44 | 45 | type InvestmentSpecification interface { 46 | Amount() value_objects.Money 47 | BankAccountID() uuid.UUID 48 | TargetID() uuid.UUID 49 | } 50 | 51 | type InvestmentFactory interface { 52 | Create(specification InvestmentSpecification) Investment 53 | } 54 | 55 | type EtfInvestmentFactory struct{} 56 | 57 | func (f *EtfInvestmentFactory) Create(specification InvestmentSpecification) Investment { 58 | return EtfInvestment{ 59 | EtfID: specification.TargetID(), 60 | InvestedAmount: specification.Amount(), 61 | BankAccountID: specification.BankAccountID(), 62 | } 63 | } 64 | 65 | type StockInvestmentFactory struct{} 66 | 67 | func (f *StockInvestmentFactory) Create(specification InvestmentSpecification) Investment { 68 | return StockInvestment{ 69 | CompanyID: specification.TargetID(), 70 | InvestedAmount: specification.Amount(), 71 | BankAccountID: specification.BankAccountID(), 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/investment/infrastructure/investment.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/infrastructure/bankAccount/dto" 5 | "github.com/MaksimDzhangirov/PracticalDDD/pkg/investment/domain/model" 6 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type CryptoCurrencyGorm struct { 11 | UUID string `gorm:"column:uuid"` 12 | } 13 | 14 | // инфраструктурный уровень 15 | type CryptoInvestmentGorm struct { 16 | ID int `gorm:"primaryKey;column:id"` 17 | UUID string `gorm:"column:uuid"` 18 | CryptoCurrencyID int `gorm:"column:crypto_currency_id"` 19 | CryptoCurrency CryptoCurrencyGorm `gorm:"foreignKey:CryptoCurrencyID"` 20 | InvestedAmount int `gorm:"column:amount"` 21 | InvestedCurrencyID int `gorm:"column:currency_id"` 22 | Currency dto.CurrencyGorm `gorm:"foreignKey:InvestedCurrencyID"` 23 | BankAccountID int `gorm:"column:bank_account_id"` 24 | BankAccount dto.BankAccountGorm `gorm:"foreignKey:BankAccountID"` 25 | } 26 | 27 | type CryptoInvestmentDBFactory struct { 28 | } 29 | 30 | func (f *CryptoInvestmentDBFactory) ToEntity(dto CryptoInvestmentGorm) (model.CryptoInvestment, error) { 31 | id, err := uuid.Parse(dto.UUID) 32 | if err != nil { 33 | return model.CryptoInvestment{}, err 34 | } 35 | 36 | cryptoId, err := uuid.Parse(dto.CryptoCurrency.UUID) 37 | if err != nil { 38 | return model.CryptoInvestment{}, err 39 | } 40 | 41 | currencyId, err := uuid.Parse(dto.Currency.UUID) 42 | if err != nil { 43 | return model.CryptoInvestment{}, err 44 | } 45 | 46 | accountId, err := uuid.Parse(dto.BankAccount.UUID) 47 | if err != nil { 48 | return model.CryptoInvestment{}, err 49 | } 50 | 51 | return model.CryptoInvestment{ 52 | ID: id, 53 | CryptoCurrencyID: cryptoId, 54 | InvestedMoney: value_objects.NewMoney(dto.InvestedAmount, currencyId), 55 | BankAccountID: accountId, 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/loan/domain/model/loan.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | const ( 9 | LongTerm = iota 10 | ShortTerm 11 | ) 12 | 13 | type Loan struct { 14 | ID uuid.UUID 15 | Type int 16 | BankAccountID uuid.UUID 17 | Amount value_objects.Money 18 | RequiredLifeInsurance bool 19 | } 20 | 21 | type LoanFactory struct{} 22 | 23 | func (f *LoanFactory) CreateShortTermLoan(bankAccountID uuid.UUID, amount value_objects.Money) Loan { 24 | return Loan{ 25 | Type: ShortTerm, 26 | BankAccountID: bankAccountID, 27 | Amount: amount, 28 | } 29 | } 30 | 31 | func (f *LoanFactory) CreateLongTermLoan(bankAccountID uuid.UUID, amount value_objects.Money) Loan { 32 | return Loan{ 33 | Type: LongTerm, 34 | BankAccountID: bankAccountID, 35 | Amount: amount, 36 | RequiredLifeInsurance: true, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/product/domain/model/product.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/google/uuid" 4 | 5 | type MaterialType = string 6 | 7 | const Plastic = "plastic" 8 | 9 | type Product struct { 10 | ID uuid.UUID 11 | Material MaterialType 12 | IsDeliverable bool 13 | Quantity int 14 | } 15 | 16 | type ProductSpecification interface { 17 | IsValid(product Product) bool 18 | } 19 | 20 | type AndSpecification struct { 21 | specifications []ProductSpecification 22 | } 23 | 24 | func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification { 25 | return AndSpecification{ 26 | specifications: specifications, 27 | } 28 | } 29 | 30 | func (s AndSpecification) IsValid(product Product) bool { 31 | for _, specification := range s.specifications { 32 | if !specification.IsValid(product) { 33 | return false 34 | } 35 | } 36 | 37 | return true 38 | } 39 | 40 | type OrSpecification struct { 41 | specifications []ProductSpecification 42 | } 43 | 44 | func NewOrSpecification(specifications ...ProductSpecification) ProductSpecification { 45 | return OrSpecification{ 46 | specifications: specifications, 47 | } 48 | } 49 | 50 | func (s OrSpecification) IsValid(product Product) bool { 51 | for _, specification := range s.specifications { 52 | if specification.IsValid(product) { 53 | return true 54 | } 55 | } 56 | 57 | return false 58 | } 59 | 60 | type NotSpecification struct { 61 | specification ProductSpecification 62 | } 63 | 64 | func NewNotSpecification(specification ProductSpecification) ProductSpecification { 65 | return NotSpecification{ 66 | specification: specification, 67 | } 68 | } 69 | 70 | func (s NotSpecification) IsValid(product Product) bool { 71 | return !s.specification.IsValid(product) 72 | } 73 | 74 | type HasAtLeast struct { 75 | pieces int 76 | } 77 | 78 | func NewHasAtLeast(pieces int) ProductSpecification { 79 | return HasAtLeast{ 80 | pieces: pieces, 81 | } 82 | } 83 | 84 | func (h HasAtLeast) IsValid(product Product) bool { 85 | return product.Quantity >= h.pieces 86 | } 87 | 88 | func IsPlastic(product Product) bool { 89 | return product.Material == Plastic 90 | } 91 | 92 | func IsDeliverable(product Product) bool { 93 | return product.IsDeliverable 94 | } 95 | 96 | type FunctionSpecification func(product Product) bool 97 | 98 | func (fs FunctionSpecification) IsValid(product Product) bool { 99 | return fs(product) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/product/infrastructure/create/product.go: -------------------------------------------------------------------------------- 1 | package create 2 | 3 | import "github.com/MaksimDzhangirov/PracticalDDD/pkg/product/domain/model" 4 | 5 | type ProductSpecification interface { 6 | Create(product model.Product) model.Product 7 | } 8 | 9 | type AndSpecification struct { 10 | specifications []ProductSpecification 11 | } 12 | 13 | func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification { 14 | return AndSpecification{ 15 | specifications: specifications, 16 | } 17 | } 18 | 19 | func (s AndSpecification) Create(product model.Product) model.Product { 20 | for _, specification := range s.specifications { 21 | product = specification.Create(product) 22 | } 23 | return product 24 | } 25 | 26 | type HasAtLeast struct { 27 | pieces int 28 | } 29 | 30 | func NewHasAtLeast(pieces int) ProductSpecification { 31 | return HasAtLeast{ 32 | pieces: pieces, 33 | } 34 | } 35 | 36 | func (h HasAtLeast) Create(product model.Product) model.Product { 37 | product.Quantity = h.pieces 38 | return product 39 | } 40 | 41 | func IsPlastic(product model.Product) model.Product { 42 | product.Material = model.Plastic 43 | return product 44 | } 45 | 46 | func IsDeliverable(product model.Product) model.Product { 47 | product.IsDeliverable = true 48 | return product 49 | } 50 | 51 | type FunctionSpecification func(product model.Product) model.Product 52 | 53 | func (fs FunctionSpecification) Create(product model.Product) model.Product { 54 | return fs(product) 55 | } -------------------------------------------------------------------------------- /pkg/product/infrastructure/product.go: -------------------------------------------------------------------------------- 1 | package infrastructure 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type ProductSpecification interface { 9 | Query() string 10 | Value() []interface{} 11 | } 12 | 13 | type AndSpecification struct { 14 | specifications []ProductSpecification 15 | } 16 | 17 | func NewAndSpecification(specifications ...ProductSpecification) ProductSpecification { 18 | return AndSpecification{ 19 | specifications: specifications, 20 | } 21 | } 22 | 23 | func (s AndSpecification) Query() string { 24 | var queries []string 25 | for _, specification := range s.specifications { 26 | queries = append(queries, specification.Query()) 27 | } 28 | 29 | query := strings.Join(queries, " AND ") 30 | 31 | return fmt.Sprintf("(%s)", query) 32 | } 33 | 34 | func (s AndSpecification) Value() []interface{} { 35 | var values []interface{} 36 | for _, specification := range s.specifications { 37 | values = append(values, specification.Value()...) 38 | } 39 | return values 40 | } 41 | 42 | type OrSpecification struct { 43 | specifications []ProductSpecification 44 | } 45 | 46 | func NewOrSpecification(specifications ...ProductSpecification) ProductSpecification { 47 | return OrSpecification{ 48 | specifications: specifications, 49 | } 50 | } 51 | 52 | func (s OrSpecification) Query() string { 53 | var queries []string 54 | for _, specification := range s.specifications { 55 | queries = append(queries, specification.Query()) 56 | } 57 | 58 | query := strings.Join(queries, " OR ") 59 | 60 | return fmt.Sprintf("(%s)", query) 61 | } 62 | 63 | func (s OrSpecification) Value() []interface{} { 64 | var values []interface{} 65 | for _, specification := range s.specifications { 66 | values = append(values, specification.Value()...) 67 | } 68 | return values 69 | } 70 | 71 | type HasAtLeast struct { 72 | pieces int 73 | } 74 | 75 | func NewHasAtLeast(pieces int) ProductSpecification { 76 | return HasAtLeast{ 77 | pieces: pieces, 78 | } 79 | } 80 | 81 | func (h HasAtLeast) Query() string { 82 | return "quantity >= ?" 83 | } 84 | 85 | func (h HasAtLeast) Value() []interface{} { 86 | return []interface{}{h.pieces} 87 | } 88 | 89 | func IsPlastic() string { 90 | return "material = 'plastic'" 91 | } 92 | 93 | func IsDeliverable() string { 94 | return "deliverable = 1" 95 | } 96 | 97 | type FunctionSpecification func() string 98 | 99 | func (fs FunctionSpecification) Query() string { 100 | return fs() 101 | } 102 | 103 | func (fs FunctionSpecification) Value() []interface{} { 104 | return nil 105 | } -------------------------------------------------------------------------------- /services/account_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MaksimDzhangirov/PracticalDDD/domain/services" 6 | domain "github.com/MaksimDzhangirov/PracticalDDD/domain/userAccount/entity" 7 | "net/http" 8 | ) 9 | 10 | // инфраструктурный уровень 11 | type AccountAPIService struct { 12 | client *http.Client 13 | } 14 | 15 | func NewAccountService(client *http.Client) services.AccountService { 16 | return &AccountAPIService{ 17 | client: client, 18 | } 19 | } 20 | 21 | func (s *AccountAPIService) Update(account domain.Account) error { 22 | var request *http.Request 23 | // 24 | // какой-то код 25 | // 26 | response, err := s.client.Do(request) 27 | if err != nil { 28 | return err 29 | } 30 | // 31 | // какой-то код 32 | // 33 | fmt.Printf("Response code: %d", response.StatusCode) 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /services/bank_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | domain "github.com/MaksimDzhangirov/PracticalDDD/domain/bankAccount/entity" 5 | "github.com/MaksimDzhangirov/PracticalDDD/domain/exchangeRate/repository" 6 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 7 | ) 8 | 9 | type ExchangeRateService interface { 10 | IsConversionPossible (from domain.Currency, to domain.Currency) bool 11 | Convert(to domain.Currency, from value_objects.Money) (value_objects.Money, error) 12 | } 13 | 14 | type DefaultExchangeRateService struct { 15 | repository repository.ExchangeRateRepository 16 | } 17 | 18 | func NewExchangeRateService(repository repository.ExchangeRateRepository) ExchangeRateService { 19 | return &DefaultExchangeRateService{ 20 | repository: repository, 21 | } 22 | } 23 | 24 | func (s *DefaultExchangeRateService) IsConversionPossible(from domain.Currency, to domain.Currency) bool { 25 | var result bool 26 | // 27 | // какой-то код 28 | // 29 | return result 30 | } 31 | 32 | func (s *DefaultExchangeRateService) Convert(to domain.Currency, from value_objects.Money) (value_objects.Money, error) { 33 | var result value_objects.Money 34 | // 35 | // какой-то код 36 | // 37 | return result, nil 38 | } -------------------------------------------------------------------------------- /services/casino_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/domain/bonus/repository" 5 | "github.com/MaksimDzhangirov/PracticalDDD/domain/services" 6 | domain "github.com/MaksimDzhangirov/PracticalDDD/domain/userAccount/entity" 7 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 8 | ) 9 | 10 | type CasinoService struct { 11 | bonusRepository repository.BonusRepository 12 | accountService services.AccountService 13 | // 14 | // какие-нибудь другие поля 15 | // 16 | } 17 | 18 | func (s *CasinoService) Bet(account domain.Account, money value_objects.Money) error { 19 | bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money) 20 | if err != nil { 21 | return err 22 | } 23 | // 24 | // какой-то код 25 | // 26 | for _, bonus := range bonuses { 27 | err = bonus.Apply(&account) 28 | if err != nil { 29 | return err 30 | } 31 | } 32 | // 33 | // какой-то код 34 | // 35 | err = s.accountService.Update(account) 36 | if err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /services/order_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/MaksimDzhangirov/PracticalDDD/domain/order/entity" 5 | "github.com/MaksimDzhangirov/PracticalDDD/domain/order/repository" 6 | "github.com/MaksimDzhangirov/PracticalDDD/events" 7 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 8 | ) 9 | 10 | type OrderService struct { 11 | repository repository.OrderRepository 12 | publisher events.EventPublisher 13 | } 14 | 15 | func (s *OrderService) Create(order entity.Order) (*entity.Order, error) { 16 | result, err := s.repository.Create(order) 17 | if err != nil { 18 | return nil, err 19 | } 20 | // 21 | // обновляем адрес в базе данных 22 | // 23 | s.publisher.Notify(events.NewOrderCreated(result.ID())) 24 | 25 | return result, nil 26 | } 27 | 28 | func (s *OrderService) ChangeAddress(order entity.Order, address value_objects.Address) { 29 | evt := order.ChangeAddress(address) 30 | 31 | s.publisher.Notify(evt) // публикуем события только внутри объект, не хранящих состояние 32 | } -------------------------------------------------------------------------------- /services/transaction_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MaksimDzhangirov/PracticalDDD/domain/bonus/repository" 6 | "github.com/MaksimDzhangirov/PracticalDDD/domain/userAccount/entity" 7 | value_objects "github.com/MaksimDzhangirov/PracticalDDD/value-objects" 8 | ) 9 | 10 | // правильно - состояние передаётся в виде аргумента current 11 | type TransactionService struct { 12 | bonusRepository repository.BonusRepository 13 | } 14 | 15 | func (s *TransactionService) Deposit(current value_objects.Money, account entity.Account, money value_objects.Money) (value_objects.Money, error) { 16 | bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money) 17 | if err != nil { 18 | return value_objects.Money{}, err 19 | } 20 | fmt.Printf("%v", bonuses) 21 | // 22 | // какой-то код 23 | // 24 | return current.Add(money) // возвращаем новое значение, которое представляет новое состояние 25 | } 26 | -------------------------------------------------------------------------------- /value-objects/value_objects.go: -------------------------------------------------------------------------------- 1 | package value_objects 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/google/uuid" 7 | "math" 8 | "time" 9 | ) 10 | 11 | // Объект-значение в веб-сервисе 12 | //type Currency struct { 13 | // Code string 14 | // HTML int 15 | //} 16 | 17 | // Сущность в сервисе Payment 18 | type Currency struct { 19 | ID uuid.UUID 20 | Code string 21 | HTML int 22 | } 23 | 24 | type Money struct { 25 | Value float64 26 | Currency Currency 27 | } 28 | 29 | func (m Money) ToHTML() string { 30 | return fmt.Sprintf(`%.2f %d`, m.Value, m.Currency.HTML) 31 | } 32 | 33 | type Salutation string 34 | 35 | func (s Salutation) IsPerson() bool { 36 | return s != "company" 37 | } 38 | 39 | type Color struct { 40 | Red byte 41 | Green byte 42 | Blue byte 43 | } 44 | 45 | func (c Color) ToCSS() string { 46 | return fmt.Sprintf(`rgb(%d, %d, %d`, c.Red, c.Green, c.Blue) 47 | } 48 | 49 | // проверяем на равенство Объекты-значения 50 | func (c Color) EqualTo(other Color) bool { 51 | return c.Red == other.Red && c.Green == other.Green && c.Blue == other.Blue 52 | } 53 | 54 | // проверяем на равенство Объекты-значения 55 | func (m Money) EqualTo(other Money) bool { 56 | return m.Value == other.Value && m.Currency.EqualTo(other.Currency) 57 | } 58 | 59 | // проверяем на равенство Сущности 60 | func (c Currency) EqualTo(other Currency) bool { 61 | return c.ID.String() == other.ID.String() 62 | } 63 | 64 | type Address struct { 65 | Street string 66 | Number int 67 | Suffix string 68 | Postcode int 69 | } 70 | 71 | type Phone struct { 72 | CountryPrefix string 73 | AreaCode string 74 | Number string 75 | } 76 | 77 | type Coin struct { 78 | Value Money 79 | Color Color 80 | } 81 | 82 | type Colors []Color 83 | 84 | type Birthday time.Time 85 | 86 | func (b Birthday) IsYoungerThen(other time.Time) bool { 87 | return time.Time(b).After(other) 88 | } 89 | 90 | func (b Birthday) IsAdult() bool { 91 | return time.Time(b).AddDate(18, 0, 0).Before(time.Now()) 92 | } 93 | 94 | const ( 95 | Freelancer = iota 96 | Partnership 97 | LLC 98 | Corporation 99 | ) 100 | 101 | type LegalForm int 102 | 103 | func (s LegalForm) IsIndividual() bool { 104 | return s == Freelancer 105 | } 106 | 107 | func (s LegalForm) HasLimitedResponsibility() bool { 108 | return s == LLC || s == Corporation 109 | } 110 | 111 | type Customer struct { 112 | ID uuid.UUID 113 | Name string 114 | LegalForm LegalForm 115 | Date time.Time 116 | } 117 | 118 | func (c Customer) ToPerson() Person { 119 | return Person{ 120 | FullName: c.Name, 121 | Birthday: Birthday(c.Date), 122 | } 123 | } 124 | 125 | func (c Customer) ToCompany() Company { 126 | return Company{ 127 | Name: c.Name, 128 | CreationDate: c.Date, 129 | } 130 | } 131 | 132 | type Person struct { 133 | FullName string 134 | Birthday Birthday 135 | } 136 | 137 | type Company struct { 138 | Name string 139 | CreationDate time.Time 140 | } 141 | 142 | // Неправильно. Состояние изменяется внутри объекта-значения 143 | func (m Money) AddAmount(amount float64) { 144 | m.Value += amount 145 | } 146 | 147 | // Правильно. Возвращаем новый объект-значение с новым состоянием 148 | func (m Money) WithAmount(amount float64) Money { 149 | return Money{ 150 | Value: m.Value + amount, 151 | Currency: m.Currency, 152 | } 153 | } 154 | 155 | // Неправильно. Состояние изменяется внутри объекта-значения 156 | //func (m Money) Deduct(other Money) { 157 | // m.Value -= other.Value 158 | //} 159 | 160 | // Правильно. Возвращаем новый объект-значение с новым состоянием 161 | func (m Money) DeductedWith(other Money) Money { 162 | return Money{ 163 | Value: m.Value - other.Value, 164 | Currency: m.Currency, 165 | } 166 | } 167 | 168 | // Неправильно. Состояние изменяется внутри объекта-значения 169 | func (c *Color) KeepOnlyGreen() { 170 | c.Red = 0 171 | c.Blue = 0 172 | } 173 | 174 | // Правильно. Возвращаем новый объект-значение с новым состоянием 175 | func (c Color) WithOnlyGreen() Color { 176 | return Color{ 177 | Red: 0, 178 | Green: c.Green, 179 | Blue: 0, 180 | } 181 | } 182 | 183 | func NewMoney(amount int, currencyID uuid.UUID) Money { 184 | return Money{ 185 | Value: float64(amount), 186 | Currency: Currency{ 187 | ID: currencyID, 188 | }, 189 | } 190 | } 191 | 192 | func (m Money) Add(other Money) (Money, error) { 193 | if !m.Currency.EqualTo(other.Currency) { 194 | return Money{}, errors.New("currencies must be identical") 195 | } 196 | 197 | return Money{ 198 | Value: m.Value + other.Value, 199 | Currency: m.Currency, 200 | }, nil 201 | } 202 | 203 | func (m Money) Deduct(other Money) (Money, error) { 204 | if !m.Currency.EqualTo(other.Currency) { 205 | return Money{}, errors.New("currencies must be identical") 206 | } 207 | 208 | if other.Value > m.Value { 209 | return Money{}, errors.New("there is not enough amount to deduct") 210 | } 211 | return Money{ 212 | Value: m.Value - other.Value, 213 | Currency: m.Currency, 214 | }, nil 215 | } 216 | 217 | func (c Color) ToBrighter() Color { 218 | return Color{ 219 | Red: byte(math.Min(255, float64(c.Red+10))), 220 | Green: byte(math.Min(255, float64(c.Green+10))), 221 | Blue: byte(math.Min(255, float64(c.Blue+10))), 222 | } 223 | } 224 | 225 | func (c Color) ToDarker() Color { 226 | return Color{ 227 | Red: byte(math.Max(255, float64(c.Red-10))), 228 | Green: byte(math.Max(255, float64(c.Green-10))), 229 | Blue: byte(math.Max(255, float64(c.Blue-10))), 230 | } 231 | } 232 | 233 | func (c Color) Combine(other Color) Color { 234 | return Color{ 235 | Red: byte(math.Min(255, float64(c.Red+other.Red))), 236 | Green: byte(math.Min(255, float64(c.Green+other.Green))), 237 | Blue: byte(math.Min(255, float64(c.Blue+other.Blue))), 238 | } 239 | } 240 | 241 | func (c Color) IsRed() bool { 242 | return c.Red == 255 && c.Green == 0 && c.Blue == 0 243 | } 244 | 245 | func (c Color) IsYellow() bool { 246 | return c.Red == 255 && c.Green == 255 && c.Blue == 0 247 | } 248 | 249 | func (c Color) IsMagenta() bool { 250 | return c.Red == 255 && c.Green == 0 && c.Blue == 255 251 | } --------------------------------------------------------------------------------