Проектирование Сервисного Слоя и Логики Приложения

Эта статья посвящена вопросам управления Логикой Приложения и проектированию Сервисного Слоя (Service Layer), Use Case, CQRS, Event Sourcing, MVC и др.

Содержание

Виды логики

Прежде чем копнуть вглубь, было бы неплохо разобраться с тем, что такое Логика Приложения (Application Logic) и чем она отличается от Бизнес-Логики (Business Logic).

Layered Architecture

Одно из наиболее часто-цитируемых определений основных концептуальных слоев дает Eric Evans:

User Interface (or Presentation Layer)
Responsible for showing information to the user and interpreting the user’s commands. The external actor might sometimes be another computer system rather than a human user.
Application Layer
Defines the jobs the software is supposed to do and directs the expressive domain objects to work out problems. The tasks this layer is responsible for are meaningful to the business or necessary for interaction with the application layers of other systems. This layer is kept thin. It does not contain business rules or knowledge, but only coordinates tasks and delegates work to collaborations of domain objects in the next layer down. It does not have state reflecting the business situation, but it can have state that reflects the progress of a task for the user or the program.
Domain Layer (or Model Layer)
Responsible for representing concepts of the business, information about the business situation, and business rules. State that reflects the business situation is controlled and used here, even though the technical details of storing it are delegated to the infrastructure. This layer is the heart of business software.
Infrastructure Layer
Provides generic technical capabilities that support the higher layers: message sending for the application, persistence for the domain, drawing widgets for the UI, and so on. The infrastructure layer may also support the pattern of interactions between the four layers through an architectural framework.

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” [4] by Eric Evans

Ward Cunningham дает следующие определения:

Factor your application classes into four layers in the following way (see Figure 1: FourLayerArchitecture):

The View layer. This is the layer where the physical window and widget objects live. It may also contain Controller classes as in classical MVC. Any new user interface widgets developed for this application are put in this layer. In most cases today this layer is completely generated by a window-builder tool.

The ApplicationModel layer. This layer mediates between the various user interface components on a GUI screen and translates the messages that they understand into messages understood by the objects in the domain model. It is responsible for the flow of the application and controls navigation from window to window. This layer is often partially generated by a window-builder and partially coded by the developer.

The DomainModel layer. This is the layer where most objects found in an OO analysis and design will reside. Examples of the types of objects found in this layer may be Orders, Employees, Sensors, or whatever is appropriate to the problem domain.

The Infrastructure layer. This is where the objects that represent connections to entities outside the application (specifically those outside the object world) reside. Examples of objects in this layer would include SQLTables, 3270Terminals, SerialPorts, SQLBrokers and the like.

- Four Layer Architecture, Ward Cunningham

Но что означает сам термин Бизнес (Business)? Непонимание этого термина часто приводит к серьезным проблемам проектирования. В это трудно поверить, но большинство разработчиков, даже с многолетним стажем, этого не понимают, и полагают что это что-то связанное с финансами.

Что такое Бизнес-Логика (Business Logic)?

Самое авторитетное пояснение термина Business можно найти, как обычно, на сайте Ward Cunningham:

Software intersects with the Real World. Imagine that.

Там же можно найти и определение термина Business Rule:

A Business Rule (in a programming context) is knowledge that gets applied to a set of data to create new value. Or it may be a rule about how to create, modify, or remove data. Or perhaps it is a rule that specifies when certain processes occur.

For example, we have a rule about email addresses – when the Driver Name field on our object identifier changes, we erase the email address. When we receive a new email address, we make sure that it contains an “@” sign and a valid domain not on our blacklist.

Business Logic Definition:

Business logic is that portion of an enterprise system which determines how data is:

  • Transformed and/or calculated. For example, business logic determines how a tax total is calculated from invoice line items.
  • Routed to people or software systems, aka workflow.

Следует отличать термин Business (по сути - синоним слова Domain) от термина Business Domain:

A category about the business domain, such as accounting, finance, inventory, marketing, tracking, billing, reporting, charting, taxes, etc.

Также следует отличать Business и от Business Process:

A Business Process is some reproduceable process within an organization. Often it is a something that you want to setup once and reuse over and over again.

Companies spend a lot of time and money identifying Business Processes, designing the software that captures a Business Process and then testing and documenting these processes.

One example of a Business Process is “Take an order on my web site”. It might involve a customer, items from a catalog and a credit card. Each of these things is represented by business objects and together they represent a Business Process.

Википедия дает следующее определение термину Business Logic:

In computer software, business logic or domain logic is the part of the program that encodes the real-world Business Rules that determine how data can be created, stored, and changed. It is contrasted with the remainder of the software that might be concerned with lower-level details of managing a database or displaying the user interface, system infrastructure, or generally connecting various parts of the program.

И поясняет, чем отличается Business Logic от Business Rules:

Business logic should be distinguished from business rules.[“Definition of business logic“] Business logic is the portion of an enterprise system which determines how data is transformed or calculated, and how it is routed to people or software (workflow). Business rules are formal expressions of business policy. Anything that is a process or procedure is business logic, and anything that is neither a process nor a procedure is a business rule. Welcoming a new visitor is a process (workflow) consisting of steps to be taken, whereas saying every new visitor must be welcomed is a business rule. Further, business logic is procedural whereas business rules are declarative.[William Ulrich. “OMG Business Rules Symposium” (архив оригинала от 2013-12-24)]

Craig Larman считает термин Business синонимом к термину Domain, и в книге “Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development” он многократно приводит их рядом, дополняя один термин другим, взятым в скобки. Термину Business Rules он дает следующее определение:

Business Rules - Business rules (also called Domain Rules) typically describe requirements or policies that transcend one software project - they are required in the domain or business, and many applications may need to conform to them. An excellent example is government tax laws. Domain rule details may be recorded in the Supplementary Specification, but because they are usually more enduring and applicable than for one software project, placing them in a central Business Rules artifact (shared by all analysts of the company) makes for better reuse of the analysis effort.

<...>

The Business Rules (or Domain Rules) capture long-living and spanning rules or policies, such as tax laws, that transcend one particular application.

<...>

Domain rules [Ross97, GK00] dictate how a domain or business may operate. They are not requirements of any one application, although an application’s requirements are often influenced by domain rules. Company policies, physical laws (such as how oil flows underground), and government laws are common domain rules.

They are commonly called business rules, which is the most common type, but that term is poor, as many software applications are for non-business problems, such as weather simulation or military logistics. A weather simulation has “domain rules,” related to physical laws and relationships, that influence the application requirements.

It’s useful to identify and record domain rules in a separate application-independent artifact - what the UP calls the Business Rules artifact - so that this analysis can be shared and reused across the organization and across projects, rather than buried within a project-specific document.

—“Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development” by Craig Larman

Резюмируя, я обобщу все своими словами:

Бизнес-Логика (деловые регламенты, доменные модели) -
это моделирование объектов и процессов предметной области (т.е. реального мира). Это то, что программа должна делать (от слова “дело” - именно так переводится слово “business”), и ради чего она создается.
Логика приложения -
это то, что обеспечивает и координирует работу Бизнес-Логики.

Подвиды Бизнес-Правил (Business Rules)

Robert Martin в “Clean Architecture” подразделяет Бизнес-Правила на два вида:

  • Application-specific Business Rules

  • Application-independent Business Rules

    То есть систему можно разделить на горизонтальные уровни: пользовательский интерфейс, Бизнес-Правила, характерные для приложения, Бизнес-Правила, не зависящие от приложения, и база данных — кроме всего прочего.

    Thus we find the system divided into decoupled horizontal layers—the UI, application-specific Business Rules, application-independent Business Rules, and the database, just to mention a few.

    - “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin

Главы 16, 20 и 22 of Clean Architecture разъясняют в подробностях типы Бизнес-Правил.

При этом, Robert Martin выводит свои 4 слоя: Entities, Use Cases, Interface Adapters, Frameworks and Drivers.

Нужно отметить, что Robert Martin под “Business Rules” понимает не только правила, но и процедуры, смывая грань между “Business Rules” и “Business Logic”:

Строго говоря, бизнес-правила — это правила или процедуры, делающие или экономящие деньги.

Strictly speaking, business rules are rules or procedures that make or save the business money.

- “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin, Chapter 20 “Business Rules”

При этом, у него можно наблюдать небольшое противоречие. С одной стороны, вся суть “Business Rules” у него сводится к тому, что они относятся исключительно к реальному миру:

