├── .gitignore ├── docs ├── ddd │ ├── Entity.md │ ├── Module.md │ ├── Aggregate.md │ ├── Domain Event.md │ ├── Domain Service.md │ └── Value Object.md └── solid │ ├── Dependency Inversion Principle.md │ ├── Interface Segregation Principle.md │ ├── Liskov Substitution Principle.md │ ├── Single Responsibility Principle.md │ └── Open Closed Principle.md ├── go.mod ├── img └── solid_2x.png ├── main.go ├── code └── solid │ ├── single-responsibility │ └── single-responsibility.go │ └── open-closed │ └── open-closed.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /docs/ddd/Entity.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/ddd/Module.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/ddd/Aggregate.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/ddd/Domain Event.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/ddd/Domain Service.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module goavengers/go-principles 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /img/solid_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goavengers/go-principles/HEAD/img/solid_2x.png -------------------------------------------------------------------------------- /docs/solid/Dependency Inversion Principle.md: -------------------------------------------------------------------------------- 1 | ### Dependency Inversion Principle (принцип инверсии зависимостей) -------------------------------------------------------------------------------- /docs/solid/Interface Segregation Principle.md: -------------------------------------------------------------------------------- 1 | ### Interface Segregation Principle (принцип разделения интерфейса) -------------------------------------------------------------------------------- /docs/solid/Liskov Substitution Principle.md: -------------------------------------------------------------------------------- 1 | ### Liskov Substitution Principle (принцип подстановки Барбары Лисков) -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "goavengers/go-principles/code/solid/open-closed" 4 | 5 | func main() { 6 | open_closed.AnimalSoundsWrong() 7 | open_closed.AnimalSoundsTrust() 8 | } 9 | -------------------------------------------------------------------------------- /code/solid/single-responsibility/single-responsibility.go: -------------------------------------------------------------------------------- 1 | package single_responsibility 2 | 3 | type IAnimal interface { 4 | GetAnimal() string 5 | } 6 | 7 | type IAnimalStorage interface { 8 | Save(animal Animal) 9 | Get(animal Animal) 10 | } 11 | 12 | type AnimalStorage struct{} 13 | 14 | func (storage *AnimalStorage) Save(animal Animal) { 15 | // impl 16 | } 17 | 18 | func (storage *AnimalStorage) Get(animal Animal) { 19 | // impl 20 | } 21 | 22 | type Animal struct { 23 | name string 24 | } 25 | 26 | func (animal *Animal) GetName() string { 27 | return animal.name 28 | } 29 | -------------------------------------------------------------------------------- /code/solid/open-closed/open-closed.go: -------------------------------------------------------------------------------- 1 | package open_closed 2 | 3 | import "fmt" 4 | 5 | type Animal interface { 6 | MakeSound() string 7 | } 8 | 9 | type AnimalBase struct { 10 | name string 11 | } 12 | 13 | type Lion struct{} 14 | 15 | func (lion *Lion) MakeSound() string { 16 | return "roar" 17 | } 18 | 19 | type Squirrel struct{} 20 | 21 | func (squirrel *Squirrel) MakeSound() string { 22 | return "squeak" 23 | } 24 | 25 | type Snake struct{} 26 | 27 | func (snake *Snake) MakeSound() string { 28 | return "hiss" 29 | } 30 | 31 | func AnimalSoundsWrong() { 32 | animals := []AnimalBase{ 33 | AnimalBase{name: "lion"}, 34 | AnimalBase{name: "mouse"}, 35 | AnimalBase{name: "snake"}, 36 | } 37 | 38 | for _, animal := range animals { 39 | if animal.name == "lion" { 40 | fmt.Println("roar") 41 | } else if animal.name == "mouse" { 42 | fmt.Println("squeak") 43 | } else if animal.name == "snake" { 44 | fmt.Println("hiss") 45 | } 46 | } 47 | } 48 | 49 | func AnimalSoundsTrust() { 50 | animals := []Animal{ 51 | &Lion{}, 52 | &Squirrel{}, 53 | &Snake{}, 54 | } 55 | 56 | for _, animal := range animals { 57 | fmt.Println(animal.MakeSound()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /docs/solid/Single Responsibility Principle.md: -------------------------------------------------------------------------------- 1 | ### Single Responsibility Principle (принцип единственности ответственности) 2 | 3 | > «Одно поручение. Всего одно.» — Локи говорит Скурджу в фильме «Тор: Рагнарёк». 4 | 5 | На самом верхнем уровне мы декомпозируем систему на пакаджи. В соответствии с этим принципом каждый пакадж должен заниматься какой-то отдельной вещью. 6 | 7 | Дальше пакадж мы делим на структуры с набором методов. Каждая структура и связанные с ней методы несут отвественность за какую-то более специфическую задачу внутри пакаджа. 8 | 9 | Каждый метод структуры выполняет какую-то одну единственную задачу. 10 | 11 | Представим, что у нас есть стукртура реализующая интерфейс `IAnimal` 12 | 13 | ```go 14 | type IAnimal interface { 15 | GetAnimal() string 16 | SaveAnimal() 17 | } 18 | ``` 19 | 20 | ```go 21 | type Animal struct { 22 | name string 23 | } 24 | 25 | func (animal *Animal) GetAnimal() string { 26 | return animal.name 27 | } 28 | 29 | func (animal *Animal) SaveAnimal() { 30 | // impl 31 | } 32 | ``` 33 | 34 | Стуктура `Animal`, представленная здесь, описывает какое-то животное. 35 | Эта стуктура нарушает принцип единственной ответственности. 36 | Как именно нарушается этот принцип? 37 | 38 | В соответствии с принципом единственной ответственности структура должена решать лишь какую-то одну задачу. 39 | Она же решает две, занимаясь работой с хранилищем данных в методе `SaveAnimal` и манипулируя свойствами объекта в методе `GetAnimal`. 40 | 41 | Как такая структура класса может привести к проблемам? 42 | 43 | Если изменится порядок работы с хранилищем данных, используемым приложением, то придётся вносить изменения во все структуры, работающие с хранилищем. 44 | Такая архитектура не отличается гибкостью, изменения одних подсистем затрагивают другие, что напоминает эффект домино. 45 | 46 | Для того чтобы привести вышеприведённый код в соответствие с принципом единственной ответственности, создадим ещё одну стуктуру, единственной задачей которой является работа с хранилищем, в частности — сохранение в нём объектов структуры. 47 | 48 | ```go 49 | type IAnimal interface { 50 | GetAnimal() string 51 | } 52 | 53 | type IAnimalStorage interface { 54 | Save(animal Animal) 55 | Get(animal Animal) 56 | } 57 | 58 | type AnimalStorage struct {} 59 | func (storage *AnimalStorage) Save(animal Animal) { 60 | // impl 61 | } 62 | 63 | func (storage *AnimalStorage) Get(animal Animal) { 64 | // impl 65 | } 66 | 67 | type Animal struct { 68 | name string 69 | } 70 | 71 | func (animal *Animal) GetName() string { 72 | return animal.name 73 | } 74 | ``` 75 | 76 | Вот что по этому поводу говорит __Стив Фентон__: 77 | 78 | > Проектируя классы, мы должны стремиться к тому, чтобы объединять родственные компоненты, то есть такие, изменения в которых происходят по одним и тем же причинам. 79 | > Нам следует стараться разделять компоненты, изменения в которых вызывают различные причины. 80 | 81 | Правильное применение принципа единственной ответственности приводит к высокой степени связности элементов внутри модуля, то есть к тому, что задачи, решаемые внутри него, хорошо соответствуют его главной цели. 82 | 83 | Код: [Принцип единственности ответственности](./code/solid/single-responsibility/single-responsibility.go) -------------------------------------------------------------------------------- /docs/solid/Open Closed Principle.md: -------------------------------------------------------------------------------- 1 | ### Open/Closed Principle (принцип открытости/закрытости) 2 | 3 | > Система должна быть открыта для расширения и закрыта для модификации. 4 | 5 | Не будем уходить далеко и рассмотрим пример с животными, структура `Animal`: 6 | 7 | ```go 8 | type Animal struct { 9 | name string 10 | } 11 | ``` 12 | 13 | Мы хотим перебрать список животных, каждое из которых представлено объектом класса Animal, и узнать о том, какие звуки они издают. 14 | Представим, что мы решаем эту задачу с помощью функции `AnimalSounds`: 15 | 16 | ```go 17 | func AnimalSounds() { 18 | animals := []Animal{ 19 | Animal{name: "lion"}, 20 | Animal{name: "mouse"}, 21 | } 22 | 23 | for _, animal := range animals { 24 | if animal.name == "lion" { 25 | fmt.Println("roar") 26 | } else if animal.name == "mouse" { 27 | fmt.Println("squeak") 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | Самая главная проблема такой архитектуры заключается в том, что функция определяет то, какой звук издаёт то или иное животное, анализируя конкретные объекты. 34 | Функция `AnimalSounds` не соответствует принципу открытости-закрытости, так как, например, при появлении новых видов животных, нам, для того, чтобы с её помощью можно было бы узнавать звуки, издаваемые ими, придётся её изменить. 35 | 36 | 37 | Добавим в массив новый элемент: 38 | 39 | ```go 40 | func AnimalSounds() { 41 | animals := []Animal{ 42 | Animal{name: "lion"}, 43 | Animal{name: "mouse"}, 44 | 45 | // Новый 46 | Animal{name: "snake"}, 47 | } 48 | 49 | ... 50 | } 51 | ``` 52 | 53 | После этого нам придётся поменять код функции `AnimalSounds`: 54 | 55 | ```go 56 | func AnimalSounds() { 57 | ... 58 | 59 | for _, animal := range animals { 60 | if animal.name == "lion" { 61 | fmt.Println("roar") 62 | } else if animal.name == "mouse" { 63 | fmt.Println("squeak") 64 | } else if animal.name == "snake" { 65 | fmt.Println("hiss") 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | Как видите, при добавлении в массив нового животного придётся дополнять код функции. 72 | Пример это очень простой, но если подобная архитектура используется в реальном проекте, функцию придётся постоянно расширять, добавляя в неё новые выражения `if`. 73 | 74 | Как привести функцию `AnimalSounds` в соответствие с принципом открытости-закрытости? Например — так: 75 | 76 | ```go 77 | type Animal interface { 78 | MakeSound() string 79 | } 80 | 81 | type Lion struct {} 82 | func (lion *Lion) MakeSound() string { 83 | return "roar" 84 | } 85 | 86 | type Squirrel struct {} 87 | func (squirrel *Squirrel) MakeSound() string { 88 | return "squeak" 89 | } 90 | 91 | type Snake struct {} 92 | func (snake *Snake) MakeSound() string { 93 | return "hiss" 94 | } 95 | 96 | func AnimalSounds() { 97 | animals := []Animal{ 98 | &Lion{}, 99 | &Squirrel{}, 100 | &Snake{}, 101 | } 102 | 103 | for _, animal := range animals { 104 | fmt.Println(animal.MakeSound()) 105 | } 106 | } 107 | ``` 108 | 109 | Можно заметить, что у кадой структуры реализующий интерфейс `Animal` теперь есть метод `MakeSound`. 110 | При таком подходе нужно, чтобы структуры, предназначенные для описания конкретных животных, реализовывали бы интерфейс `Animal`. 111 | 112 | В результате у каждой стуктуры, описывающего животного, будет собственный метод `MakeSound`, а при переборе массива с животными в функции `AnimalSounds` достаточно будет вызвать этот метод для каждого элемента массива. 113 | 114 | Если теперь добавить в массив объект, описывающий новое животное, функцию `AnimalSounds` менять не придётся. 115 | Мы привели её в соответствие с принципом открытости-закрытости. 116 | 117 | Код: [Принцип Открытости/Закрытости](./code/solid/open-closed/open-closed.go) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Философия архитектуры ООП, SOLID-принципы, Dry, KISS и YAGNI

