Архитектурные принципыArchitectural principles

"Если бы строители возводили здания так же, как программисты пишут программы, первый же дятел уничтожил бы всю цивилизацию"."If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization."
- Джеральд Вайнберг (Gerald Weinberg)- Gerald Weinberg

При разработке архитектуры и проектировании программных решений важно учитывать удобство поддержки.You should architect and design software solutions with maintainability in mind. В этом разделе описываются принципы принятия решений о выборе архитектуры, которые помогут вам строить прозрачные и удобные в поддержке приложения.The principles outlined in this section can help guide you toward architectural decisions that will result in clean, maintainable applications. В общем случае эти принципы рекомендуют создавать приложения, состоящие из слабо связанных с другими частями приложения компонентов, взаимодействие между которыми осуществляется с использованием явных интерфейсов или систем обмена сообщениями.Generally, these principles will guide you toward building applications out of discrete components that are not tightly coupled to other parts of your application, but rather communicate through explicit interfaces or messaging systems.

Общие принципы проектированияCommon design principles

Разделение задачSeparation of concerns

Основополагающим принципом разработки является разделение задач.A guiding principle when developing is Separation of Concerns. Этот принцип подразумевает разделение программного обеспечения на компоненты в соответствии с выполняемыми ими функциями.This principle asserts that software should be separated based on the kinds of work it performs. Рассмотрим пример приложения, в котором используется логика выбора отображаемых пользователю элементов, а также осуществляется форматирование этих элементов для максимально эффективного показа.For instance, consider an application that includes logic for identifying noteworthy items to display to the user, and which formats such items in a particular way to make them more noticeable. Выбор форматируемых элементов должен быть отделен от самих функций форматирования, так как это разные задачи, не имеющие тесной связи.The behavior responsible for choosing which items to format should be kept separate from the behavior responsible for formatting the items, since these behaviors are separate concerns that are only coincidentally related to one another.

С точки зрения архитектуры для соблюдения этого принципа при проектировании следует отделять бизнес-логику от инфраструктуры и функций пользовательского интерфейса.Architecturally, applications can be logically built to follow this principle by separating core business behavior from infrastructure and user-interface logic. В идеальном случае бизнес-правила и логика должны размещаться в отдельном проекте, который не должен зависеть от других проектов в приложении.Ideally, business rules and logic should reside in a separate project, which should not depend on other projects in the application. Благодаря такому разделению бизнес-модель легко тестировать и развивать, не затрагивая при этом реализацию возможностей более низкого уровня.This separation helps ensure that the business model is easy to test and can evolve without being tightly coupled to low-level implementation details. Разделение задач является одним из основных вопросов, который следует учитывать при использовании слоев архитектур приложения.Separation of concerns is a key consideration behind the use of layers in application architectures.

ИнкапсуляцияEncapsulation

Инкапсуляция отдельных частей приложения позволяет изолировать их друг от друга.Different parts of an application should use encapsulation to insulate them from other parts of the application. Корректировка внутренней реализации компонентов и слоев приложения должна быть возможна без влияния на участников совместной работы при условии, что при этом не нарушаются внешние контракты.Application components and layers should be able to adjust their internal implementation without breaking their collaborators as long as external contracts are not violated. Правильно реализованная инкапсуляция позволяет получить слабо связанную модульную структуру приложения, поскольку объекты и пакеты можно с легкостью заменять альтернативными реализациями, если при этом сохраняется один и тот же интерфейс.Proper use of encapsulation helps achieve loose coupling and modularity in application designs, since objects and packages can be replaced with alternative implementations so long as the same interface is maintained.

Инкапсуляция классов реализуется посредством ограничения доступа извне к внутреннему состоянию класса.In classes, encapsulation is achieved by limiting outside access to the class's internal state. Если внешнему субъекту требуется изменить состояние объекта, он должен использовать четко определенную функцию (или метод задания свойств) вместо того, чтобы напрямую получать доступ к закрытому состоянию объекта.If an outside actor wants to manipulate the state of the object, it should do so through a well-defined function (or property setter), rather than having direct access to the private state of the object. Аналогичным образом, компоненты приложений и сами приложения должны предоставлять четко определенные интерфейсы, которые будут использоваться участниками совместной работы вместо того, чтобы допускать изменение их состояния напрямую.Likewise, application components and applications themselves should expose well-defined interfaces for their collaborators to use, rather than allowing their state to be modified directly. Это позволяет свободно модернизировать внутреннюю структуру приложения со временем, не беспокоясь о том, что это может нарушить функционирование других участников совместной работы (при условии, что соблюдаются условия открытых контрактов).This frees the application's internal design to evolve over time without worrying that doing so will break collaborators, so long as the public contracts are maintained.