Строго говоря, бизнес-правила — это правила или процедуры, делающие или экономящие деньги. Еще строже говоря, бизнес-правила — это правила, делающие или экономящие деньги независимо от наличия или отсутствия их реализации на компьютере. Они делают или экономят деньги, даже когда выполняются вручную.

Банк взимает N% за кредит — это бизнес-правило, которое приносит банку деньги. И неважно, имеется ли компьютерная программа, вычисляющая процент, или служащий вычисляет его на счетах.

Strictly speaking, business rules are rules or procedures that make or save the business money. Very strictly speaking, these rules would make or save the business money, irrespective of whether they were implemented on a computer. They would make or save money even if they were executed manually.

The fact that a bank charges N% interest for a loan is a business rule that makes the bank money. It doesn’t matter if a computer program calculates the interest, or if a clerk with an abacus calculates the interest.

- “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin, Chapter 20 “Business Rules”

Далее Robert Martin говорит важную информацию - “Business Rules” являются причиной существования Приложения. Из этого следует, что Приложение уже не может являться причиной существования “Business Rules”:

Бизнес-правила являются причиной существования программной системы. Они составляют основу функционирования. Они порождают код, который делает или экономит деньги. Они — наши семейные реликвии.

Бизнес-правила должны оставаться в неприкосновенности, незапятнанными низкоуровневыми аспектами, такими как пользовательский интерфейс или база данных. В идеале код, представляющий бизнес-правила, должен быть сердцем системы, а другие задачи — просто подключаться к ним. Реализация бизнес-правил должна быть самым независимым кодом в системе, готовым к многократному использованию.

Business rules are the reason a software system exists. They are the core functionality. They carry the code that makes, or saves, money. They are the family jewels.

The business rules should remain pristine, unsullied by baser concerns such as the user interface or database used. Ideally, the code that represents the business rules should be the heart of the system, with lesser concerns being plugged in to them. The business rules should be the most independent and reusable code in the system.

- “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin, Chapter 20 “Business Rules”

Однако, с другой стороны, он допускает существование “Business Rules” в контексте функционирования приложения:

Не все бизнес-правила так же чисты, как сущности. Некоторые бизнес-правила делают или экономят деньги, определяя и ограничивая деятельность автоматизированной системы. Эти правила не могут выполняться вручную, потому что имеют смысл только как часть автоматизированной системы.

Not all business rules are as pure as Entities. Some business rules make or save money for the business by defining and constraining the way that an automated system operates. These rules would not be used in a manual environment, because they make sense only as part of an automated system.

- “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin, Chapter 20 “Business Rules”

Не совсем понятно - “Business Rules” являются причиной существования Приложения, или имеют имеют смысл только как часть Приложения? “Business rules are the reason a software system exists” или “they make sense only as part of an automated system”?

Тут просматривается небольшое взаимоисключение, и это именно та причина, по которой я придерживаюсь формулировки Eric Evans - “Application Layer does not contain business rules”. Тут нужно добавить, что в силу “DDD Trilemma”, доменная логика все-таки может просачиваться на уровень логики приложения, см. вариант “Domain model purity + Performance” (“Split the decision-making process between the domain layer and controllers ”).

Понятно, что здесь не хватает термина для выражения различных явлений, и Robert Martin решает дифференцировать уже существующий термин “Business Rules”, разделив его на два уровня - “Critical Business Rules” и “Application-specific Business Rules”:

Вариант использования описывает способ использования автоматизированной системы. Он определяет, что должен ввести пользователь, что должно быть выведено в ответ и какие действия должны быть выполнены для получения выводимой информации. В отличие от критических бизнес-правил внутри сущностей, вариант использования описывает бизнес-правила, характерные для конкретного приложения.

A use case is a description of the way that an automated system is used. It specifies the input to be provided by the user, the output to be returned to the user, and the processing steps involved in producing that output. A use case describes application-specific business rules as opposed to the Critical Business Rules within the Entities.

- “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin, Chapter 20 “Business Rules”

Но далее он сводит обязанности Use Case к обязанностям Application Logic, и подчеркивает, что Use Case координирует “Critical Business Rules”, реализованных в виде Entities:

Варианты использования определяют, как и когда вызываются критические бизнес-правила в сущности. Варианты использования управляют действиями сущности.

<...>

Почему сущности относятся к высокому уровню, а варианты использования к низкому? Потому что варианты использования характерны для единственного приложения и, соответственно, ближе к вводу и выводу системы. Сущности — это обобщения, которые можно использовать в множестве разных приложений, соответственно, они дальше от ввода и вывода системы. Варианты использования зависят от сущностей; сущности не зависят от вариантов использования.

Use cases contain the rules that specify how and when the Critical Business Rules within the Entities are invoked. Use cases control the dance of the Entities.

<...>

Why are Entities high level and use cases lower level? Because use cases are specific to a single application and, therefore, are closer to the inputs and outputs of that system. Entities are generalizations that can be used in many different applications, so they are farther from the inputs and outputs of the system. Use cases depend on Entities; Entities do not depend on use cases.

- “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin, Chapter 20 “Business Rules”

Хотя, Robert Martin выделяет отдельную категорию классов UseCase (Interactor) для Application-specific Business Rules, на практике этот уровень часто “округляется” до уровня Application Logic. Так, например, Martin Fowler и Randy Stafford разделяют “Business Logic” на два вида - Логика Домена (Domain Logic) и Логика Приложения (Application Logic):

Подобно сценарию транзакции (Transaction Script, 133) и модели предметной области (Domain Model, 140), слой служб представляет собой типовое решение по организации бизнес-логики. Многие проектировщики, и я в том числе, любят разносить бизнес-логику по двум категориям: логика домена (domain logic) имеет дело только с предметной областью как таковой (примером могут служить стратегии вычисления зачтенного дохода по контракту), а логика приложения (application logic) описывает сферу ответственности приложения [11] (скажем, уведомляет пользователей и сторонние приложения о протекании процесса вычисления доходов). Логику приложения часто называют также “логикой рабочего процесса”, несмотря на то что под “рабочим процессом” часто понимаются совершенно разные вещи.

Like Transaction Script (110) and Domain Model (116), Service Layer is a pattern for organizing business logic. Many designers, including me, like to divide “business logic” into two kinds: “domain logic,” having to do purely with the problem domain (such as strategies for calculating revenue recognition on a contract), and “application logic,” having to do with application responsibilities [Cockburn UC] (such as notifying contract administrators, and integrated applications, of revenue recognition calculations). Application logic is sometimes referred to as “workflow logic,” although different people have different interpretations of “workflow.”

- “Patterns of Enterprise Application Architecture” [3] by Martin Fowler, Randy Stafford

Местами он склонен относить “Business Rules” к Доменой Логике (Domain Logic):

Проблемы возникли с усложнением доменой логики - бизнес-правил, алгоритмов вычислений, условий проверок и т.д.

The problem came with domain logic: business rules, validations, calculations, and the like.

- “Patterns of Enterprise Application Architecture” [3] by Martin Fowler

И даже признает наличие определенной расплывчатости:

Не стоит забывать и о том, что принято обозначать расплывчатым термином бизнес-логика. Я нахожу его забавным, поскольку могу припомнить только несколько вещей, менее логичных, нежели так называемая бизнес-логика.

Then there’s the matter of what comes under the term “business logic.” I find this a curious term because there are few things that are less logical than business logic.

- “Patterns of Enterprise Application Architecture” [3] by Martin Fowler

Почему важно отделять Business Logic от Application Logic?

Поскольку целью создания приложения является реализация именно Business Logic - критически важно обеспечить их переносимость, и отделить их от Application Logic. Эти два вида логики будут изменяться в разное время, с разной частотой и по разным причинам, поэтому их следует разделить так, чтобы их можно было изменять независимо [2] . В свое время Гради Буч сказал, что “Архитектура отражает важные проектные решения по формированию системы, где важность определяется стоимостью изменений” [2] .

Способы организации Логики Приложения (Application Logic)

Широко распространены четыре способа организации Логики Приложения (Application Logic):

  1. Оркестровый Сервис (“request/response”, т.е. сервис осведомлен об интерфейсе других сервисов), он же - Сервисный Слой (Service Layer).
  2. Хореографический Сервис (Event-Driven, т.е. loosely coupled), который является разновидностью паттерна Command, и используется, как правило, в Event-Driven Architecture (в частности, в CQRS и Event Sourcing приложениях, наглядный пример - reducer в Redux), и в DDD-приложениях (обработчик Domain/Integration Event).
  3. Front Controller и Application Controller (которые тоже, по сути, является разновидностью паттерна Command).

