Skip to content

Commit

Permalink
remove the concept of ports (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugoArregui authored Mar 18, 2024
1 parent d940c64 commit 8bb2fd7
Showing 1 changed file with 6 additions and 12 deletions.
18 changes: 6 additions & 12 deletions content/basics/philosophy.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,29 +97,23 @@ function createCacheInMemoryMock(): ICacheComponent {

We have implemented all of our business logic without making _any_ technology decision. There is no mechanism in markdown to emphatize this sentence as much as I'd like, this is huge. The whole business logic is easyly testable and completely decoupled from bikeshedding discussions and libraries. We did not talk about which server we are going to use, we don't know if the MQ is Kafka, AMQP, SQS or UDP messages or messaging pidgeons.

## Ports

**Ports are the implementation of the components**. Is the result of invoking a function that returns the actual `cache` that we will pass to the functions.

Ports could be part of our program to begin with, and a good practice is to extract the ports into their own repository as soon as the API (the component) is stable. To enable other teams or projects to leverage the port.

## Adapters

Adapters are pure functions that transforms external data from ports into our internal usable representation `fn(rawPortData) -> ApplicationData`.
Adapters are pure functions that transforms external data into our internal usable representation `fn(rawData) -> ApplicationData`.

A good example is the adapters for Postgress queries. Every record returned by Postgres uses underscores, but depending on the conventions, we normally use camelCase for our records.

It is recommended that everything is well typed, and that our services have a cannonical representation of the data that does not change with the exposed APIs, and it is consistent no matter which port or component are we using. An example is the schema of a notification. It shouldn't matter whether the notification arrives from SNS, a message queue or UDP message. Or if it is encoded as JSON, XML or ProtocolBuffer. Our service should have a cannonical `Notification` type that is always consistent. To do that, we use the Adapters, from the hexagonal architecture. Adapters abstract us from the subtleties of every port and their protocols to have cannonical representations of our data.

## Controllers

The "glue" between all the other layers, orchestrating calls between pure business logic, adapters, and ports.
The "glue" between all the other layers, orchestrating calls between pure business logic, components and adapters.

Controllers always receive an hydrated context containing components and parameters to call the business logic.

## Development approach

The goal of the initiative, is to enable docummented and consistent creation of services, using reusable pieces (components) in a seamlessly way while keeping the whole thing testable and maintainable. Often projects explode in complexity because they rely on mountains of constructs and abstraction patterns. While leveraging simple constructs like functions and records (ports) might make things simpler.
The goal of the initiative, is to enable docummented and consistent creation of services, using reusable pieces (components) in a seamlessly way while keeping the whole thing testable and maintainable. Often projects explode in complexity because they rely on mountains of constructs and abstraction patterns. While leveraging simple constructs like functions and records might make things simpler.

To create big systems the proposal goes as follows:

Expand Down Expand Up @@ -234,7 +228,7 @@ Write the glue code using the components to achieve business results. We call th

This framework is heavily inspired by Hexagonal Architecture, where the components instances are ports, and the business logic live in controllers, legeraging adapters + core logic.

The controllers connect several components together to achieve an use case. The first examples in this document were the controllers, the handler functions. Then we need to wire the handlers to the ports, and that process is called "wiring".
The controllers connect several components together to achieve an use case. The first examples in this document were the controllers, the handler functions. Then we need to wire the handlers to the components, and that process is called "wiring".

The controllers (handlers) will receive only the context they need. That makes testability easier when using static typing, because there is no need to pass unwanted or unused components to a handler.

Expand Down Expand Up @@ -285,9 +279,9 @@ main().catch((err) => {
<!--
## Service lifecycle
A service is a set of ports wired together. In practical means, the lifecycle of a service _should_ be reduced to:
A service is a set of components wired together. In practical means, the lifecycle of a service _should_ be reduced to:
1. **Create components**: instantiate the ports
1. **Create components**: instantiate the components.
2. **Wire the components together**: e.g, bind the HTTP request to the handlers. Bind the MQ message to the cache writer handler.
3. **Start the components**: start the lifecycle of the components by themselves: `http.listen()`, `mq.connect()`
4. **Shut down**: under some condition (like SIGTERM), gracefully stop the components and exit the process.
Expand Down

0 comments on commit 8bb2fd7

Please sign in to comment.