4 |
Вместе мы разберемся!
5 |
6 | 7 | ## Содержание 8 | 9 | - [ ] OOP (Object Oriented Programming) 10 | - [x] [SOLID](#solid) 11 | - [x] [DRY - Don’t repeat yourself](#dry) 12 | - [x] [KISS (Keep it simple, stupid!)](#kiss) 13 | - [x] [Avoid Creating a YAGNI (You aren’t going to need it)](#yagni) 14 | - [ ] LOD (Law of Demeter) 15 | - [ ] [DDD](#ddd) 16 | 17 | ## SOLID 18 | 19 | За этой аббревиатурой скрываются 5 базовых принципов ООП, предложенные __Робертом Мартином__ в его статье [«Принципы проектирования и шаблоны проектирования»](https://web.archive.org/web/20150906155800/http://www.objectmentor.com/resources/articles/Principles_and_Patterns.pdf). Следование их духу сделает код легко тестируемым, расширяемым, читаемым и поддерживаемым. 20 | 21 | Вот шпаргалка по этим принципам: 22 | 23 | - (__S__) Single Responsibility Principle (принцип единственности ответственности) 24 | - (__O__) Open/Closed Principle (принцип открытости/закрытости) 25 | - (__L__) Liskov Substitution Principle (принцип подстановки Барбары Лисков) 26 | - (__I__) Interface Segregation Principle (принцип разделения интерфейса) 27 | - (__D__) Dependency Inversion Principle (принцип инверсии зависимостей) 28 | 29 | По словам __Роберта С. Мартина__, симптомы гниющего дизайна или плохого кода: 30 | 31 | - __Жесткость__ (трудно менять код, так как простое изменение затрагивает много мест); 32 | - __Неподвижность__ (сложно разделить код на модули, которые можно использовать в других программах); 33 | - __Вязкость__ (разрабатывать и/или тестировать код довольно тяжело); 34 | - __Ненужная Сложность__ (в коде есть неиспользуемый функционал); 35 | - __Ненужная Повторяемость__ (Copy/Paste); 36 | - __Плохая Читабельность__ (трудно понять что код делает, трудно его поддерживать); 37 | - __Хрупкость__ (легко поломать функционал даже небольшими изменениями); 38 | 39 | Но это улучшение, теперь мы можем сказать что то вроде "мне не нравится это потому, что слишком сложно модифицировать", или "мне не нравится это потому, что я не могу сказать, что этот код пытается сделать", но что насчет того, чтобы вести обсуждение позитивно? 40 | 41 | Разве это не было бы здорово, если бы существовал способ описать свойства хорошего дизайна, а не только плохого и иметь возможность рассуждать объективными терминами? Для этого нам на помощь спешат принципы проектирования архитектуры и написания программного кода. 42 | 43 | Сейчас мы рассмотрим эти принципы на схематичных примерах. Обратите внимание на то, что главная цель примеров заключается в том, чтобы помочь читателю понять принципы __SOLID__, узнать, как их применять и как следовать им, проектируя приложения. Автор материала не стремился к тому, чтобы выйти на работающий код, который можно было бы использовать в реальных проектах. 44 | 45 | В golang в качестве отдельных частей у нас есть - пакаджи, структуры, методы и интерфейсы. Давайте расссмотрим SOLID в этих терминах. 46 | 47 | - [x] [Single Responsibility Principle (принцип единственности ответственности)](./docs/solid/Single%20Responsibility%20Principle.md) 48 | - [x] [Open/Closed Principle (принцип открытости/закрытости)](./docs/solid/Open%20Closed%20Principle.md) 49 | - [ ] [Liskov Substitution Principle (принцип подстановки Барбары Лисков)](./docs/solid/Liskov%20Substitution%20Principle.md) 50 | - [ ] [Interface Segregation Principle (принцип разделения интерфейса)](./docs/solid/Interface%20Segregation%20Principle.md) 51 | - [ ] [Dependency Inversion Principle (принцип инверсии зависимостей)](./docs/solid/Dependency%20Inversion%20Principle.md) 52 | 53 | ## Object Oriented Programming 54 | ## DRY - Don’t repeat yourself 55 | 56 | > Не повторяй сам себя 57 | 58 | Следование принципу программирования __«DRY»__ позволяет добиться высокой сопровождаемости проекта, простоты внесения изменений и качественного тестирования. 59 | 60 | Если код не дублируется, то для изменения логики достаточно внесения исправлений всего в одном месте и проще тестировать одну (пусть и более сложную) функцию, а не набор из десятков однотипных. Следование принципу __DRY__ всегда приводит к декомпозиции сложных алгоритмов на простые функции. А декомпозиция сложных операций на более простые (и повторно используемые) значительно упрощает понимание программного кода. Повторное использование функций, вынесенных из сложных алгоритмов, позволяет сократить время разработки и тестирования новой функциональности. 61 | 62 | Следование принципу __DRY__ приводит к модульной архитектуре приложения и к чёткому разделению ответственности за бизнес-логику между программными классами. А это — залог сопровождаемой архитектуры. Хотя чаще не __DRY__ приводит к модульности, а уже модульность, в свою очередь, обеспечивает принципиальную возможность соблюдения этого принципа в больших проектах. 63 | 64 | В рамках одного программного класса (или модуля) следовать __DRY__ и не повторяться обычно достаточно просто. Также не требует титанических усилий делать это в рамках небольших проектов, где все разработчики «владеют» всем кодом системы. А вот в больших проектах ситуация с __DRY__ несколько сложнее — повторы чаще всего появляются из-за отсутствия у разработчиков целостной картины или несогласованности действий в рамках команды. Следовать принципу __«don’t repeat yourself»__ в рамках больших проектов не так просто, как это может показаться на первый взгляд. От разработчиков требуется тщательное планирование архитектуры, а от архитектора или тимлида требуется наличие видения системы в целом и чёткая постановка задач разработчикам. 65 | 66 | В пректировании __DRY__ тоже имеет место — доступ к конкретному функционалу должен быть доступен в одном месте, унифицирован и сгруппирован по какому-либо принципу, а не «разбросан» по системе в произвольных вариациях. Этот подход пересекается с принципом единственной ответственности из пяти принципов [__SOLID__](#solid), сформулированных **Робертом Мартином**. 67 | 68 | ## KISS (Keep it simple, stupid! — Делайте вещи проще!) 69 | 70 | > Принцип программирования KISS — делайте вещи проще 71 | 72 | Большая часть программных систем необосновано перегружена практически ненужными функциями, что ухудшает удобство их использование конечными пользователями, а также усложняет их поддержку и развитие разработчиками. Следование принципу __«KISS»__ позволяет разрабатывать решения, которые просты в использовании и в сопровождении. 73 | 74 | __KISS__ — это принцип проектирования и программирования, при котором простота системы декларируется в качестве основной цели или ценности. Есть два варианта расшифровки аббревиатуры: __«keep it simple, stupid»__ и более корректный __«keep it short and simple»__. 75 | 76 | **В проектировании следование принципу __KISS__ выражается в том, что:** 77 | 78 | - не имеет смысла реализовывать дополнительные функции, которые не будут использоваться вовсе или их использование крайне маловероятно, как правило, большинству пользователей достаточно базового функционала, а усложнение только вредит удобству приложения; 79 | 80 | - не стоит перегружать интерфейс теми опциями, которые не будут нужны большинству пользователей, гораздо проще предусмотреть для них отдельный «расширенный» интерфейс (или вовсе отказаться от данного функционала); 81 | 82 | - бессмысленно делать реализацию сложной бизнес-логики, которая учитывает абсолютно все возможные варианты поведения системы, пользователя и окружающей среды, — во-первых, это просто невозможно, а во-вторых, такая фанатичность заставляет собирать «звездолёт», что чаще всего иррационально с коммерческой точки зрения. 83 | 84 | **В программировании следование принципу __KISS__ можно описать так:** 85 | 86 | - не имеет смысла беспредельно увеличивать уровень абстракции, надо уметь вовремя остановиться; 87 | - бессмысленно закладывать в проект избыточные функции «про запас», которые может быть когда-нибудь кому-либо понадобятся (тут скорее правильнее [подход согласно принципу __YAGNI__](#yagni), который рассмотрим чуть ниже); 88 | - не стоит подключать огромную библиотеку, если вам от неё нужна лишь пара функций; 89 | - декомпозиция чего-то сложного на простые составляющие — это архитектурно верный подход (тут __KISS__ перекликается с [DRY](#dry)); 90 | - абсолютная математическая точность или предельная детализация нужны не всегда — большинство систем создаются не для запуска космических шаттлов, данные можно и нужно обрабатывать с той точностью, которая достаточна для качественного решения задачи, а детализацию выдавать в нужном пользователю объёме, а не в максимально возможном объёме. 91 | 92 | Также __KISS__ имеет много общего c принципом разделения интерфейса из пяти принципов [__SOLID__](#solid), сформулированных __Робертом Мартином__. 93 | 94 | ## Avoid Creating a YAGNI (You aren’t going to need it — Вам это не понадобится) 95 | 96 | > Лучший код — это ненаписанный код. 97 | 98 | Если упрощенно, то следование данному принципу заключается в том, что возможности, которые не описаны в требованиях к системе, просто не должны реализовываться. Это позволяет вести разработку, руководствуясь экономическими критериями — Заказчик не должен оплачивать ненужные ему функции, а разработчики не должны тратить своё оплачиваемое время на реализацию того, что не требуется. 99 | 100 | Основная проблема, которую решает принцип __YAGNI__ — это устранение тяги программистов к излишней абстракции, к экспериментам _«из интереса»_ и к реализации функционала, который сейчас не нужен, но, по мнению разработчика, может либо вскоре понадобиться, либо просто будет полезен, хотя в реальности такого очень часто не происходит. 101 | 102 | «Бесплатных» функций в программных продуктах просто не бывает. Если рассматривать материальную сторону, то любые ненужные, но фактически реализованные «фичи» оплачиваются либо Заказчиком (в бюджет закладываются расходы на те функции, которые не нужны), либо Исполнителем из прибыли по проекту. И тот, и другой варианты с точки зрения бизнеса неверны. Если же говорить о нематериальных затратах, то любые «бонусные» возможности усложняют сопровождение, увеличивают вероятность ошибок и усложняют взаимодействие с продуктом, — между объёмом кодовой базы и описанными характеристиками есть прямая зависимость. Больше написанного кода — труднее сопровождать и выше вероятность появления «багов». 103 | 104 | Принципы __YAGNI__ и [__KISS__](#kiss) очень похожи, если __KISS__ нацелен на упрощение и полезен в плане работы с теми требованиями, которые имеют место быть, то __YAGNI__ более категоричен и применяется для ограждения проектов по разработке ПО от «размывания» их рамок. 105 | 106 | Подход к реализации проектов строго по ТЗ верен с нескольких ракурсов. Заказчик не должен платить за то, что ему не надо, а продукт должен быть максимально сопровождаем и его качество не должно страдать от интеграции ненужных функций. 107 | 108 | ## LOD (Law of Demeter) 109 | 110 | ## DDD 111 | 112 | - [x] [Value Object](./docs/ddd/Value%20Object.md) 113 | - [ ] [Domain Event](./docs/ddd/Value%20Object.md) 114 | - [ ] [Domain Service](./docs/ddd/Domain%20Service.md) 115 | - [ ] [Aggregate](./docs/ddd/Aggregate.md) 116 | - [ ] [Entity](./docs/ddd/Entity.md) 117 | - [ ] [Module](./docs/ddd/Module.md) 118 | 119 | -------------------------------------------------------------------------------- /docs/ddd/Value Object.md: -------------------------------------------------------------------------------- 1 | ### DDD Value Object 2 | 3 | Давайте начнем путешествие по практическому предметно-ориентированному дизайну на 4 | Голанге с самого важного паттерна - объекта-ценности. 5 | 6 | #### Просто, но красиво 7 | 8 | Объект Value - это простой шаблон на первый взгляд. 9 | Он группирует несколько атрибутов в единый блок, который обеспечивает определенное поведение. 10 | Эта единица представляет собой определенное качество или количество, которое мы можем найти в реальном мире и привязать к более сложному объекту. 11 | Он обладает определенной ценностью или характеристиками. 12 | Это может быть цвет или деньги ( подтип объекта-значения), номер телефона или любой другой небольшой объект, который предоставляет некоторую ценность, как в блоке кода ниже. 13 | 14 | ```go 15 | type Money struct { 16 | Value float64 17 | Currency Currency 18 | } 19 | 20 | func (m Money) ToHTML() string { 21 | returs fmt.Sprintf(`%.2f%s`, m.Value, m.Currency.HTML) 22 | } 23 | 24 | type Salutation string 25 | 26 | func (s Salutation) IsPerson() bool { 27 | returs s != "company" 28 | } 29 | 30 | type Color struct { 31 | Red byte 32 | Green byte 33 | Blue byte 34 | } 35 | 36 | func (c Color) ToCSS() string { 37 | return fmt.Sprintf(`rgb(%d, %d, %d)`, c.Red, c.Green, c.Blue) 38 | } 39 | 40 | type Address struct { 41 | Street string 42 | Number int 43 | Suffix string 44 | Postcode int 45 | } 46 | 47 | type Phone struct { 48 | CountryPrefix string 49 | AreaCode string 50 | Number string 51 | } 52 | ``` 53 | 54 | В Golang объекты-значения могут быть представлены как новые структуры или путем расширения какого-либо примитивного типа. 55 | В обоих случаях идея состоит в том, чтобы обеспечить дополнительное поведение, уникальное для этого отдельного значения или группы значений. 56 | Во многих случаях объект значения может предоставлять определенные методы для форматирования строк, чтобы определить, 57 | как значения должны вести себя при кодировании или декодировании JSON. 58 | Тем не менее, основная цель этих методов должна заключаться в поддержке бизнес-инвариантов, связанных с этой характеристикой или качеством в реальной жизни. 59 | 60 | #### Идентичность и равенство 61 | 62 | Объект Value не имеет идентичности, и это его критическое отличие от шаблона Entity . Шаблон сущности имеет идентификатор как описание его уникальности. Если две Сущности имеют некоторую идентичность, это означает, что мы говорим об одних и тех же объектах. Объект-значение не имеет этого идентификатора. Объект значения имеет только несколько полей, которые лучше описывают его значение. Чтобы проверить равенство между двумя объектами-значениями, нам нужно проверить равенство всех полей, как в блоке кода ниже. 63 | 64 | ```go 65 | // checking equality for value objects 66 | func (c Color) EqualTo(other Color) bool { 67 | return c.Red == other.Red && c.Green == other.Green && c.Blue == other.Blue 68 | } 69 | 70 | // checking equality for value objects 71 | func (m Money) EqualTo(other Money) bool { 72 | return m.Value == other.Value && m.Currency.EqualTo(other.Currency) 73 | } 74 | 75 | // checking equality for entities 76 | func (c Currency) EqualTo(other Currency) bool { 77 | return c.ID.String() == other.ID.String() 78 | } 79 | ``` 80 | 81 | В приведенном выше примере структуры Money и Color определили методы EqualTo, которые проверяют все свои поля. С другой стороны, Currency проверяет равенство Identities, которым в этом примере является UUID. 82 | Как вы могли заметить, объект Value также может ссылаться на некоторый объект Entity, например Money и Currency в этом случае. Он также может содержать некоторые другие объекты-значения меньшего размера, например структуру Coin, которая содержит Color и Money. Или определить срез как набор цветов. 83 | 84 | ```go 85 | type Coin struct { 86 | Value Money 87 | Color Color 88 | } 89 | 90 | type Colors []Color 91 | ``` 92 | 93 | В одном ограниченном контексте у нас могут быть десятки объектов-значений. Тем не менее, некоторые из них могут быть сущностями внутри других ограниченных контекстов. Так будет с валютой. В простом веб-сервисе, где мы хотим отобразить деньги, мы можем рассматривать валюту как объект-значение, привязанный к нашим деньгам, которые мы не планируем менять. С другой стороны, в Платежной службе мы хотим получать обновления в реальном времени с помощью некоторого API службы Exchange, где нам нужно использовать удостоверения внутри модели домена. В этом случае у нас будут разные реализации валюты на разных сервисах. 94 | 95 | ```go 96 | // value object on web service 97 | type Currency struct { 98 | Code string 99 | HTML int 100 | } 101 | 102 | // entity on payment service 103 | type Currency struct { 104 | ID uuid.UUID 105 | Code string 106 | HTML int 107 | } 108 | ``` 109 | 110 | Шаблон, который мы хотим использовать, объект-значение или сущность, зависит только от того, что этот объект представляет в ограниченном контексте. Если это многократно используемый объект, независимо хранящийся в базе данных, может изменяться и применяться ко многим другим объектам или связан с некоторой внешней сущностью, которая требуется для изменения при изменении внешнего, мы говорим о сущности. Но если объект описывает какое-то значение, принадлежит определенной сущности, является простой копией из внешней службы или не должен существовать независимо в базе данных, тогда это объект-значение. 111 | 112 | #### Явность 113 | 114 | Самая полезная особенность Value Object - это его ясность. Он обеспечивает ясность для внешнего мира в тех случаях, когда исходные типы из Golang (или любого другого языка программирования) не поддерживают конкретное поведение или поддерживаемое поведение не является интуитивно понятным. Мы можем работать с заказчиком по многим проектам, которые должны соответствовать некоторым бизнес-инвариантам, например, быть взрослым или представлять какое-либо юридическое лицо. В таких случаях допустимы более явные типы, такие как Birthday и LegalForm. 115 | 116 | ```go 117 | type Birthday time.Time 118 | 119 | func (b Birthday) IsYoungerThen(other time.Time) bool { 120 | return time.Time(b).After(other) 121 | } 122 | 123 | func (b Birthday) IsAdult() bool { 124 | return time.Time(b).AddDate(18, 0, 0).Before(time.Now()) 125 | } 126 | 127 | const ( 128 | Freelancer = iota 129 | Partnership 130 | LLC 131 | Corporation 132 | ) 133 | 134 | type LegalForm int 135 | 136 | func (s LegalForm) IsIndividual() bool { 137 | return s == Freelancer 138 | } 139 | 140 | func (s LegalForm) HasLimitedResponsability() bool { 141 | return s == LLC || s == Corporation 142 | } 143 | ``` 144 | 145 | Иногда объект-значение не нужно явно определять как часть какой-либо другой сущности или объекта-значения. Тем не менее, мы можем определить объект значения как вспомогательный объект, который обеспечивает ясность для последующего использования в коде. Так обстоит дело с Клиентом, которым может быть Лицо или Компания. В зависимости от типа клиента у нас есть разные потоки в приложении. Одним из лучших подходов может быть преобразование клиентов, чтобы с ними было легче справляться. 146 | 147 | ```go 148 | type Customer struct { 149 | ID uuid.UUID 150 | Name string 151 | LegalForm LegalForm 152 | Date time.Time 153 | } 154 | 155 | func (c Customer) ToPerson() Person { 156 | return Person{ 157 | FullName: c.Name, 158 | Birthday: c.Date, 159 | } 160 | } 161 | 162 | func (c Customer) ToCompany() Company { 163 | return Company{ 164 | Name: c.Name, 165 | CreationDate: c.Date, 166 | } 167 | } 168 | 169 | type Person struct { 170 | FullName string 171 | Birthday Birthday 172 | } 173 | 174 | type Company struct { 175 | Name string 176 | CreationDate time.Time 177 | } 178 | ``` 179 | 180 | Хотя случаи с преобразованием могут происходить в некоторых проектах, в большинстве случаев они говорят нам, что мы должны добавить эти объекты-значения как реальную часть нашей модели предметной области. Фактически, всякий раз, когда мы замечаем, что какая-то конкретная меньшая группа полей постоянно взаимодействует друг с другом, но они находятся внутри какой-то более крупной группы, это уже знак того, что мы должны сгруппировать их в объект-значение и использовать его таким же образом внутри нашей большей группы. (который теперь становится меньше). 181 | 182 | #### Неизменность 183 | 184 | Объекты-значения неизменны. Не существует единой причины, причины или другого аргумента для изменения состояния объекта-значения в течение его жизненного цикла. Иногда несколько объектов могут содержать один и тот же объект-значение (хотя это не идеальное решение). В таких ситуациях мы определенно не хотим изменять объекты-значения в неожиданных местах. Итак, всякий раз, когда мы хотим изменить внутреннее состояние объекта-значения или объединить несколько из них, нам всегда нужно вернуть новый экземпляр с новым состоянием, как в блоке кода ниже. 185 | 186 | ```go 187 | // wrong way to change the state inside value object 188 | func (m *Money) AddAmount(amount float64) { 189 | m.Amount += amount 190 | } 191 | 192 | // right way to return new value objects with new state 193 | func (m Money) WithAmount(amount float64) Money { 194 | return Money { 195 | Amount: m.Amount + amount, 196 | Currency: m.Currency, 197 | } 198 | } 199 | 200 | // wrong way to change the state inside value object 201 | func (m *Money) Deduct(other Money) { 202 | m.Amount -= other.Amount 203 | } 204 | 205 | // right way to return new value objects with new state 206 | func (m Money) DeductedWith(other Money) Money { 207 | return Money { 208 | Amount: m.Amount + other.Amount, 209 | Currency: m.Currency, 210 | } 211 | } 212 | 213 | // wrong way to change the state inside value object 214 | func (c *Color) KeppOnlyGreen() { 215 | c.Red = 0 216 | c.Bed = 0 217 | } 218 | 219 | // right way to return new value objects with new state 220 | func (c Color) WithOnlyGreen() Color { 221 | return Color { 222 | Red: 0, 223 | Green: c.Green, 224 | Blue: 0, 225 | } 226 | } 227 | ``` 228 | 229 | Во всех примерах единственный правильный способ - всегда возвращать свежие экземпляры и оставлять старые нетронутыми. Хорошая практика в Golang - всегда привязывать функции к значениям вместо ссылок на объекты значений, чтобы быть уверенным, что мы никогда не изменим внутреннее состояние. 230 | 231 | ```go 232 | func (m Money) Deduct(other Money) (Money, error) { 233 | if !m.Currency.EqualTo(other.Currency) { 234 | return Money{}, errors.New("currencies must be identical") 235 | } 236 | 237 | if other.Amount > m.Amount { 238 | return Money{}, errors.New("there is not enough amount to deduct") 239 | } 240 | 241 | return Money { 242 | Amount: m.Amount - other.Amount, 243 | Currency: m.Currency, 244 | } 245 | } 246 | ``` 247 | 248 | Эта неизменяемость означает, что мы не должны проверять объект-значение в течение всего его жизненного цикла, а только при создании, как это показано в приведенном выше примере. Когда мы хотим создать новый объект-значение, мы всегда должны выполнять проверку и возвращать ошибки, если бизнес-инварианты не выполняются, и создавать объект-значение только в том случае, если он действителен. С этого момента больше не нужно проверять объект значения. 249 | 250 | #### Богатое поведение 251 | 252 | Value Object обеспечивает множество различных вариантов поведения. Его основная цель - предоставить доступный интерфейс. Если это анемия, нам, вероятно, следует подумать о причине ее существования без каких-либо методов. Если Value Object действительно имеет смысл в каком-то конкретном месте кода, то он предоставляет огромное количество дополнительных бизнес-инвариантов, которые намного лучше описывают проблему, которую мы хотим решить. 253 | 254 | ```go 255 | func (c Color) ToBrighter() Color { 256 | return Color { 257 | Red: math.Min(255, c.Red + 10), 258 | Green: math.Min(255, c.Green + 10), 259 | Blue: math.Min(255, c.Blue + 10), 260 | } 261 | } 262 | 263 | func (c Color) ToDarker() Color { 264 | return Color { 265 | Red: math.Max(0, c.Red - 10), 266 | Green: math.Max(0, c.Green - 10), 267 | Blue: math.Max(0, c.Blue - 10), 268 | } 269 | } 270 | 271 | func (c Color) Combine(other Color) Color { 272 | return Color { 273 | Red: math.Min(255, c.Red + other.Red), 274 | Green: math.Min(255, c.Green + other.Green), 275 | Blue: math.Min(255, c.Blue + other.Blue), 276 | } 277 | } 278 | 279 | func (c Color) IsRed() bool { 280 | return c.Red == 255 && c.Green == 0 && c.Blue == 0 281 | } 282 | 283 | func (c Color) IsYellow() bool { 284 | return c.Red == 255 && c.Green == 255 && c.Blue == 0 285 | } 286 | 287 | func (c Color) IsMagenta() bool { 288 | return c.Red == 255 && c.Green == 0 && c.Blue == 255 289 | } 290 | 291 | func (c Color) ToCSS() string { 292 | return fmt.Sprintf(`rgb(%d, %d, %d)`, c.Red, c.Green, c.Blue) 293 | } 294 | ``` 295 | 296 | Декомпозиция всей модели предметной области на небольшие части, такие как объекты-значения (и сущности), делает код понятным и приближенным к бизнес-логике в реальном мире. Каждый объект-значение может описывать некоторые небольшие компоненты и поддерживать многие модели поведения, аналогичные обычным бизнес-процессам. В конце концов, это значительно упрощает весь процесс модульного тестирования и помогает охватить все крайние случаи. --------------------------------------------------------------------------------