在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称(OpenSource Name):dustinsand/hex-arch-kotlin-spring-boot开源软件地址(OpenSource Url):https://github.com/dustinsand/hex-arch-kotlin-spring-boot开源编程语言(OpenSource Language):Kotlin 90.7%开源软件介绍(OpenSource Introduction):Hexagonal Architecture (aka Ports and Adapters) / Clean Architecture / DDD
Personally I've used hexagonal architectures with DDD for years because it aligns well with how I reason about designing applications. However, I'm not aware of a "standard" project structure for JVM microservices so this is my interpretation of how to design and implement a hexagonal architecture for the JVM. Project DescriptionThis project is used to show how the hexagonal architecture can be applied to a Microservice (Spring Boot) and a native binary AWS Lambda (Quarkus) in a multi module project. The multi module project allows for code re-use (each application re-uses the application core and the output adapters) across modules. The sample code is intentionally simple in order to focus on how to structure the packages for a hexagonal architecture and apply the concepts. Objective Of This Architecture
ConceptsThe hexagonal architecture conceptually consists of two parts: the Application Core, and the Ports and Adapters. Outer layers depend on inner layers. Inner layers expose interfaces that outer layers must adapt to and implement. This form of dependency inversion protects the integrity of the domain and application layers. Outside the application layer, we have ports (Interfaces) and adapters (Implementations) that handle the technical delivery to the outside world. The adapters handle the technical delivery by using the application services in the domain layer. Application CoreThe inside of the hexagon contains the business logic and is unaware of the technologies used to implement the application. The business logic does not care if the application provides a REST API or a GraphQL API or whether the data is stored in a database or a file. The application core declares the ports that are needed to fulfill the use cases. The technical details of how the adapters implement the ports are not a concern of the application core. Ports and AdaptersThe ports (Interfaces) are at the inside edge of the hexagon, and they declare the requirements of the application to fulfill the use cases. Input ports allow different types of drivers to interface with the application core. Output ports allow different kinds of technologies to be driven by the application core. The ports at the edge allow the design to be switched with other technologies without impacting the core of the application. For example, the application could be driven by a REST API or a GraphQL API using the same input ports while the application could drive storing the data in a relational database, or a file using the same output ports. Adapters are at the outside edge of the hexagon, and are the implementation of the port using the technology of choice. For example, a REST API input adapter, or a PostgreSQL output adapter. Gradle Multi-Module ProjectUsed Gradle's multi-module capability to demonstrate how a hexagonal project could be modularized. The modules are 99.9% in Kotlin, but there are edge cases when Java is required so you will find Kotlin and Java classes mixed together in the same src directory. I chose src/main/java and src/test/java for both the Kotlin and Java source files. Modulesvoter-application-coreThe inner hexagon. See definition in 'How it works' Package StructuredomainIsolated from technical complexities of clients, frameworks and adapter (infrastructure) concerns. Contains the business models. Dependencies face inward so it does not depend on any layers outside of it. Why is this important? Adapters (Infrastructure) can be changed without changes to the domain. The objects in this layer contain the data and the logic pertaining to the business. Independent of the business processes that trigger that logic, they are independent and completely unaware of the Application Layer. The business objects that represent something in the domain. Examples of these objects are, first of all, Entities but also Value Objects, Enums and any objects used in the Domain Model. The Domain Model is also where Domain Events “live”. These events are triggered when a specific set of data changes and they carry those changes with them. In other words, when an entity changes, a Domain Event is triggered and it carries the changed properties new values. These events are perfect, for example, to be used in Event Sourcing. modelUsing DDD this is where you define the Entities, Aggregates, Value Objects and Domain Events to model your domain. Refer to the DDD tactical patterns for descriptions serviceSometimes we encounter some domain logic that involves different entities, of the same type or not, and we feel that that domain logic does not belong in the entities themselves, we feel that that logic is not their direct responsibility. So our first reaction might be to place that logic outside the entities, in an Application Service. However, this means that that domain logic will not be reusable in other use cases: domain logic should stay out of the application layer! The solution is to create a Domain Service, which has the role of receiving a set of entities and performing some business logic on them. A Domain Service belongs to the Domain Layer, and therefore it knows nothing about the classes in the Application Layer, like the Application Services or the Repositories. In the other hand, it can use other Domain Services and, of course, the Domain Model objects. Characteristics of a domain service are:
application layerport.inputThis layer defines use case (application behavior) interfaces of the application. port.outputThis layer defines infrastructure interfaces implemented by the adapters so there is no coupling between domain layer and technical code. serviceThe API representing the business use cases of the application. Dependent on the domain layer to orchestrate the business use cases and acts as a facade to the domain. The domain model can continue to evolve without impacting clients. This layer is also responsible for coordinating notifications to other systems when significant events occur within the domain. Clients must adapt to the input defined by the API and also transform the output from the API into their own format. The application layer then acts as an anti-corruption layer, ensuring the domain layer stays unaffected by external technical details. Example role of an Application Service to fulfill a use case:
adapter-outputThe outer hexagon containing a gradle module per adapter. Output adapters can be re-used by applications (microservice, lambda, cli). For example, voter-ms uses modules from adapter-output. output (preferred 'out' for the name, but 'out' is a reserved word in Kotlin)These are the outbound adapter technical details that the application uses (right side of diagram below in tan), for example, a database, a 3rd party APIs. These are needed to support the domain use cases. voter-msThe microservice is a composition of the voter-application-core and adapter-output modules. adapter input layerThe adapter layer provides the technical capabilities of the application to be consumed, such as a UI, web services, messaging endpoints. It also provides the application the ability to consume external services such as databases, 3rd party services, logging, security and other bounded contexts. These are all technical details that should not directly affect the use case exposed and the domain logic of an application. Typically hexagonal architectures diagram the left side (see "in" below) for the the clients which use the domain. The right side (see "out" below) of the diagram are the services used by the domain. input (preferred 'in' for the name, but 'in' is a reserved word in Kotlin)The entry point (left side of diagram) of clients to use the application layer. The inbound adapter translates whatever comes from a client into a method call in the application layer. voter-lambdaThe lambda is a composition of the voter-application-core and adapter-output modules. It demonstrates:
adapter input layerThe adapter layer provides the technical capabilities of the application to be triggered by AWS Lambda. input (preferred 'in' for the name, but 'in' is a reserved word in Kotlin)The entry point (left side of diagram) of AWS Triggers to use the application layer. The inbound adapter translates whatever comes from a client into a method call in the application layer. TargetsRun the tests
Run the microservice
Run the lambdaSee the Lambda README Communication Across LayersWhen communicating across layers, to prevent exposing the details of the domain model to the outside world, you don’t pass domain objects across boundaries. For the same reasons, you don’t send raw, unchecked user input straight into the domain layer. Instead, you use simple data transfer objects (DTOs), presentation models, and application event objects to communicate changes or actions in the domain. Testing in IsolationSeparating the concerns in the application and ensuring the domain logic is not dependent on any technical details allows you to test domain and application logic in isolation independent of any adapter frameworks. The application layer is more appropriate for integration testing and the domain layer is more appropriate for unit testing with mocks. Enforcing the ArchitectureThe architecture is at risk of eroding over time if boundaries between layers are not maintained. ArchUnit is used to check if the code follows the architecture. ArchUnit does so by analyzing the given Java bytecode, importing all classes into a Java code structure. Kotlin added the keyword modifier, internal, to enforce component boundaries and overcome the weakness of package-private in Java. I liked the idea in the reference, Clean Boundaries, to be explicit and put "internal" classes in a package named internal which could then be enforced by ArchUnit. However, Kotlin doesn't support Package annotations so you will see the InternalPackage annotation is written in Java and the package-info.java in the "internal" packages are also in Java. I'm experimenting if this is a good idea or not with Kotlin because it does seem redundant since Kotlin has the internal modifier. However, I like that I can test for any class in an "internal" package is not used inappropriately (developers can still forget to use the internal modifier) and it guides the developer to use the dependency inversion principle. ArchUnit’s main focus is to automatically test architecture and coding rules, using any plain Java unit testing framework. See src/test/java/.../HexagonalArchitectureTest.kt and InternalPackageTest.kt for the rules checked. |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论