“A Front Controller handles all calls for a Web site, and is usually structured in two parts: a Web handler and a command hierarchy.”

- “Patterns of Enterprise Application Architecture” [3] by Martin Fowler and others.

“For both the domain commands and the view, the application controller needs a way to store something it can invoke. A Command [Gang of Four] is a good choice, since it allows it to easily get hold of and run a block of code.”

- “Patterns of Enterprise Application Architecture” [3] by Martin Fowler and others.

4. Use Case (см. также), который также, является разновидностью паттерна Command. На 15:50 Robert C. Martin проводит параллель между Use Case и паттерном Command.

Собственно говоря, производной паттерна Command является даже Method Object.

Use Case обязан своим существованием именно наличию application-specific Business Rules, которые не имеют смысла существования вне контекста приложения. Он обеспечивает их независимость от приложения путем инверсии контроля (IoC).

Если бы Use Case не содержал Бизнес-Логики, то не было бы и смысла отделять его от Page Controller, иначе приложение пыталось бы абстрагироваться от самого себя же.

Мы видим, что в организации Логики Приложения широко применяются разновидности паттерна Команда (Command).

Рассмотренные способы организовывают, в первую очередь, Логику Приложения, и лишь во вторую очередь, Бизнес-Логику, которая не обязательно должна присутствовать, кроме случая использования Use Case, т.к. иначе он утратил бы причины для существования.

При правильной организации Бизнес-Логики, и высоком качестве ORM (в случае его использования, конечно же), зависимость Бизнес-Логики от приложения будет минимальна. Основная сложность любого ORM заключается в том, чтобы организовать доступ к связанным объектам не подмешивая Логику Приложения (и логику доступа к данным) в Domain Models, - эту тему мы подробно рассмотрим в одном из следующих постов.

Понимание общих признаков в способах управления Логикой Приложения позволяет проектировать более гибкие приложения, и, как результат, более безболезненно заменять архитектурный шаблон, например, из Layered в Event-Driven. Частично эта тема затрагивается в Chapter 16 “Independence” of “Clean Architecture” by Robert C. Martin и в разделе “Premature Decomposition” of Chapter 3 “How to Model Services” of “Building Microservices” by Sam Newman.

Что такое Сервис?

SERVICE - An operation offered as an interface that stands alone in the model, with no encapsulated state.

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” [4]

In some cases, the clearest and most pragmatic design includes operations that do not conceptually belong to any object. Rather than force the issue, we can follow the natural contours of the problem space and include SERVICES explicitly in the model.

There are important domain operations that can’t find a natural home in an ENTITY or VALUE OBJECT . Some of these are intrinsically activities or actions, not things, but since our modeling paradigm is objects, we try to fit them into objects anyway...

A SERVICE is an operation offered as an interface that stands alone in the model, without encapsulating state, as ENTITIES and VALUE OBJECTS do. S ERVICES are a common pattern in technical frameworks, but they can also apply in the domain layer.

The name service emphasizes the relationship with other objects. Unlike ENTITIES and VALUE OBJECTS , it is defined purely in terms of what it can do for a client. A SERVICE tends to be named for an activity, rather than an entity—a verb rather than a noun. A SERVICE can still have an abstract, intentional definition; it just has a different flavor than the definition of an object. A SERVICE should still have a defined responsibility, and that responsibility and the interface fulfilling it should be defined as part of the domain model. Operation names should come from the UBIQUITOUS LANGUAGE or be introduced into it. Parameters and results should be domain objects.

SERVICES should be used judiciously and not allowed to strip the ENTITIES and VALUE OBJECTS of all their behavior. But when an operation is actually an important domain concept, a SERVICE forms a natural part of a MODEL-DRIVEN DESIGN . Declared in the model as a SERVICE, rather than as a phony object that doesn’t actually represent anything, the standalone operation will not mislead anyone.

A good SERVICE has three characteristics.

1. The operation relates to a domain concept that is not a natural part of an ENTITY or VALUE OBJECT . 2. The interface is defined in terms of other elements of the domain model. 3. The operation is stateless.

Statelessness here means that any client can use any instance of a particular SERVICE without regard to the instance’s individual history. The execution of a SERVICE will use information that is accessible globally, and may even change that global information (that is, it may have side effects). But the SERVICE does not hold state of its own that affects its own behavior, as most domain objects do.

When a significant process or transformation in the domain is not a natural responsibility of an ENTITY or VALUE OBJECT, add an operation to the model as a standalone interface declared as a SERVICE. Define the interface in terms of the language of the model and make sure the operation name is part of the UBIQUITOUS LANGUAGE. Make the SERVICE stateless.

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” [4]

Классификация Сервисов по уровням логики

Eric Evans разделяет Сервисы на три уровня логики:

Partitioning Services into Layers

Application

Funds Transfer App Service

  • Digests input (such as an XML request).
  • Sends message to domain service for fulfillment.
  • Listens for confirmation.
  • Decides to send notification using infrastructure service.
Domain

Funds Transfer Domain Service

  • Interacts with necessary Account and Ledger objects, making appropriate debits and credits.
  • Supplies confirmation of result (transfer allowed or not, and so on).
Infrastructure Send Notification Service
Sends e-mails, letters, and other communications as directed by the application.

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” [4]

Most SERVICES discussed in the literature are purely technical and belong in the infrastructure layer. Domain and application SERVICES collaborate with these infrastructure SERVICES. For example, a bank might have an application that sends an e-mail to a customer when an account balance falls below a specific threshold. The interface that encapsulates the e-mail system, and perhaps alternate means of notification, is a SERVICE in the infrastructure layer.

It can be harder to distinguish application SERVICES from domain SERVICES. The application layer is responsible for ordering the notification. The domain layer is responsible for determining if a threshold was met—though this task probably does not call for a SERVICE, because it would fit the responsibility of an “account” object. That banking application could be responsible for funds transfers. If a SERVICE were devised to make appropriate debits and credits for a funds transfer,that capability would belong in the domain layer. Funds transfer has a meaning in the banking domain language, and it involves fundamental business logic. Technical SERVICES should lack any business meaning at all.

Many domain or application SERVICES are built on top of the populations of ENTITIES and VALUES, behaving like scripts that organize the potential of the domain to actually get something done. ENTITIES and VALUE OBJECTS are often too fine-grained to provide a convenient access to the capabilities of the domain layer. Here we encounter a very fine line between the domain layer and the application layer. For example, if the banking application can convert and export our transactions into a spreadsheet file for us to analyze, that export is an application SERVICE. There is no meaning of “file formats” in the domain of banking, and there are no business rules involved.

On the other hand, a feature that can transfer funds from one account to another is a domain SERVICE because it embeds significant business rules (crediting and debiting the appropriate accounts, for example) and because a “funds transfer” is a meaningful banking term. In this case, the SERVICE does not do much on its own; it would ask the two Account objects to do most of the work. But to put the “transfer” operation on the Account object would be awkward, because the operation involves two accounts and some global rules.

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” [4]

Модель предметной области более предпочтительна в сравнении со сценарием транзакции, поскольку исключает возможность дублирования бизнес-логики и позволяет бороться со сложностью с помощью классических проектных решений. Но размещение логики приложения в “чистых” классах домена чревато нежелательными последствиями. Во-первых, классы домена допускают меньшую вероятность повторного использования, если они реализуют специфическую логику приложения и зависят от тех или иных прикладных инструментальных пакетов. Во-вторых, смешивание логики обеих категорий в контексте одних и тех же классов затрудняет возможность новой реализации логики приложения с помощью специфических инструментальных средств, если необходимость такого шага становится очевидной. По этим причинам слой служб предусматривает распределение “разной” логики по отдельным слоям, что обеспечивает традиционные преимущества расслоения, а также большую степень свободы применения классов домена в разных приложениях.

Domain Models (116) are preferable to Transaction Scripts (110) for avoiding domain logic duplication and for managing complexity using classical design patterns. But putting application logic into pure domain object classes has a couple of undesirable consequences. First, domain object classes are less reusable across applications if they implement application-specific logic and depend on application-specific packages. Second, commingling both kinds of logic in the same classes makes it harder to reimplement the application logic in, say, a workflow tool if that should ever become desirable. For these reasons Service Layer factors each kind of business logic into a separate layer, yielding the usual benefits of layering and rendering the pure domain object classes more reusable from application to application.

