Skip to content

Commit

Permalink
update README
Browse files Browse the repository at this point in the history
  • Loading branch information
mobiletoly committed May 7, 2020
1 parent b54e893 commit c6f781b
Show file tree
Hide file tree
Showing 13 changed files with 67 additions and 60 deletions.
89 changes: 48 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,31 +73,56 @@ by using `ports.provided` package ("provided by domain") and expects some functi
- Frameworks (less are better)
- Database, network clients (no SQL Exposed calls, no Kafka calls, no SQS call etc)
- No transport logic (e.g. no JSON parsing, no JSON annotations etc). It might be tempting to use JSON objects received
from web service controllers, but don't it, use **ports** for DTO (Data Transfer Objects) to communicate with
from web service controllers, but don't do this, use **ports** for DTO (Data Transfer Objects) to communicate with
**adapters** module. It will require extra work and some extra effort to write data structures that might look very
similar, but when you decide to change your JSON payload, and your business logic will remain the same - it will pay off.
- DAO

#### Ports module

Ports is a bridge between Adapters and Domain. Domain declare interfaces it requires from Adapters and put them
in `ports.required` package. Then Adapters will provide implementations (injected via Dependency Injection, we use
Koin framework in our application). If Domain wants to share some functionality (usually business services) with
Adapters - then it will provide interfaces in `ports.provided` for Adapters to use. Also, Ports can declare data
classes (or POJOs) to allow Domain and Adapters communicate to each other.

###### Depends on
No module dependencies

###### What should be in ports
- `ports.provided` - interfaces for *provided* Domain services to be called from Adapters (in our case domain services will be
called from web service routes from **adapters** module). DTO objects for Domain<=>Adapters communication can be
declared here as well.
- `ports.required` - interfaces for Adapters services *required* by Domain to perform its business logic.

###### What should not be in ports
- Anything else


#### Adapters module

Platform-specific code. Don't put any of your business logic here.

###### Depends on
- **ports** module. Adapters use this module to fulfill Domain need. For example Domain needs an access in
getting access to persistent storage (and Domain does not care how data is stored) to add/delete/update Address Book
item. In this case Domain will add its requirements into `ports.required` package and Adapters should fulfill
these requirements. In our app Domain declares an interface `ports.requires.addressbook.AddressBookItemRepository`
- **ports** module. Adapters use this module to fulfill Domain need. For example Domain needs an access to persistent
storage (and Domain does not care how data is stored) to add/delete/update Address Book item. In this
case Domain will add its requirements into `ports.required` package and Adapters should fulfill
these requirements. In our app Domain declares an interface `ports.required.addressbook.AddressBookItemRepository`
and Adapters provides a database-specific implementation of this interface in
`adapters.db.AddressBookItemRepositoryDbImpl` class.
`adapters.db.AddressBookItemRepositoryDbImpl` class. Also Domain needs to generate random AddressBook item and it
adds this requirement via `ports.required.RandomPersonClient` interface and lets Adapters model to choose what
method to choose (pick from database, generate using its own algorith etc). In our case Adapters decided to
call free 3rd party REST service to fetch for random names and contact information and injects
`adapters.clients.randomperson.RandomPersonHttpClient` via dependency injection.

###### What should be in adapters
- Web Service controllers (in our app it is **ktor** handlers)
- HTTP/REST clients
- Kafka code to post data
- Kafka code to send or receive data
- Database repositories to provide access to underlying database.
- DAO objects. Yes, DAO objects should not be in **ports** or **adapters** modules if they contain framework-specific
code or annotations (e.g. JPA annotations). Exceptions can be made, for example Exposed SQL uses plain data classes
and therefore we decided to put them in **ports**, therefore we use this classes not only as DAO but as DTO objects
and we decided to put them in **ports**, therefore we use this classes not only as DAO but as DTO objects
as well (something that we transfer between Adapters and Domain). In your case you might want to consider using
DAO objects in Adapters only and have transformers to convert DAO objects to DTO objects (DTO objects declared
in `ports.provided`).
Expand All @@ -106,26 +131,6 @@ in `ports.provided`).
- Avoid any business logic code