Инверсия зависимостейDependency inversion

Зависимость в приложении должна быть направлена в сторону абстракции, а не на детали реализации.The direction of dependency within the application should be in the direction of abstraction, not implementation details. При написании большинства приложений направление зависимостей времени компиляции задается в сторону времени выполнения. Это создает прямую схему зависимостей.Most applications are written such that compile-time dependency flows in the direction of runtime execution, producing a direct dependency graph. Это значит, что если модуль A вызывает функцию в модуле B, которая вызывает функцию в модуле C, то во время компиляции A будет зависеть от B, который, в свою очередь, зависит от C, как показано на рис. 4-1.That is, if module A calls a function in module B, which calls a function in module C, then at compile time A will depend on B, which will depend on C, as shown in Figure 4-1.

Схема прямых зависимостей

Рис. 4-1.Figure 4-1. Схема прямых зависимостей.Direct dependency graph.

Применение принципа инверсии зависимостей позволяет модулю A вызывать методы абстракции, которую реализует модуль B. Это значит, что модуль A может вызывать модуль B во время выполнения, однако B будет зависеть от интерфейса, управляемого модулем A, во время компиляции (таким образом, типовая зависимость времени компиляции инвертируется).Applying the dependency inversion principle allows A to call methods on an abstraction that B implements, making it possible for A to call B at runtime, but for B to depend on an interface controlled by A at compile time (thus, inverting the typical compile-time dependency). Во время выполнения поток выполнения программы остается неизменным, однако при этом легко могут быть подключены новые реализации интерфейсов.At run time, the flow of program execution remains unchanged, but the introduction of interfaces means that different implementations of these interfaces can easily be plugged in.

Схема инвертированных зависимостей

Рис. 4-2.Figure 4-2. Схема инвертированных зависимостей.Inverted dependency graph.

Инверсия зависимостей является важной частью процесса создания слабо связанных приложений, так как детали реализации могут описывать зависимости и реализовывать абстракции более высокого уровня, а не компоненты того же уровня.Dependency inversion is a key part of building loosely coupled applications, since implementation details can be written to depend on and implement higher-level abstractions, rather than the other way around. В результате получаются приложения с более высоким уровнем тестируемости, модульности и удобства в обслуживании.The resulting applications are more testable, modular, and maintainable as a result. Практика внедрения зависимостей базируется на соблюдении принципа инверсии зависимостей.The practice of dependency injection is made possible by following the dependency inversion principle.

Явные зависимостиExplicit dependencies

Методы и классы должны явно требовать наличия всех совместно работающих объектов, которые необходимы для их корректного функционирования.Methods and classes should explicitly require any collaborating objects they need in order to function correctly. Благодаря конструкторам классов классы могут идентифицировать объекты, которые им необходимы для сохранения корректного состояния и правильного функционирования.Class constructors provide an opportunity for classes to identify the things they need in order to be in a valid state and to function properly. Если определены классы, которые могут конструироваться и вызываться, но которые корректно работают только при наличии определенных глобальных или инфраструктурных компонентов, поведение таких классов будет не до конца прозрачным для клиентов.If you define classes that can be constructed and called, but that will only function properly if certain global or infrastructure components are in place, these classes are being dishonest with their clients. Контракт конструктора указывает клиенту на то, что ему требуются только заданные компоненты (если класс использует конструктор без параметров, не требуется ничего), однако во время выполнения выясняется, что объекту фактически требуется что-то еще.The constructor contract is telling the client that it only needs the things specified (possibly nothing if the class is just using a parameterless constructor), but then at runtime it turns out the object really did need something else.