- “Patterns of Enterprise Application Architecture” [3]

Сервисы уровня Доменной Логики (Domain Logic)

Политика самого высокого уровня принадлежит Доменной Логике (Domain Logic), поэтому, с нее и начнем. К счастью, это самый немногочисленный представитель Сервисов.

Подробно тему Сервисов Логики Предметной Области и причины их существования раскрывает Vaughn Vernon:

Further, don’t confuse a Domain Service with an Application Service. We don’t want to house business logic in an Application Service, but we do want business logic housed in a Domain Service. If you are confused about the difference, compare with Application. Briefly, to differentiate the two, an Application Service, being the natural client of the domain model, would normally be the client of a Domain Service. You’ll see that demonstrated later in the chapter. Just because a Domain Service has the word service in its name does not mean that it is required to be a coarse-grained, remote-capable, heavyweight transactional operation.

...

You can use a Domain Service to

  • Perform a significant business process
  • Transform a domain object from one composition to another
  • Calculate a Value requiring input from more than one domain object

- “Implementing Domain-Driven Design” by Vaughn Vernon

Сервисы уровня Логики Приложения (Application Logic)

Это самый многочисленный представитель Сервисов. Именно его часто называют Сервисный Слой (Service Layer).

Сервисы уровня Инфраструктурного Слоя (Infrastructure Layer)

Отдельно следует выделять Сервисы уровня Инфраструктурного Слоя (Infrastructure Layer).

The infrastructure layer usually does not initiate action in the domain layer. Being “below” the domain layer, it should have no specific knowledge of the domain it is serving. Indeed, such technical capabilities are most often offered as SERVICES . For example, if an application needs to send an e-mail, some message-sending interface can be located in the infrastructure layer and the application layer elements can request the transmission of the message. This decoupling gives some extra versatility. The message-sending interface might be connected to an e-mail sender, a fax sender, or whatever else is available. But the main benefit is simplifying the application layer, keeping it narrowly focused on its job: knowing when to send a message, but not burdened with how.

The application and domain layers call on the SERVICES provided by the infrastructure layer. When the scope of a SERVICE has been well chosen and its interface well designed, the caller can remain loosely coupled and uncomplicated by the elaborate behavior the SERVICE interface encapsulates.

But not all infrastructure comes in the form of SERVICES callable from the higher layers. Some technical components are designed to directly support the basic functions of other layers (such as providing an abstract base class for all domain objects) and provide the mechanisms for them to relate (such as implementations of MVC and the like). Such an “architectural framework” has much more impact on the design of the other parts of the program.

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” [4]

Infrastructure Layer - Provides generic technical capabilities that support the higher layers: message sending for the application, persistence for the domain, drawing widgets for the UI, and so on. The infrastructure layer may also support the pattern of interactions between the four layers through an architectural framework.

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” [4]

Классификация Сервисов по способу взаимодействия

По способу взаимодействия Сервисы разделяются на Оркестровые (“request/response”, т.е. сервис осведомлен об интерфейсе других сервисов) и Хореографические (Event-Driven, т.е. loosely coupled) [8]. Их еще называют идиоматическими стилями взаимодействия. Главный недостаток первого - это высокая осведомленность об интерфейсе других Сервисов, т.е. Высокое Сопряжение (High Coupling), что снижает их реиспользование. Последний же является разновидностью паттерна Command, и используется, как правило, в Event-Driven Architecture (в частности, в CQRS и Event Sourcing приложениях, наглядный пример - reducer в Redux), и в DDD-приложениях (обработчик Domain/Integration Event).

With orchestration, we rely on a central brain to guide and drive the process, much like the conductor in an orchestra. With choreography, we inform each part of the system of its job, and let it work out the details, like dancers all finding their way and reacting to others around them in a ballet.

<...>

The downside to this orchestration approach is that the customer service can become too much of a central governing authority. It can become the hub in the middle of a web, and a central point where logic starts to live. I have seen this approach result in a small number of smart “god” services telling anemic CRUD-based services what to do.

With a choreographed approach, we could instead just have the customer service emit an event in an asynchronous manner, saying Customer created. The email service, postal service, and loyalty points bank then just subscribe to these events and react accordingly, as in Figure 4-4. This approach is significantly more decoupled. If some other service needed to reach to the creation of a customer, it just needs to subscribe to the events and do its job when needed. The downside is that the explicit view of the business process we see in Figure 4-2 is now only implicitly reflected in our system.

- “Building Microservices. Designing Fine-Grained Systems” [8] by Sam Newman

Оркестровые Сервисы

Оркестровые Сервисы являются представителями классического Сервисного Слоя, и подробнее рассматриваются ниже по тексту.

Хореографические Сервисы

Существует интересная статья “Clarified CQRS” by Udi Dahan, на которую ссылается Martin Fowler в своей статье “CQRS”.

И в этой статье есть интересный момент.

The reason you don’t see this layer explicitly represented in CQRS is that it isn’t really there...

- “Clarified CQRS” by Udi Dahan

На самом деле, обработчик команды - это и есть Сервис, только событийно-ориентированный, который следует заданному интерфейсу. Он должен содержать логику уровня приложения (а не бизнес-логику).

Our command processing objects in the various autonomous components actually make up our service layer.

- “Clarified CQRS” by Udi Dahan

Хореографические Сервисы бывают только уровня Логики Приложения, даже если они подписаны на Доменные События (Domain Event).

Частые ошибки проектирования Хореографических Сервисов

Иногда, особенно у frontend-разработчиков, можно наблюдать как они проксируют Оркестровыми Сервисами обращения к Хореографическим Сервисам. Часто это происходит при использовании Redux/NgRx в Angular-приложении, в котором широко используются Сервисы. Имея слабо-сопряженные (Low Coupling) событийно-ориентированные Сервисы в виде обработчиков команды, было бы проектной ошибкой пытаться связать их в сильно-зацепленные (High Coupling) классические Сервисы Оркестрового типа (с единственной целью - помочь Логике Приложения скрыть их от самой же себя).

Each command is independent of the other, so why should we allow the objects which handle them to depend on each other?

- “Clarified CQRS” by Udi Dahan

Тут, правда, возникает вопрос осведомленности обработчиков команды и самого приложения об интерфейсе конкретной реализации CQRS. Для выравнивания интерфейсов служит паттерн Adapter, которому, при необходимости, можно предусмотреть место.

Другой распространенной ошибкой является размещение Бизнес-Логики в Хореографических Сервисах и искусственное вырождение поведения Доменных Моделей с выносом всей бизнес-логики в обработчики команд, т.е. в Сервисы.

Это приводит к появлению проблемы, о которой говорил Eric Evans:

“Если требования архитектурной среды к распределению обязанностей таковы, что элементы, реализующие концептуальные объекты, оказываются физически разделенными, то код больше не выражает модель.

Нельзя разделять до бесконечности, у человеческого ума есть свои пределы, до которых он еще способен соединять разделенное; если среда выходит за эти пределы, разработчики предметной области теряют способность расчленять модель на осмысленные фрагменты.”

“If the framework’s partitioning conventions pull apart the elements implementing the conceptual objects, the code no longer reveals the model.

There is only so much partitioning a mind can stitch back together, and if the framework uses it all up, the domain developers lose their ability to chunk the model into meaningful pieces.”

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” by Eric Evans

В приложениях с обширной бизнес-логикой это может сильно ухудшить качество бизнес-моделирования, и препятствовать процессу дистилляции моделей по мере переработки бизнес-знаний [4]. Также такой код обретает признаки “Divergent Change” [7] и “Shotgun Surgery” [7], что сильно затрудняет исправление ошибок бизнес-моделирования и Итерационное Проектирование (Evolutionary Design). В конечном итоге это приводит к стремительному росту стоимости изменения программы.

Должен заметить, что Udi Dahan в своей статье допускает и использование Transaction Script для организации бизнес-логики. В таком случае, выбор между Transaction Script и Domain Model подробно рассмотрен в “Patterns of Enterprise Application Architecture” by M. Fowler and others. Transaction Script может быть уместным при сочетании Redux и GraphQL для минимизации сетевого трафика. При использовании же REST-API, и наличии обширной бизнес-логики, более уместным будет использование Domain Model и DDD.

Классификация Сервисов по способу обмена данными

По способу обмена данными Сервисы разделяются на Синхронные и Асинхронные.