#### Ports module

Ports is a bridge between Adapters and Domain. Domain declare interfaces it requires from Adapters and put them
in `ports.required` package. Then Adapters will provide implementations (injected via Dependency Injection, we use
Koin framework in our application). If Domain wants to share some functionality (usually business services) with
Adapters - then it will provide interfaces in `ports.provided`, implement them and use DI to inject for Adapters
to use.

###### Depends on
No module dependencies

###### What should be in ports
- `ports.provided` - interfaces for *provided* Domain services to be called from Adapters (in our case domain services will be
called from web service routes from **adapters** module). DTO objects for Domain<=>Adapters communication can be
declared here as well.
- `ports.required` - interfaces for Adapters services *required* by Domain to perform its business logic.

###### What should not be in ports
- Anything else

#### App module

Application launcher. Should be a very simple code. Can contain application specific resources.
Expand All @@ -134,28 +139,30 @@ Application launcher. Should be a very simple code. Can contain application spec
All other modules.


#### Ports module

Ports is a bridge between Adapters and Domain. Domain declares interfaces it requires from Adapters and put them
in `ports.required` package. Then Adapters will provide implementations (injected via Dependency Injection, we use
Koin framework in our application). If Domain wants to share some functionality (usually business services) with
Adapters - then it will provide interfaces in `ports.provided`, implement them and use DI to inject for Adapters
to use.

###### Depends on
No module dependencies

#### Shared module

**shared** module should not be used too often. It may contain some utility functionality, for example in our app we
have some logger utility functions in shared module, because logging is required in both **adapter** and **domain**
have some logger utility functions in the shared module, because logging required in both **adapter** and **domain**
modules.

###### Depends on
No module dependencies



### Example of workflow in modules

1. User performs HTTP POST request to /addressBookItems to create new AddressBook item. This request handled by
REST controller in `adapters.routes.AddressBookItemRoute` class. JSON payload is deserialized and copied into
`ports.provided.addressbook.SaveAddressBookItemRequestDto` data class required by Domain's service.
2. REST controller calls method `addAddressBookItem(...)` declared by interface `ports.provided.AddressBoookService`
(and implemented by **domain** module in `AddressBookServiceImpl` class)
3. Domain code in class `domain.addressbook.AddressBookServiceImpl` performs business logic related to validating and
storing AddressBook item received as DTO object from REST controller. At some point of time domain logic requires storing
of new item in persistent storage. Domain code calls method `upsert(...)` (update or insert) declared by interface
`AddressBookItemRepository` (and implemented by Adapters code in `AddressBookItemRepositoryDbImpl` class) as well as
method `upsert(...)` declared by interface `PostalAddressRepository`.
4. Domain's service code returns result back to Adapters REST controller when it gets serialized into JSON and returned
back to user.


# Setup
Expand Down
6 changes: 3 additions & 3 deletions adapters/src/main/kotlin/adapters/AdapterModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import adapters.http.HttpClientFactoryImpl
import adapters.routes.AddressBookItemRoute
import adapters.routes.HealthCheckRoute
import adapters.services.healthcheck.HealthCheckService
import adapters.util.DateSupplierImpl
import adapters.util.DateSupplierSystemTimeImpl
import ports.required.TransactionService
import ports.required.addressbook.AddressBookItemRepository
import ports.required.addressbook.PostalAddressRepository
import com.zaxxer.hikari.HikariDataSource
import io.ktor.application.Application
import org.koin.core.scope.Scope
import org.koin.dsl.module
import ports.provided.util.DateSupplier
import ports.required.util.DateSupplier
import ports.required.randomperson.RandomPersonClient
import javax.sql.DataSource

