DDD: ТАКТИЧЕСКИЙ ДИЗАЙН. ЧАСТЬ 1
1
ОБУЧЕНИЕ DDD: СОДЕРЖАНИЕ
2
| 3 |
| 8 |
| 53 |
| 59 |
| 82 |
ТАКТИЧЕСКОЕ МОДЕЛИРОВАНИЕ DDD
3
ОБЗОР ТАКТИЧЕСКОГО МОДЕЛИРОВАНИЯ
4
ОБЗОР ТАКТИЧЕСКОГО МОДЕЛИРОВАНИЯ
5
МИР DDD
6
МЫ ОСТАЕМСЯ С ГИБКОЙ СИСТЕМОЙ ПРОЕКТА
7
СУЩНОСТИ
8
ОБЗОР
9
"Когда объект выдается по идентичности, быстрее чем его атрибуты, в начале делать определение в модели. Держите определение класса простым и сосредоточенным на непрерывности жизненного цикла и идентичности. Определите средство из отличия, каждый объект на его форме или же история". (Эванс*)
Взято из: http://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf
Под лицензией GPL 4.0 - https://creativecommons.org/licenses/by/4.0/
ОБЗОР
10
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ
11
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ
12
ПОКОЛЕНИЕ СТРАТЕГИЙ
13
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: ПРЕДОСТАВЛЕННАЯ ПОЛЬЗОВАТЕЛЕМ
14
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: СОЗДАНА ПРИЛОЖЕНИЕМ
15
Что, если уникальная идентификация не является достоверной (на форме заголовок написан с ошибкой)? Потребности пользователя могут дать путь к правильной идентификации.
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: СОЗДАНА ПРИЛОЖЕНИЕМ
16
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: СОЗДАНА ПРИЛОЖЕНИЕМ
17
Пример:
-> c8e6cf04-4bf8-11e6-beb8-9e71128cae77
-> АПМ-S-2016-07-17-C8E6CF04
String rawId = java.util.UUID.randomUUID.toString();
String rawId = "APM-S-2016-07-17-C8E6CF04";
SprintId sprintId = new SprintId(rawId);
Date sprintCreationDate = sprintId.creationDate();
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: СОЗДАНА ПРИЛОЖЕНИЕМ
18
public class Sprint extends Entity { private SprintId sprintId;
public Date creationDate() {
return this.sprintId().creationDate();
}
...
}
public class HibernateSprintRepository implements
SprintRepository {
public SprintId nextIdentity() { return new
SprintId(java.util.UUID.randomUUID().toString().toU pperCase());
}
...
}
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: МЕХАНИЗМ СТОЙКОСТИ
19
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: МЕХАНИЗМ СТОЙКОСТИ
20
Поздно:
<id name="id" type="long" column="sprint_id">
<generator class="sequence">
<param name="sequence">sprint_seq</param>
</generator>
</id>
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: МЕХАНИЗМ СТОЙКОСТИ
21
Рано:
public SprintId nextIdentity() {
Long rawSprintId = (Long) this.session()
.createSQLQuery(
"select sprint_seq.nextval as sprint_id from dual")
.addScalar("sprint_id", Hibernate.LONG)
.uniqueResult();
return new SprintId(rawSprintId);
}
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: СОЗДАНО ДРУГИМ ОГРАНИЧЕННЫМ КОНТЕКСТОМ
22
23
УНИКАЛЬНАЯ ИДЕНТИФИКАЦИЯ: СОЗДАНО ДРУГИМ ОГРАНИЧЕННЫМ КОНТЕКСТОМ
Пример – Выбор спринта id из местного контекста и ещё одного из ограниченного контекста.
КОГДА СЛЕДУЕТ СОЗДАВАТЬ ИДЕНТИФИКАЦИЮ?
24
РАННЕЕ СОЗДАНИЕ ИДЕНТИФИКАЦИИ
25
ПОЗДНЕЕ СОЗДАНИЕ ИДЕНТИФИКАЦИИ
26
ПОЗДНЕЕ СОЗДАНИЕ ИДЕНТИФИКАЦИИ: ПОТЕНЦИАЛЬНЫЕ ПРОБЛЕМЫ
27
ПОЗДНЕЕ СОЗДАНИЕ ИДЕНТИФИКАЦИИ: ПРОВЕРКА ВРЕМЕНИ
28
Сравнивать атрибуты сущности:
public class Sprint extends Entity {
...
@Override
public boolean equals(Object anObject) {
boolean equalObjects = false;
if (anObject != null && this.getClass() == anObject.getClass()) {
Sprint anotherSprint = (Sprint) anObject;
equalObjects = this.productId().equals(anotherSprint.productId()) && this.tenantId().equals(anotherSprint.tenantId()) && this.begins().equals(anotherSprint.begins()) && this.ends().equals(anotherSprint.ends()) && this.name().equals(anotherSprint.name());
}
return equalObjects;
}
@Override
public int hashCode() { int hashCode =
+ (...) + this.productId().hashCode()
+ this.tenantId().hashCode() + this.begins().hashCode()
+ this.ends().hashCode() + this.name().hashCode();
return hashCode;
}
...
}
СУРРОГАТНАЯ ИДЕНТИФИКАЦИЯ
29
ПРИМЕР СУРРОГАТНОЙ ИДЕНТИФИКАЦИИ – ЧЛЕН
30
Применение слоя супертипа (Фаулер*), спрятать шаблон суррогатной идентификации в высший класс
public class IdentifiedDomainObject {
private static final long serialVersionUID = 1L; private long id;
protected IdentifiedDomainObject() { super();
this.setId(-1);
}
protected long id() {
return this.id;
}
private void setId(long anId) { this.id = anId;
}
}
ПРИМЕР СУРРОГАТНОЙ ИДЕНТИФИКАЦИИ – ЧЛЕН
31
Участник группы является частью значения-объекта:
Группа общности. Группа имеет члены группы экземпляров:
Суррогат ID используется в отдельной базе данных.
public class GroupMember extends IdentifiedDomainObject {
...
public GroupMember(TenantId aTenantId, String aName, GroupMemberType aType) {
this();
this.setName(aName); this.setTenantId(aTenantId); this.setType(aType);
}
@Override
public boolean equals(Object anObject) {
boolean equalObjects = false;
if (anObject != null && this.getClass() == anObject.getClass()) {
GroupMember typedObject = (GroupMember) anObject;
equalObjects = this.tenantId().equals(typedObject.tenantId()) && this.name().equals(typedObject.name()) && this.type().equals(typedObject.type());
}
return equalObjects;
}
}
public class Group extends ConcurrencySafeEntity { private String description;
private Set<GroupMember> groupMembers; private String name;
private TenantId ;
...
}
ПРИМЕР СУРРОГАТНОЙ ИДЕНТИЧНОСТИ
32
Настройка спящего режима в созданном пространстве
<hibernate-mapping>
<class
name="com.saasovation.identityaccess.domain.model. identity.GroupMember"
table="tbl_group_member" lazy="true">
<id name="id" type="long" column="id" unsaved-value="-1">
<generator class="native"/>
</id>
<property name="name" column="name" type="java.lang.String" update="true"
insert="true" lazy="false"
/>
...
</class>
</hibernate-mapping>
CREATE TABLE `tbl_group_member` (
`id` int(11) NOT NULL
auto_increment,
`name` varchar(100) NOT NULL,
...
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
ОТКРЫТИЕ СУБЪЕКТОВ
33
ПУТИ ОТКРЫТИЯ СУБЪЕКТОВ
34
РОЛИ И ОБЯЗАННОСТИ
35
ПРИМЕР: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ
36
Рассмотрим следующее возможные случаи:
ПРИМЕР: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ
37
Аренда?
Копать глубже:
ПРИМЕР: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ
38
Идентификация объектов:
ПРИМЕР: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ
39
РЕШЕНО: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ
40
Вариант использования — «Клиенты могут быть активными, или быть деактивированы».
Первый (неправильный - нет выраженного поведения) делать (геттер/сеттер):
public class Tenant extends Entity {
...
private boolean active;
...
public boolean getIsActive() {...} public boolean setIsActive() {...}
}
РЕШЕНО: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ. ПОВЕДЕНИЕ.
41
Лучший путь:
public class Tenant extends Entity {
...
private boolean active;
...
public boolean isActive() {...}
public boolean activate() {...}
public boolean deactivate() {...}
}
РЕШЕНО: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ. ПОВЕДЕНИЕ.
42
Напишите проверку, чувствительную к языку:
public class TenantTest {
public void can_be_activated_and_deactivated() throws Exception {
Tenant = new Tenant(...);
assertTrue(tenant.isActive());
tenant.deactivate(); assertFalse(tenant.isActive());
tenant.activate(); assertTrue(tenant.isActive());
}
}
ПРИМЕР: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ
43
Моделирование атаки:
ПРИМЕР: ОБНАРУЖЕНИЕ АТАК В БЕЗОПАСНОМ КОНТЕКСТЕ
44
Моделирование пользователя:
ОРГАНИЗАЦИЯ СБОРКИ
45
Следующая информация должна быть предоставлена во время сборки:
ОРГАНИЗАЦИЯ СБОРКИ
46
Реализация пользовательского конструктора:
public class User extends Entity {
public User(TenantId aTenantId, String aUsername, String aPassword, Person aPerson) {
this();
this.setPerson(aPerson); this.setTenantId(aTenantId); this.setUsername(aUsername); this.protectPassword("", aPassword);
...
DomainEventPublisher
.instance()
.publish(new UserRegistered(
this.tenantId(),
aUsername, aPerson.name(),
aPerson.contactInformation().emailAddress()));
}
...
}
ОРГАНИЗАЦИЯ СБОРКИ
47
Сборка пользователя – метод фабрики на совокупность посетителей:
public class Tenant extends Entity {
public User registerUser(String anInvitationIdentifier, String aUsername, String aPassword, Person aPerson) {
this.assertStateTrue(this.isActive(), "Tenant is not active.");
User = null;
if (this.isRegistrationAvailableThrough(anInvitationIdentifier)) {
aPerson.setTenantId(this.tenantId());
user = new User(
this.tenantId(), aUsername, aPassword, aPerson);
}
return user;
}
...
}
ПРОВЕРКА
48
НАСТРОЙКИ ПРОВЕРКИ (ВАЛИДАЦИИ)
49
Используйте инкапсуляцию в себе:
"Инкапсуляция в себе является спроектированными своими классами так, чтобы любой доступ к данным, даже в пределах тот же класса, идет через методы" (Фаулер*)
НАСТРОЙКИ ВАЛИДАЦИИ
50
Применение самоинкапсуляции:
public final class EmailAddress extends AssertionConcern {
...
public EmailAddress(String anAddress) {
super(); this.setAddress(anAddress);
}
public String address() { return this.address;
}
private void setAddress(String anAddress) {
this.assertArgumentNotEmpty(anAddress, "The email address is required.");
this.assertArgumentLength(anAddress, 1, 100, "Email address must be 100 characters or less."); this.assertArgumentTrue(
Pattern.matches("\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*", anAddress), "Email address format is invalid.");
this.address = anAddress;
}
}
ПРОВЕРКА ВСЕГО ОБЪЕКТА
51
Корень агрегаторов команды метода - подтверждает совокупный объекты.
ПРОВЕРКА КЛАСТЕРОВ АГРЕГАТОВ
52
ЗНАЧЕНИЯ ОБЪЕКТОВ
53
ОБЗОР
54
Взятый из: http://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf
Общий под творческий Общины Атрибуция 4.0 Международный Лицензия - https://creativecommons.org/licenses/by/4.0/
ОБЗОР
55
ХАРАКТЕРИСТИКИ ЗНАЧЕНИЙ
56
ПРИМЕР: ЗНАЧЕНИЕ ОБЪЕКТА
57
public class BusinessPriority extends ValueObject {
public BusinessPriority(BusinessPriorityRatings aBusinessPriorityRatings) {
this();
this.setRatings(aBusinessPriorityRatings);
}
public float priority(BusinessPriorityTotals aTotals) {
float costAndRisk = this.costPercentage(aTotals) + this.riskPercentage(aTotals); return this.valuePercentage(aTotals) / costAndRisk;
}
public float riskPercentage(BusinessPriorityTotals aTotals) {
return (float) 100 * this.ratings().risk() / aTotals.totalRisk();
}
public float totalValue() {
return this.ratings().benefit() + this.ratings().penalty();
}
public float valuePercentage(BusinessPriorityTotals aTotals) { return (float) 100 * this.totalValue() / aTotals.totalValue();
}
@Override
public boolean equals(Object anObject) { ... }
@Override
public int hashCode() { ... }
}
ПОСТОЯННЫЕ ЗНАЧЕНИЯ
58
АГРЕГАТЫ
59
ОБЗОР
60
ПРИМЕР: ПРОЕКТИРОВАНИЕ АГРЕГАТОВ В ПРЕДЕЛАХ ГИБКОГО ПРОЕКТА УПРАВЛЯЕМОГО КОНТЕКСТА (НА ОСНОВЕ НА ВОНА ВЕРНОН - ПРАВИЛА ДЛЯ ДИЗАЙНА АГРЕГАТА*)
61
Рассмотрены следующие варианты использования:
ИСХОДНАЯ ПОПЫТКА: БОЛЬШОЙ АГРЕГАТ
62
Основанный на на: Вона Вернон Агрегаты Дизайн Правила сочинение:
https://vaughnvernon.co/wordpress/wp-content/uploads/2014/10/DDD_COMMUNITY_ESSAY_AGGREGATES_PART_1.pdf Общий под творческий Общины Attribution-NoDerivs 3.0 Непортированный Лицензия - http://creativecommons.org/licenses/by-nd/3.0/
public class Product extends ConcurrencySafeEntity { private ProductId productId;
private String description;
private String name;
private TenantId ;
private Set<BacklogItem> backlogItems; private Set<Release> releases;
private Set<Sprint> sprints;
...
}
ИСХОДНАЯ ПОПЫТКА: БОЛЬШОЙ АГРЕГАТ
63
Основанный на на: Вона Вернон Агрегаты Дизайн Правила сочинение:
https://vaughnvernon.co/wordpress/wp-content/uploads/2014/10/DDD_COMMUNITY_ESSAY_AGGREGATES_PART_1.pdf Общий под творческий Общины Attribution-NoDerivs 3.0 Непортированный Лицензия - http://creativecommons.org/licenses/by-nd/3.0/
public class Product {
...
public void planBacklogItem(
String aSummary, String aCategory,
BacklogItemType aType, StoryPoints aStoryPoints) {
...
}
public void scheduleRelease(
String aName, String aDescription,
Date aBegins, Date anEnds) {
...
}
public void scheduleSprint(
String aName, String aGoals, Date aBegins, Date anEnds) {
...
}
...
}
БОЛЬШОЙ АГРЕГАТ. НЕДОСТАТКИ
64
Потому что общие метки границы консистенции, все сгруппированные объекты, должны быть обновлены в течение самой сделки. Если выбран оптимистичный параллелизм, большие агрегаты находятся в последовательности проблем:
РЕДИЗАЙН НАШЕГО АГРЕГАТА
65
РЕДИЗАЙН НАШЕГО АГРЕГАТА
66
Код основан на:
Под лицензий - http://creativecommons.org/licenses/by-nd/3.0/
public class Product {
public BacklogItem planBacklogItem(
BacklogItemId aNewBacklogItemId, String aSummary,
String aCategory, BacklogItemType aType, StoryPoints aStoryPoints) {
...
}
public Release scheduleRelease(
ReleaseId aNewReleaseId, String aName,
String aDescription, Date aBegins, Date anEnds) {
...
}
public Sprint scheduleSprint(
SprintId aNewSprintId, String aName, String aGoals, Date aBegins,
Date anEnds) {
...
}
...
}
РЕДИЗАЙН НАШЕГО АГРЕГАТА
67
Под лицензий - http://creativecommons.org/licenses/by-nd/3.0/
Основан на коде:
public class ProductBacklogItemService {
@Transactional
public void planProductBacklogItem(
String aTenantId, String aProductId,
String aSummary, String aCategory,
String aBacklogItemType, String aStoryPoints) {
Product = productRepository.productOfId(
new TenantId(aTenantId), new ProductId(aProductId));
BacklogItem plannedBacklogItem =
product.planBacklogItem(
aSummary, aCategory,
BacklogItemType.valueOf(aBacklogItemType),
StoryPoints.valueOf(aStoryPoints));
backlogItemRepository.add(plannedBacklogItem);
}
...
}
ПРАВИЛА ДИЗАЙНА АГРЕГАТОВ (НА ОСНОВЕ ВОНА ВЕРНОН - ПРАВИЛА АГРЕГАТОВ*)
68
Под лицензией Attribution-NoDerivs 3.0 - http://creativecommons.org/licenses/by-nd/3.0/
Основано на книгах Вона Вернон - Правила Дизайна Агрегатов:
https://vaughnvernon.co/wordpress/wp-content/uploads/2014/10/DDD_COMMUNITY_ESSAY_AGGREGATES_PART_1.pdf https://vaughnvernon.co/wordpress/wp-content/uploads/2014/10/DDD_COMMUNITY_ESSAY_AGGREGATES_PART_2.pdf
МОДЕЛЬ ИСТИННЫХ ИНВАРИАНТОВ В ПОСЛЕДОВАТЕЛЬНОСТИ ГРАНИЦ
69
ССЫЛКА НА ДРУГИЕ АГРЕГАТЫ ПО ИДЕНТИФИКАЦИИ
70
ДИЗАЙН МАЛЕНЬКОГО АГРЕГАТА
71
ИСПОЛЬЗОВАТЬ ВОЗМОЖНЫЕ ПОСЛЕДОВАТЕЛЬНОСТИ ВНЕ НАШИХ ГРАНИЦ
72
ИСПОЛЬЗОВАТЬ ВОЗМОЖНЫЕ ПОСЛЕДОВАТЕЛЬНОСТИ ВНЕ НАШИХ ГРАНИЦ
73
Пример основан на книге Вона Вернон - Правила дизайна агрегатов:
Под лицензией - http://creativecommons.org/licenses/by-nd/3.0/
Например, совершение и отставание предмета для спринта, требует обновления двух агрегатов: спринт и элемент невыполненной работы. Первый обновлятся BackLogItem. BackLogItem поднимает соответствующее событие:
public class BacklogItem extends ConcurrencySafeEntity {
...
public void commitTo(Sprint aSprint) {
...
DomainEventPublisher
.instance()
.publish(new BacklogItemCommitted( this.tenantId(), this.backlogItemId(), this.sprintId()));
}
...
}
public class BackLogItemApplicationService {
...
public void commitBacklogItemToSprint(
String aTenantId, String aBacklogItemId, String aSprintId) {
TenantId tenantId = new TenantId(aTenantId); BacklogItem backlogItem =
backlogItemRepository().backlogItemOfId( tenantId, new BacklogItemId(aBacklogItemId));
Sprint sprint = sprintRepository().sprintOfId( tenantId, new SprintId(aSprintId));
backlogItem.commitTo(sprint); this.backlogItemRepository().save(backlogItem);
...
}
}
ИСПОЛЬЗОВАТЬ ВОЗМОЖНЫЕ ПОСЛЕДОВАТЕЛЬНОСТИ ЗА ПРЕДЕЛАМИ ГРАНИЦЫ (ПРОДОЛЖЕНИЕ НАШЕГО ПРИМЕРА)
74
Пример основан на книге Вона Вернон - Правила дизайна агрегатов:
https://vaughnvernon.co/wordpress/wp-content/uploads/2014/10/DDD_COMMUNITY_ESSAY_AGGREGATES_PART_2.pdf Лицензия - http://creativecommons.org/licenses/by-nd/3.0/
Затем, событие BackLogItemCommitted обрабатывается и обновляет агрегат спринта:
public class Sprint extends ConcurrencySafeEntity {
...
public void commit(BacklogItem aBacklogItem) {
...
int ordering = this.backlogItems().size() + 1;
CommittedBacklogItem committedBacklogItem =
new CommittedBacklogItem(
this.tenantId(), this.sprintId(), aBacklogItem.backlogItemId(), ordering);
this.backlogItems().add(committedBacklogItem);
}
...
}
public class SprintApplicationService {
...
public void handle(BacklogItemCommitted backLogCommittedEvent) {
BacklogItem backlogItem =
backlogItemRepository().backlogItemOfId(
backLogCommittedEvent.tenantId(), backLogCommittedEvent.backlogItemId());
Sprint sprint =
sprintRepository().sprintOfId(
backLogCommittedEvent.tenantId(), backLogCommittedEvent.committedToSprintId());
sprint.commit(backlogItem);
this.sprintRepository().save(sprint);
}
...
}
ОПЦИИ ВОЗМОЖНЫХ ПОСЛЕДОВАТЕЛЬНОСТЕЙ
75
ОПЦИИ ВОЗМОЖНЫХ ПОСЛЕДОВАТЕЛЬНОСТЕЙ
76
ОПЦИИ ВОЗМОЖНЫХ ПОСЛЕДОВАТЕЛЬНОСТЕЙ
77
СЕРВИС ОПТИМИСТИЧЕСКОЙ СОВМЕСТНОСТИ (НЕТ БЛОКИРОВКИ СТРАТЕГИИ)
78
ДРУГИЕ РЕКОМЕНДАЦИИ
79
НАРУШЕНИЕ ПРАВИЛ
80
НАРУШЕНИЕ ПРАВИЛ
81
Если в самом деле, в самом деле нужно – здесь есть причины:
ФАБРИКИ
82
ОБЗОР
83
Взято из: http://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf
Под лицензией GPL 4.0 - https://creativecommons.org/licenses/by/4.0/
РАСПОЛОЖЕНИЕ МЕТОДА ФАБРИКИ
84
АГРЕГАТ, КАК ФАБРИКА
85
АГРЕГАТ, КАК ФАБРИКА
86
Пример основан на: https://github.com/VaughnVernon/IDDD_Samples/blob/master/iddd_collaboration/src/main/java/com/saasovation/collaboration/domain/model/forum/Forum.java
public class Forum extends Entity {
...
public Discussion startDiscussionFor( String aSubject,
Author anAuthor,
ForumIdentityService aForumIdentityService) {
if (this.isClosed()) {
throw new IllegalStateException("Forum is closed.");
}
Discussion =
new Discussion(
this.tenant(), this.forumId(),
aForumIdentityService.nextDiscussionId(), anAuthor,
aSubject);
return discussion;
}
...
}
МЕТОД ФАБРИКИ НА ТЕХНИЧЕСКИЙ ДОМЕН СЕРВИСА
87
public class TranslatingCollaboratorService implements CollaboratorService {
...
@Override
public Author authorFrom(Tenant aTenant, String anIdentity) {
Author author =
this.userInRoleAdapter()
.toCollaborator(
aTenant,
anIdentity, "Author", Author.class);
return author;
}
...
}
ВЫВОДЫ
88