Классификация Сервисов по состоянию

Stateless Service

Как правило, большинство сервисов являются stateless, т.е. не имеют состояния. Они хорошо изучены, и добавить по ним нечего.

Statefull Service

Классы UseCases/Interactors [2] являются разновидностью паттерна Команда (Command), и, в определенной мере, могут рассматриваться как Statefull Сервис.

Похожую идею выражает и Eric Evans:

We might like to create a Funds Transfer object to represent the two entries plus the rules and history around the transfer. But we are still left with calls to SERVICES in the interbank networks. What’s more, in most development systems, it is awkward to make a direct interface between a domain object and external resources. We can dress up such external SERVICES with a FACADE that takes inputs in terms of the model, perhaps returning a Funds Transfer object as its result. But whatever intermediaries we might have, and even though they don’t belong to us, those SERVICES are carrying out the domain responsibility of funds transfer.

- “Domain-Driven Design: Tackling Complexity in the Heart of Software” [4]

И Randy Stafford с Martin Fowler:

Двумя базовыми вариантами реализации слоя служб являются создание интерфейса доступа к домену (domain facade) и конструирование сценария операции (operation script). При использовании подхода, связанного с интерфейсом доступа к домену, слой служб реализуется как набор “тонких” интерфейсов, размещенных “поверх” модели предметной области. В классах, реализующих интерфейсы, никакая бизнес-логика отражения не находит — она сосредоточена исключительно в контексте модели предметной области. Тонкие интерфейсы устанавливают границы и определяют множество операций, посредством которых клиентские слои взаимодействуют с приложением, обнаруживая тем самым характерные свойства слоя служб.

Создавая сценарий операции, вы реализуете слой служб как множество более “толстых” классов, которые непосредственно воплощают в себе логику приложения, но за бизнес-логикой обращаются к классам домена. Операции, предоставляемые клиентам слоя служб, реализуются в виде сценариев, создаваемых группами в контексте классов, каждый из которых определяет некоторый фрагмент соответствующей логики. Подобные классы, расширяющие супертип слоя (Layer Supertype, 491) и уточняющие объявленные в нем абстрактные характеристики поведения и сферы ответственности, формируют “службы” приложения (в названиях служебных типов принято употреблять суффикс “Service”). Слой служб и заключает в себе эти прикладные классы.

The two basic implementation variations are the domain facade approach and the operation script approach. In the domain facade approach a Service Layer is implemented as a set of thin facades over a Domain Model (116). The classes implementing the facades don’t implement any business logic. Rather, the Domain Model (116) implements all of the business logic. The thin facades establish a boundary and set of operations through which client layers interact with the application, exhibiting the defining characteristics of Service Layer.

In the operation script approach a Service Layer is implemented as a set of thicker classes that directly implement application logic but delegate to encapsulated domain object classes for domain logic. The operations available to clients of a Service Layer are implemented as scripts, organized several to a class defining a subject area of related logic. Each such class forms an application “service,” and it’s common for service type names to end with “Service.” A Service Layer is comprised of these application service classes, which should extend a Layer Supertype (475), abstracting their responsibilities and common behaviors.

- “Patterns of Enterprise Application Architecture” [3] by Martin Fowler, Randy Stafford

Обратите внимание на использование термина “Domain Model”. Эти ребята - последние из числа тех, кто может спутать “Domain Model” и “DataMapper”, особенно, при таком количестве редакторов и рецензентов. Т.е. клиент ожидает от доменной модели интерфейс, который она, по какой-то причине (обычно это Single Responsibility Principle), не реализует и не должна реализовать. С другой стороны, клиент не может реализовать это поведение сам, так как это привело бы к появлению “G14: Feature Envy” [1]. Для выравнивания интерфейсов служит паттерн Adapter (aka Wrapper), см. “Design Patterns Elements of Reusable Object-Oriented Software” [6]. Отличается Statefull Services от обычного Adapter только тем, что он содержит логику более низкого уровня, т.е. Логику Приложения (Application Logic), нежели Доменная Модель.

Этот подход сильно напоминает мне “Cross-Cutting Concerns[1] с тем только отличием, что “Cross-Cutting Concerns” реализует интерфейс оригинального объекта, в то время как domain facade дополняет его. Когда объект-обертка реализует интерфейс оригинального объекта, то его обычно называют Aspect или Decorator. Часто в таких случаях можно услышать термин Proxy, но, на самом деле паттерн Proxy имеет немного другое назначение. Такой подход часто используется для того, чтобы наделить Доменную Модель логикой доступа к связанным объектам, при этом сохраняя Доменную Модель совершенно “чистой”, т.е. отделенной от поведения логики более низкого уровня.

При работе с унаследованным кодом мне доводилось встречать разбухшие Доменные Модели с огромным числом методов (я встречал до нескольких сотен методов). При анализе таких моделей часто обнаруживаются посторонние обязанности в классе, а размер класса, как известно, измеряется количеством его обязанностей. Statefull Сервисы и паттерн Adapter - хорошая альтернатива для того, чтобы вынести из модели несвойственные ей обязанности, и заставить похудеть разбухшие модели.

Назначение Сервисного Слоя

Слой служб устанавливает множество доступных действий и координирует отклик приложения на каждое действие.

A Service Layer defines an application’s boundary with a layer of services that establishes a set of available operations and coordinates the application’s response in each operation.

- “Patterns of Enterprise Application Architecture” [3]

Корпоративные приложения обычно подразумевают применение разного рода интерфейсов к хранимым данным и реализуемой логике — загрузчиков данных, интерфейсов пользователя, шлюзов интеграции и т.д. Несмотря на различия в назначении, подобные интерфейсы часто нуждаются в одних и тех же функциях взаимодействия с приложением для манипулирования данными и выполнения бизнес-логики. Функции могут быть весьма сложными и способны включать транзакции, охватывающие многочисленные ресурсы, а также операции по координации реакций на действия. Описание логики взаимодействия в каждом отдельно взятом интерфейсе сопряжено с многократным повторением одних и тех же фрагментов кода.

Слой служб определяет границы приложения и множество операций, предоставляемых им для интерфейсных клиентских слоев кода. Он инкапсулирует бизнес-логику приложения, управляет транзакциями и координирует реакции на действия.

Enterprise applications typically require different kinds of interfaces to the data they store and the logic they implement: data loaders, user interfaces, integration gateways, and others. Despite their different purposes, these interfaces often need common interactions with the application to access and manipulate its data and invoke its business logic. The interactions may be complex, involving transactions across multiple resources and the coordination of several responses to an action. Encoding the logic of the interactions separately in each interface causes a lot of duplication.

A Service Layer defines an application’s boundary and its set of available operations from the perspective of interfacing client layers. It encapsulates the application’s business logic, controlling transactions and coordinating responses in the implementation of its operations.

- “Patterns of Enterprise Application Architecture” [3]

Преимуществом использования слоя служб является возможность определения набора общих операций, доступных для применения многими категориями клиентов, и координация откликов приложения на выполнение каждой операции. В сложных случаях отклики могут включать в себя логику приложения, передаваемую в рамках атомарных транзакций с использованием нескольких ресурсов. Таким образом, если у бизнес-логики приложения есть более одной категории клиентов, а отклики на варианты использования передаются через несколько ресурсов транзакций, использование слоя служб с транзакциями, управляемыми на уровне контейнера, становится просто необходимым, даже если архитектура приложения не является распределенной.

The benefit of Service Layer is that it defines a common set of application operations available to many kinds of clients and it coordinates an application’s response in each operation. The response may involve application logic that needs to be transacted atomically across multiple transactional resources. Thus, in an application with more than one kind of client of its business logic, and complex responses in its use cases involving multiple transactional resources, it makes a lot of sense to include a Service Layer with container-managed transactions, even in an undistributed architecture.

- “Patterns of Enterprise Application Architecture” [3]

Один из общих подходов к реализации бизнес-логики состоит в расщеплении слоя предметной области на два самостоятельных слоя: “поверх” модели предметной области или модуля таблицы располагается слой служб (Service Layer, 156). Обычно это целесообразно только при использовании модели предметной области или модуля таблицы, поскольку слой домена, включающий лишь сценарий транзакции, не настолько сложен, чтобы заслужить право на создание дополнительного слоя. Логика слоя представления взаимодействует с бизнес-логикой исключительно при посредничестве слоя служб, который действует как API приложения.