Expand All @@ -37,7 +37,7 @@ val envModule = module(createdAtStart = true) {
EnvironmentVariablesImpl()
}
single<DateSupplier> {
DateSupplierImpl()
DateSupplierSystemTimeImpl()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import adapters.http.HttpClientFactory
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import mu.KotlinLogging
import ports.required.randomperson.RandomPerson
import ports.required.randomperson.RandomPersonResponseDto
import ports.required.randomperson.RandomPersonClient
import shared.util.d

Expand All @@ -16,7 +16,7 @@ class RandomPersonHttpClient(
private val httpClientFactory: HttpClientFactory
) : RandomPersonClient {

override suspend fun fetchRandomPerson(): RandomPerson {
override suspend fun fetchRandomPerson(): RandomPersonResponseDto {
logger.d("fetchRandomPerson") { "Perform HTTP GET request to URL=${appConfig.randomPerson.fetchUrl}" }
val response: RandomPersonResponse = httpClientFactory.httpClient()
.get(urlString = appConfig.randomPerson.fetchUrl) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package adapters.clients.randomperson

import ports.required.randomperson.RandomPerson
import ports.required.randomperson.RandomPersonResponseDto

internal fun RandomPersonResponse.toRandomPerson() = with(results.first()) {
RandomPerson(
RandomPersonResponseDto(
firstName = name.first,
lastName = name.last,
gender = gender,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package adapters.services.healthcheck

import adapters.config.AppConfig
import ports.provided.healthcheck.HealthCheckResponseDto
import ports.provided.util.DateSupplier
import ports.required.util.DateSupplier
import java.util.Date

class HealthCheckService(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package adapters.util

import ports.provided.util.DateSupplier
import ports.required.util.DateSupplier

class DateSupplierImpl : DateSupplier {
class DateSupplierSystemTimeImpl : DateSupplier {

override fun currentTimeMillis() = System.currentTimeMillis()
}
2 changes: 1 addition & 1 deletion app/src/test/kotlin/app/DateSupplierTestImpl.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package app

import ports.provided.util.DateSupplier
import ports.required.util.DateSupplier

class DateSupplierTestImpl : DateSupplier {

Expand Down
2 changes: 1 addition & 1 deletion app/src/test/kotlin/app/TestModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package app

import adapters.config.EnvironmentVariables
import org.koin.dsl.module
import ports.provided.util.DateSupplier
import ports.required.util.DateSupplier

// Environment-specific configuration
val envTestModule = module(createdAtStart = true) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/test/kotlin/app/routes/HealthCheckRouteTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import org.koin.experimental.property.inject
import org.koin.ktor.ext.inject
import org.spekframework.spek2.style.specification.describe
import ports.provided.healthcheck.HealthCheckResponseDto
import ports.provided.util.DateSupplier
import ports.required.util.DateSupplier

object HealthCheckRouteTest : AppRouteSpek({

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package domain.addressbook
import ports.required.addressbook.AddressBookItem
import ports.required.addressbook.Gender
import ports.required.addressbook.PostalAddress
import ports.required.randomperson.RandomPerson
import ports.required.randomperson.RandomPersonResponseDto
import java.lang.IllegalArgumentException

fun String.toGender(): Gender {
Expand All @@ -16,7 +16,7 @@ fun String.toGender(): Gender {
}
}

fun RandomPerson.buildAddressBookItem() = AddressBookItem(
fun RandomPersonResponseDto.buildAddressBookItem() = AddressBookItem(
id = null,
firstName = firstName,
lastName = lastName,
Expand All @@ -26,7 +26,7 @@ fun RandomPerson.buildAddressBookItem() = AddressBookItem(
email = email
)

fun RandomPerson.buildPostalAddress(
fun RandomPersonResponseDto.buildPostalAddress(
addressBookItemId: Long
) = PostalAddress(
id = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package ports.required.randomperson

interface RandomPersonClient {
suspend fun fetchRandomPerson(): RandomPerson
suspend fun fetchRandomPerson(): RandomPersonResponseDto
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package ports.required.randomperson

data class RandomPerson(
data class RandomPersonResponseDto(
val firstName: String,
val lastName: String,
val gender: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ports.provided.util
package ports.required.util

// While some functions look very obvious, this class is still useful when we want to perform a unit test
// Just an example, HealthRoute might return a current timestamp and we want to validate that it is current
Expand Down

0 comments on commit c6f781b

Please sign in to comment.