При соблюдении принципа явных зависимостей ваши классы и методы будут прозрачны для клиентов, указывая все необходимые им для работы объекты.By following the explicit dependencies principle, your classes and methods are being honest with their clients about what they need in order to function. Благодаря соблюдению этого принципа код и контракты программирования станут более понятными, так как пользователи будут уверены, что, если предоставлены все требуемые в параметрах метода или конструктора объекты, во время выполнения такой метод или конструктор будет работать корректно.Following the principle makes your code more self-documenting and your coding contracts more user-friendly, since users will come to trust that as long as they provide what's required in the form of method or constructor parameters, the objects they're working with will behave correctly at run time.

Единственная обязанностьSingle responsibility

Принцип единственной обязанности применяется к объектно-ориентированному проектированию, но также может рассматриваться и как архитектурный принцип аналогично разделению задач.The single responsibility principle applies to object-oriented design, but can also be considered as an architectural principle similar to separation of concerns. Этот принцип подразумевает, что объекты должны иметь только одну обязанность и только одну причину для изменения.It states that objects should have only one responsibility and that they should have only one reason to change. В частности, единственным сценарием, в котором должен изменяться объект, является обновление способа выполнения объектом его единственной обязанности.Specifically, the only situation in which the object should change is if the manner in which it performs its one responsibility must be updated. Соблюдение этого принципа позволяет создавать модульные системы с меньшей степенью связанности, так как многие виды нового поведения можно реализовать в виде новых классов, не добавляя дополнительные обязанности к существующим.Following this principle helps to produce more loosely coupled and modular systems, since many kinds of new behavior can be implemented as new classes, rather than by adding additional responsibility to existing classes. Добавление новых классов всегда более безопасно по сравнению с изменением существующих, поскольку в этот момент никакой код еще не зависит от новых классов.Adding new classes is always safer than changing existing classes, since no code yet depends on the new classes.

В монолитном приложении принцип единственной обязанности может применяться на высоком уровне к слоям приложения.In a monolithic application, we can apply the single responsibility principle at a high level to the layers in the application. Обязанность представления должна оставаться в проекте пользовательского интерфейса, тогда как обязанность доступа к данным будет отнесена к проекту инфраструктуры.Presentation responsibility should remain in the UI project, while data access responsibility should be kept within an infrastructure project. Бизнес-логика должна находиться в основном проекте приложения, где ее можно тестировать и модернизировать независимо от других обязанностей.Business logic should be kept in the application core project, where it can be easily tested and can evolve independently from other responsibilities.

Доводя этот принцип в архитектуре приложения до логической завершенности, мы получим микрослужбы.When this principle is applied to application architecture and taken to its logical endpoint, you get microservices. Любая микрослужба должна иметь единственную обязанность.A given microservice should have a single responsibility. Если вам требуется расширить функциональные возможности системы, в большинстве случаев это рекомендуется делать путем добавления новых микрослужб, а не расширения обязанностей существующих.If you need to extend the behavior of a system, it's usually better to do it by adding additional microservices, rather than by adding responsibility to an existing one.

Дополнительные сведения об архитектуре микрослужбLearn more about microservices architecture

Принцип "Не повторяйся"Don't repeat yourself (DRY)

В приложении не следует определять поведение, связанное с конкретной концепцией, в нескольких расположениях, так как такой подход часто приводит к ошибкам.The application should avoid specifying behavior related to a particular concept in multiple places as this practice is a frequent source of errors. В какой-то момент в связи с изменением требований потребуется изменить такое поведение.At some point, a change in requirements will require changing this behavior. В этом случае велик риск, что как минимум в одном расположении это обновление не будет проведено, и вся система станет несогласованной.It's likely that at least one instance of the behavior will fail to be updated, and the system will behave inconsistently.

Вместо того чтобы дублировать логику, ее следует инкапсулировать в конструкции программирования.Rather than duplicating logic, encapsulate it in a programming construct. Такая конструкция должна быть единственным исполнителем нужного поведения и использоваться в любых других частях приложения в тех случаях, когда требуется реализовать это поведение.Make this construct the single authority over this behavior, and have any other part of the application that requires this behavior use the new construct.

Примечание

Не рекомендуется связывать поведение, которое повторяется нерегулярно.Avoid binding together behavior that is only coincidentally repetitive. Например, если две разных константы имеют одно значение, вместо них не нужно использовать одну константу, поскольку с точки зрения концепции они ссылаются на разные объекты.For example, just because two different constants both have the same value, that doesn't mean you should have only one constant, if conceptually they're referring to different things.