Поддерживая внятный интерфейс приложения (API), слой служб подходит также для размещения логики управления транзакциями и обеспечения безопасности. Это дает возможность снабдить подобными характеристиками каждый метод слоя служб. Для таких целей обычно применяются файлы свойств, но атрибуты .NET предоставляют удобный способ описания параметров непосредственно в коде.

A common approach in handling domain logic is to split the domain layer in two. A Service Layer (133) is placed over an underlying Domain Model (116) or Table Module (125). Usually you only get this with a Domain Model (116) or Table Module (125) since a domain layer that uses only Transaction Script (110) isn’t complex enough to warrant a separate layer. The presentation logic interacts with the domain purely through the Service Layer (133), which acts as an API for the application.

As well as providing a clear API, the Service Layer (133) is also a good spot to place such things as transaction control and security. This gives you a simple model of taking each method in the Service Layer (133) and describing its transactional and security characteristics. A separate properties file is a common choice for this, but .NET’s attributes provide a nice way of doing it directly in the code.

- “Patterns of Enterprise Application Architecture” [3]

Традиционно Сервисный Слой относится к логике уровня Приложения. Т.е. Сервисный Слой имеет более низкий уровень, чем слой предметной области (domain logic), именуемый так же деловыми регламентами (business rules). Из этого также следует и то, что объекты предметной области не должны быть осведомлены о наличии Сервисного Слоя.

Кроме перечисленного выше, сервисный слой может выполнять следующие обязанности:

  • Компоновки атомарных операций (например, требуется одновременно сохранить данные в БД, редисе, и на файловой системе, в рамках одной бизнес-транзакции, или откатить все назад).
  • Сокрытия источника данных (здесь он дублирует функции паттерна Repository) и может быть опущен, если нет других причин.
  • Компоновки реиспользуемых операций уровня приложения (например, некая часть логики уровня приложения используется в нескольких различных контроллерах).
  • Как основа для реализации Интерфейса удаленного доступа.
  • Когда контроллер имеет какой-то большой метод, он нуждается в декомпозиции, и к нему применяется Extract Method для вычленения обязанностей в отдельные методы. При этом растет количество методов класса, что влечет за собой падение его сфокусированности или Связанности (т.е. коэффициент совместного использования свойств класса его методами). Чтобы восстановить связанность, эти методы выделяются в отдельный класс, образуя Method Object. И вот этот метод-объект и может быть преобразован в сервисный слой.
  • Сервисный слой можно использовать в качестве концентратора запросов, если он стоит поверх паттерна Repository и использует паттерн Query object. Дело в том, что паттерн Repository ограничивает свой интерфейс посредством интерфейса Query Object. А так как класс не должен делать предположений о своих клиентах, то накапливать предустановленные запросы в классе Repository нельзя, ибо он не может владеть потребностями всех клиентов. Клиенты должны сами заботиться о себе. А сервисный слой как раз и создан для обслуживания клиентов.

В остальных случаях логику сервисного слоя можно размещать прямо на уровне приложения (обычно - контроллер).

Когда Сервисный Слой не нужен?

Гораздо легче ответить на вопрос, когда слой служб не нужно использовать. Скорее всего, вам не понадобится слой служб, если у логики приложения есть только одна категория клиентов, например пользовательский интерфейс, отклики которого на варианты использования не охватывают несколько ресурсов транзакций. В этом случае управление транзакциями и выбор откликов можно возложить на контроллеры страниц (Page Controller, 350), которые будут обращаться непосредственно к слою источника данных. Тем не менее, как только у вас появится вторая категория клиентов или начнет использоваться второй ресурс транзакции, вам неизбежно придется ввести слой служб, что потребует полной переработки приложения.

The easier question to answer is probably when not to use it. You probably don’t need a Service Layer if your application’s business logic will only have one kind of client say, a user interface and its use case responses don’t involve multiple transactional resources. In this case your Page Controllers can manually control transactions and coordinate whatever response is required, perhaps delegating directly to the Data Source layer. But as soon as you envision a second kind of client, or a second transactional resource in use case responses, it pays to design in a Service Layer from the beginning.

- “Patterns of Enterprise Application Architecture” [3]

Тем не менее, широко распространена точка зрения, что доступ к модели должен всегда производиться через сервисный слой:

Таким образом, на вашем месте я предпочел бы самый тонкий слой служб, какой только возможен (если он вообще нужен). Обычно же я добавляю его только тогда, когда он действительно необходим. Впрочем, мне знакомы хорошие специалисты, которые всегда применяют слой служб, содержащий взвешенную долю бизнес-логики, так что этим моим советом вы можете благополучно пренебречь.

My preference is thus to have the thinnest Service Layer (133) you can, if you even need one. My usual approach is to assume that I don’t need one and only add it if it seems that the application needs it. However, I know many good designers who always use a Service Layer (133) with a fair bit of logic, so feel free to ignore me on this one.

- “Patterns of Enterprise Application Architecture” [3]

Идея вычленения слоя служб из слоя предметной области основана на подходе, предполагающем возможность отмежевания логики процесса от “чистой” бизнес-логики. Уровень служб обычно охватывает логику, которая относится к конкретному варианту использования системы или обеспечивает взаимодействие с другими инфраструктурами (например, с помощью механизма сообщений). Стоит ли иметь отдельные слои служб и предметной области — вопрос, достойный обсуждения. Я склоняюсь к мысли о том, что подобное решение может оказаться полезным, хотя и не всегда, но некоторые уважаемые мною коллеги эту точку зрения не разделяют.

The idea of splitting a services layer from a domain layer is based on a separation of workflow logic from pure domain logic. The services layer typically includes logic that’s particular to a single use case and also some communication with other infrastructures, such as messaging. Whether to have separate services and domain layers is a matter some debate. I tend to look as it as occasionally useful rather than mandatory, but designers I respect disagree with me on this.

- “Patterns of Enterprise Application Architecture” [3]

Сервис - не обертка для DataMapper

Часто Service Layer ошибочно делают как враппер над DataMapper. Это не совсем верно. Data Mapper обслуживает одну Domain Model (модель предметной области), Repository обслуживает один Aggregate [9], а Cервис обслуживает клиента (или группу клиентов). Сервисный слой может манипулировать в рамках бизнес-транзакции или в интересах клиента несколькими мапперами и другими сервисами. Поэтому методы сервиса обычно содержат имя возвращаемой Модели Домена в качестве суффикса (например, getUser()), в то время как методы Маппера и Хранилища в этом суффиксе не нуждается (так как имя МОдели Домена уже и так присутствует в имени класса Маппера, и Маппер обслуживает только одну Модель Домена).

Установить, какие операции должны быть размещены в слое служб, отнюдь не сложно. Это определяется нуждами клиентов слоя служб, первой (и наиболее важной) из которых обычно является пользовательский интерфейс.

Identifying the operations needed on a Service Layer boundary is pretty straightforward. They’re determined by the needs of Service Layer clients, the most significant (and first) of which is typically a user interface. (“Patterns of Enterprise Application Architecture” [3])

Инверсия Управления

Используйте инверсию управления, желательно в виде “Пассивного внедрения зависимостей” [1], Dependency Injection (DI).

Истинное внедрение зависимостей идет еще на один шаг вперед. Класс не предпринимает непосредственных действий по разрешению своих зависимостей; он остается абсолютно пассивным. Вместо этого он предоставляет set-методы и/или аргументы конструктора, используемые для внедрения зависимостей. В процессе конструирования контейнер DI создает экземпляры необходимых объектов (обычно по требованию) и использует аргументы конструктора или set-методы для скрепления зависимостей. Фактически используемые зависимые объекты задаются в конфигурационном файле или на программном уровне в специализированном конструирующем модуле.

True Dependency Injection goes one step further. The class takes no direct steps to resolve its dependencies; it is completely passive. Instead, it provides setter methods or constructor arguments (or both) that are used to inject the dependencies. During the con- struction process, the DI container instantiates the required objects (usually on demand) and uses the constructor arguments or setter methods provided to wire together the depen- dencies. Which dependent objects are actually used is specified through a configuration file or programmatically in a special-purpose construction module. “Clean Code: A Handbook of Agile Software Craftsmanship” [1]

Одна из основных обязанностей Сервисного Слоя - это сокрытие источника данных. Для тестирования можно использовать фиктивный Сервис (Service Stub). Этот же прием можно использовать для параллельной разработки, когда реализация сервисного слоя еще не готова. Иногда бывает полезно подменить Сервис генератором фэйковых данных. В общем, пользы от сервисного слоя будет мало, если нет возможности его подменить (или подменить используемые им зависимости).

Распространенная проблема Django-приложений

Широко распространенная ошибка - использование класса django.db.models.Manager (а то и django.db.models.Model) в качестве сервисного слоя. Нередко можно встретить, как какой-то метод класса django.db.models.Model принимает в качестве аргумента объект HTTP-запроса django.http.request.HttpRequest, например, для проверки прав.

Объект HTTP-запроса - это логика уровня приложения (application), в то время как класс модели - это логика уровня предметной области (domain), т.е. объекты реального мира, которую также называют правилами делового регламента (business rules). Проверка прав - это тоже логика уровня приложения.

Нижележащий слой не должен ничего знать о вышестоящем слое. Логика уровня домена не должна быть осведомлена о логике уровня приложения.

Классу django.db.models.Manager более всего соответствует класс Finder описанный в “Patterns of Enterprise Application Architecture” [3].

При реализации шлюза записи данных возникает вопрос: куда “пристроить” методы поиска, генерирующие экземпляр данного типового решения? Разумеется, можно воспользоваться статическими методами поиска, однако они исключают возможность полиморфизма (что могло бы пригодиться, если понадобится определить разные методы поиска для различных источников данных). В подобной ситуации часто имеет смысл создать отдельные объекты поиска, чтобы у каждой таблицы реляционной базы данных был один класс для проведения поиска и один класс шлюза для сохранения результатов этого поиска.

Иногда шлюз записи данных трудно отличить от активной записи (Active Record, 182). В этом случае следует обратить внимание на наличие какой-либо логики домена; если она есть, значит, это активная запись. Реализация шлюза записи данных должна включать в себя только логику доступа к базе данных и никакой логики домена.

With a Row Data Gateway you’re faced with the questions of where to put the find operations that generate this pattern. You can use static find methods, but they preclude polymorphism should you want to substitute different finder methods for different data sources. In this case it often makes sense to have separate finder objects so that each table in a relational database will have one finder class and one gateway class for the results.

It’s often hard to tell the difference between a Row Data Gateway and an Active Record (160). The crux of the matter is whether there’s any domain logic present; if there is, you have an Active Record (160). A Row Data Gateway should contain only database access logic and no domain logic. (Chapter 10. “Data Source Architectural Patterns : Row Data Gateway”, “Patterns of Enterprise Application Architecture” [3])

Хотя Django не использует паттерн Repository, она использует абстракцию критериев выборки, своего рода разновидность паттерна Query Object. Подобно паттерну Repository, класс модели (ActiveRecord) ограничивает свой интерфейс посредством интерфейса Query Object. Клиенты должны пользоваться предоставленным интерфейсом, а не возлагать на модель и ее менеджер свои обязанности по знанию своих запросов. А так как никакой класс не должен делать предположений о своих клиентах, то накапливать предустановленные запросы в классе модели нельзя, ибо он не может владеть потребностями всех клиентов. Клиенты должны сами заботиться о себе. А сервисный слой как раз и создан для обслуживания клиентов.

Попытки исключить Сервинсый Слой из Django-приложений приводит к появлению менеджеров с огромным количеством методов.

Хорошей практикой было бы сокрытие посредством сервисного слоя способа реализации Django Models в виде ActiveRecord. Это позволит безболезненно подменить ORM в случае необходимости.

Можно было бы поспорить и о размещении логики приложения. Думаю, некоторые предпочли бы реализовать ее в методах объектов домена, таких, как Contract. calculateRevenueRecognitions (), ИЛИ вообще В слое источника данных, ЧТО позволило бы обойтись без отдельного слоя служб. Тем не менее подобное размещение логики приложения кажется мне весьма нежелательным, и вот почему. Во-первых, классы объектов домена, которые реализуют логику, специфичную для приложения (и зависят от шлюзов и других объектов, специфичных для приложения), менее подходят для повторного использования другими приложениями. Это должны быть модели частей предметной области, представляющих интерес для данного приложения, поэтому подобные объекты вовсе не обязаны описывать возможные отклики на все варианты использования приложения. Во-вторых, инкапсуляция логики приложения на более высоком уровне (каковым не является слой источника данных) облегчает изменение реализации этого слоя, возможно, посредством некоторых специальных инструментальных средств.

Some might also argue that the application logic responsibilities could be implemented in domain object methods, such as Contract.calculateRevenueRecognitions(), or even in the data source layer, thereby eliminating the need for a separate Service Layer. However, I find those allocations of responsibility undesirable for a number of reasons. First, domain object classes are less reusable across applications if they implement application-specific logic (and depend on application-specific Gateways (466), and the like). They should model the parts of the problem domain that are of interest to the application, which doesn’t mean all of application’s use case responsibilities. Second, encapsulating application logic in a “higher” layer dedicated to that purpose (which the data source layer isn’t) facilitates changing the implementation of that layer perhaps to use a workflow engine. (“Patterns of Enterprise Application Architecture” [3])

Проблема Django-аннотаций

Я часто наблюдал такую проблему, когда в Django Model добавлялось какое-то новое поле, и начинали сыпаться проблемы, так как это имя уже было использовано либо с помощью аннотаций, либо с помощью Raw-SQL. Также реализация аннотаций в Django ORM делает невозможным использование паттерна Identity Map. Storm ORM/SQLAlchemy реализуют аннотации более удачно. Если Вам все-таки пришлось работать с Django Model, воздержитесь от использования механизма Django аннотаций в пользу голого паттерна DataMapper.

Особенности сервисного слоя на стороне клиента

Использование концепции агрегата и библиотек реактивного программирования, таких как RxJS, позволяет реализовывать Сервисный Слой с помощью простейшего паттерна Gateway, смотрите, например, учебный пример из документации Angular. В таком случае, Query Object обычно реализуется в виде простого словаря, который преобразуется в список GET-параметров URL. Общается такой Сервис с сервером обычно либо посредством JSON-RPC, либо посредством REST-API Actions.

Все работает хорошо до тех пор, пока не возникает необходимость выражать приоритезированные запросы, например, использующие логический оператор OR, который использует меньший приоритет чем логический оператор AND. Это порождает вопрос, кто должен отвечать за построение запроса, Сервисный Слой клиента или Сервисный Слой сервера?

С одной стороны, сервер не должен делать предположений о своих клиентах, и должен ограничивать свой интерфейс посредством интерфейса Query Object. Но это резко увеличивает уровень сложности клиента, в частности, при реализации Service Stub. Для облегчения реализации можно использовать библиотеку rql, упомянутую в статье “Реализация паттерна Repository в браузерном JavaScript”.

С другой стороны, Сервисный Слой, пусть и удаленного вызова, предназначен для обслуживания клиентов, а значит, может концентрировать в себе логику построения запросов. Если клиент не содержит сложной логики, позволяющей интерпретировать приоритезированные запросы для Service Stub, то нет необходимости его усложнять этим. В таком случае проще добавить новый метод в сервисе удаленного вызова, и избавиться от необходимости в приоритезированных запросах.

Проблема параллельного обновления

Появление интернета открыло доступ к огромному количеству данных, которое несопоставимо велико с возможностями одного сервера. Возникла необходимость в масштабировании и в распределенном хранении и обработке данных.

Одна из самых острых проблем - это проблема параллельного обновления данных.

Все состояния гонки (race condition), взаимоблокировки (deadlocks) и проблемы параллельного обновления обусловлены изменяемостью переменных. Если в программе нет изменяемых переменных, она никогда не окажется в состоянии гонки и никогда не столкнется с проблемами одновременного изменения. В отсутствие изменяемых блокировок программа не может попасть в состояние взаимоблокировки.

All race conditions, deadlock conditions, and concurrent update problems are due to mutable variables. You cannot have a race condition or a concurrent update problem if no variable is ever updated. You cannot have deadlocks without mutable locks.

- “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin

Любой порядок выражается в правильном наложении ограничений.

CQRS

Проблему параллельного обновления в значительной мере можно уменьшить наложением ограничения на двунаправленные изменения состояния путем введения однонаправленных изменений, т.е. путем отделения чтения от записи. Именно такой подход используется в Redux.

“it allows us to host the two services differently eg: we can host the read service on 25 servers and the write service on two. The processing of commands and queries is fundamentally asymmetrical, and scaling the services symmetrically does not make a lot of sense.”