Независимость сохраняемостиPersistence ignorance

Принцип независимости сохраняемости относится к типам, для которых требуется сохранение состояния, однако код которых не зависит от выбираемой для этих целей технологии.Persistence ignorance (PI) refers to types that need to be persisted, but whose code is unaffected by the choice of persistence technology. В .NET такие типы иногда называются простыми объектами CLR (POCO), поскольку они не наследуются от конкретного базового класса и не реализуют определенный интерфейс.Such types in .NET are sometimes referred to as Plain Old CLR Objects (POCOs), because they do not need to inherit from a particular base class or implement a particular interface. Принцип независимости сохраняемости очень важен, поскольку он позволяет сохранять состояние бизнес-модели различными способами, благодаря чему увеличивается гибкость приложения.Persistence ignorance is valuable because it allows the same business model to be persisted in multiple ways, offering additional flexibility to the application. Способы сохраняемости со временем могут изменяться. Например, вместо одной технологии базы данных может использоваться другая, а также могут потребоваться дополнительные способы (например, в дополнение к реляционной базе данных могут использоваться кэш Redis или Azure Cosmos DB).Persistence choices might change over time, from one database technology to another, or additional forms of persistence might be required in addition to whatever the application started with (for example, using a Redis cache or Azure Cosmos DB in addition to a relational database).

Ниже приведены некоторые примеры нарушения этого принципа:Some examples of violations of this principle include:

  • Обязательный базовый класс.A required base class.

  • Обязательная реализация интерфейса.A required interface implementation.

  • Классы, отвечающие за сохранение самих себя (например, шаблон активной записи).Classes responsible for saving themselves (such as the Active Record pattern).

  • Требуется конструктор без параметров.Required parameterless constructor.

  • Свойства, использующие ключевое слово virtual.Properties requiring virtual keyword.

  • Обязательные атрибуты сохраняемости.Persistence-specific required attributes.

Обязательное использование любых из указанных возможностей увеличивает степень связанности между типами, для которых требуется сохраняемость, и применяемой для этих целей технологией, что усложняет реализацию новых стратегий доступа к данным в будущем.The requirement that classes have any of the above features or behaviors adds coupling between the types to be persisted and the choice of persistence technology, making it more difficult to adopt new data access strategies in the future.

Ограниченные контекстыBounded contexts

Принцип ограниченных контекстов является центральным при проблемно-ориентированном проектировании.Bounded contexts are a central pattern in Domain-Driven Design. Его соблюдение позволяет решить проблему сложности в крупных приложениях или организациях за счет разбиения на отдельные концептуальные модули.They provide a way of tackling complexity in large applications or organizations by breaking it up into separate conceptual modules. Каждый такой модуль представляет контекст, который отделен от других контекстов (то есть ограничен) и может развиваться независимо.Each conceptual module then represents a context that is separated from other contexts (hence, bounded), and can evolve independently. В идеальном случае в каждом ограниченном контексте должны свободно выбираться имена используемых в нем концепций и обеспечиваться монопольный доступ к собственному хранилищу сохраняемости.Each bounded context should ideally be free to choose its own names for concepts within it, and should have exclusive access to its own persistence store.

Как минимум, в отдельном веб-приложении необходимо постараться реализовать собственный ограниченный контекст со своим хранилищем сохраняемости для бизнес-модели вместо того, чтобы использовать общую для всех приложений базу данных.At a minimum, individual web applications should strive to be their own bounded context, with their own persistence store for their business model, rather than sharing a database with other applications. Взаимодействие между ограниченными контекстами осуществляется посредством программных интерфейсов, а не за счет общей базы данных, благодаря чему бизнес-логика и события могут реагировать на происходящие изменения.Communication between bounded contexts occurs through programmatic interfaces, rather than through a shared database, which allows for business logic and events to take place in response to changes that take place. Ограниченные контексты тесно связаны с микрослужбами, которые в идеальном случае также реализуются в качестве отдельных ограниченных контекстов.Bounded contexts map closely to microservices, which also are ideally implemented as their own individual bounded contexts.

Дополнительные ресурсыAdditional resources