- “CQRS, Task Based UIs, Event Sourcing agh!” by Greg Young

Управление Логикой Приложения и Бизнес-Логикой хорошо раскрывается в статье “Clarified CQRS” by Udi Dahan.

Использование CQRS способствует использованию парадигмы Функционального Программирования.

- В последнее время наметилась тенденция в популяризации функциональных языков и функциональной парадигмы программирования. Что вы скажите, является ли объектная технология конкурентом функциональному программированию?

- Нет, эти две парадигмы не являются конкурентами, они успешно могут дополнять друг друга. Тем не менее, тенденция к функциональному программированию является важной и интересной.

На мой взгляд, когда речь идет о высокоуровневой структуре приложения (особенно больших программ), то в мире нет ничего лучше объектного подхода. Я просто не вижу, как можно писать действительно большую программу исключительно на функциональном языке.

С другой стороны, если общая структура приложения построена на основе объектов, то очень даже полезно, если некоторые ее части будут написаны на функциональном языке, для обеспечения простоты и возможности доказательства корректности, о которых я говорил ранее.

Несколько лет назад я опубликовал статью на эту тему, где сравнивал ОО и ФП подходы. В ней я постарался показать, что ОО метод включает функциональное программирование, а не наоборот.

- Да, я кажется читал эту статью, которая затем вошла в качестве одной из глав в книгу “Beautiful Architecture”.

- Вы знаете об этом? Я очень впечатлен.

- (Смеюсь...) Да, и насколько я помню, это был ваш ответ на статью Саймона Пейтона Джонса, в которой автор старался показать, что ФП подход является более предпочтительным.

- Да, совершенно верно.

ПРИМЕЧАНИЕ: Речь идет о статье Бертрана “Software Architecture: Functional vs. Object-Oriented Design in Beautiful Architecture”, опубликованной в книге “Идеальная архитектура. Ведущие специалисты о красоте программных архитектур.”. Эта статья Мейера была ответом на статью Саймона “Composing contracts: an adventure in financial engineering.”

- Давайте все же немного вернемся к вопросу OOP vs FP. Какие именно преимущества у функционального подхода на “низком уровне”?

- В Eiffel существует очень важный принцип, под названием Command-Query Separation Principle, который можно рассматривать, в некотором роде, как сближение ОО и ФП миров. Я не считаю, что наличие состояния – это однозначно плохо. Но очень важно, чтобы мы могли ясно различать операции, которые это состояние изменяют (т.е. командами), и операции, которые лишь возвращают информацию о состоянии, его не изменяя (т.е. запросами). В других языках эта разница отсутствует. Так, например, в С/С++ часто пишут функции, которые возвращают результат и изменяют состояние. Следование этому принципу позволяет безопасно использовать выражения с запросами зная, что они не изменяют состояние. В некоторых случаях можно пойти еще дальше и работать в чисто функциональном мире с полным отсутствием побочных эффектов.

- Bertrand Meyer в интервью Сергея Теплякова “Интервью с Бертраном Мейером

For both theoretical and practical reasons detailed elsewhere [10], the command-query separation principle is a methodological rule, not a language feature, but all serious software developed in Eiffel observes it scrupulously, to great referential transparency advantage. Although other schools of object-oriented programming regrettable do not apply it (continuing instead the C style of calling functions rather than procedures to achieve changes), but in my view it is a key element of the object-oriented approach. It seems like a viable way to obtain the referential transparency goal of functional programming — since expressions, which only involve queries, will not change the state, and hence can be understood as in traditional mathematics or a functional language — while acknowledging, through the notion of command, the fundamental role of the concept of state in modeling systems and computations.

- “Software architecture: object-oriented vs functional” by Bertrand Meyer

Функциональное Программирование по своей сути не может порождать побочных эффектов (т.к. Функциональное Программирование накладывает ограничение на присваивание (изменяемость)), и именно этим обусловлен рост его популярности в эпоху распределенных вычислений. Нет изменяемого состояния - нет проблем параллельного обновления.

Следует отличать парадигму Функционального Программирования от языков, поддерживающих эту парадигму, поскольку нередко языки, поддерживающие эту парадигму, позволяют не следовать ей.

Однако, несмотря на открывшиеся возможности использовать Функциональное Программирование в коде, само хранилище данных (IO-устройство) все еще подвержено проблемам параллельного обновления, поскольку имеет изменяемые записи, а значит, имеет побочный эффект.

Решением этой проблемы обычно является замена CRUD (Create, Read, Update, Delete) на CR, т.е. наложение ограничения на изменение (Update) и удаление (Delete) записей в хранилище, что получило распространение под термином Event Sourcing. Существуют специализированные хранилища, реализующие его, но он реализуется не обязательно специализированными инструментами.

Event Sourcing

Если CQRS позволяет работать с хранилищами данных в Императивном стиле, и отделяет действия (побочный эффект) от запроса (чтения) данных, то Event Sourcing идет еще дальше, и накладывает ограничение на изменение и удаление данных, превращая CRUD в CR. Такой шаблон позволяет работать с хранилищами данных в Функциональном стиле, и предоставляет такие же выгоды: нет изменяемого состояния - нет проблемы параллельного обновления. И такие же недостатки - потребность в большом количестве памяти и процессорной мощности. Именно поэтому, данный шаблон широко используется в распределенных системах, где остро проявляется потребность в его достоинствах, и, вместе с тем, не проявляются его недостатки (ведь распределенные системы не лимитированы ни в памяти, ни в процессорной мощности).

Наглядным примером Event Sourcing может быть принцип организации банковского счета в базе данных, когда счет не является источником истины, а просто отражает совокупное значение всех транзакций (т.е. событий).

Наиболее ясно эта тема раскрывается в Chapter 6 “Functional Programming” of “Clean Architecture” by Robert C. Martin.

Что особенно важно, никакая информация не удаляется из такого хранилища и не изменяется. Как следствие, от набора CRUD-операций в приложениях остаются только CR. Также отсутствие операций изменения и/или удаления с хранилищем устраняет любые проблемы конкурирующих обновлений.

Обладая хранилищем достаточного объема и достаточной вычислительной мощностью, мы можем сделать свои приложения полностью неизменяемыми — и, как следствие, полностью функциональными.

Если это все еще кажется вам абсурдным, вспомните, как работают системы управления версиями исходного кода.

More importantly, nothing ever gets deleted or updated from such a data store. As a consequence, our applications are not CRUD; they are just CR. Also, because neither updates nor deletions occur in the data store, there cannot be any concurrent update issues.

If we have enough storage and enough processor power, we can make our applications entirely immutable—and, therefore, entirely functional.

If this still sounds absurd, it might help if you remembered that this is precisely the way your source code control system works.

- “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” [2] by Robert C. Martin

Event Sourcing is naturally functional. It’s an append only log of facts that have happened in the past. You can say that any projection any state is a left fold over your previous history.

- Greg Young, “A Decade of DDD, CQRS, Event Sourcing” at 16:44

It’s actually functional.

- Greg Young, “Event Sourcing is actually just functional code” at 34:49

I have always said that Event Sourcing is “Functional Data Storage”. In this talk we will try migrating to a idiomatic functional way of looking at Event Sourcing. Come and watch all the code disappear! By the time you leave you will never want an “Event Sourcing Framework (TM)” ever again!

- Greg Young, “Functional Data”, NDC Conferences

Что почитать

This article in English “Design of Service Layer and Application Logic”.

Footnotes

[1](1, 2, 3, 4, 5)Clean Code: A Handbook of Agile Software Craftsmanship” by Robert C. Martin
[2](1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13) “Clean Architecture: A Craftsman’s Guide to Software Structure and Design” by Robert C. Martin
[3](1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)Patterns of Enterprise Application Architecture” by Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford
[4](1, 2, 3, 4, 5, 6, 7, 8, 9, 10) “Domain-Driven Design: Tackling Complexity in the Heart of Software” by Eric Evans
[5]“Implementing Domain-Driven Design” by Vaughn Vernon
[6](1, 2) “Design Patterns Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, 1994
[7](1, 2) “Refactoring: Improving the Design of Existing Code” by Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts
[8](1, 2) “Building Microservices. Designing Fine-Grained Systems” by Sam Newman
[9].NET Microservices: Architecture for Containerized .NET Applications” edition v2.2.1 (mirror) by Cesar de la Torre, Bill Wagner, Mike Rousos

Updated on Oct 12, 2019

Comments

comments powered by Disqus