Skip to content

Instantly share code, notes, and snippets.

@gicappa
Last active March 4, 2023 15:45
Show Gist options
  • Save gicappa/575eb6813b5815b70b7682ded2cc6389 to your computer and use it in GitHub Desktop.
Save gicappa/575eb6813b5815b70b7682ded2cc6389 to your computer and use it in GitHub Desktop.
Hexagonal Architecture
package application;
/* ... */
// Application level is the holder of primary drivers
// This is actually the specific implementation of the application server
// (SparkJava in this case)
public class Emapp implements Runnable {
private final Mapper mapper;
private final UserService userService;
/* ... */
// run() method is the entry point of the application
public void run() {
// Maps "/users" routes
// Responsiblity: handles the HTTP requests and creates the HTTP response
post("/users", (request, response) -> {
try {
response.status(201);
// Invoke the userService level still at application level
return userService.create(request.body());
} catch (EmappException ee) {
response.status(400);
return mapper.fromError(new Error(ee.getErrorCode(), ee.getMessage()));
}
});
}
}
package application;
/* ... */
// UserService decouples the controller (HTTP) from the parsing and sanitizing
// of the user inputs.
// If we need to change the ApplicationServer (e.g. from SparkJava to Tomcat)
// we can port this class in the new application server without changing the
// logic inside of it.
public class UserService {
private final ManageUsersUseCase manageUsersUseCase;
private final Mapper mapper;
public String create(String candidateUserPayload) {
var candidateUser = mapper.toCandidateUser(candidateUserPayload);
// These guards could be done both in the application level and in the domain
// If the front end should not allow to have them valued they should stay here
// because it is an exceptional since they should be valued.
//
// If the front end allows them to be null the check (if needed) should be done
// in the domain and handled as a functional requirement.
checkIsPresent("email", candidateUser.email());
checkIsEmailValid("email", candidateUser.email());
// Invoking the manageUsersUseCase the first object in the domain.
var userResponse = manageUsersUseCase.createUser(candidateUser);
return mapper.fromUser(userResponse);
}
}
package domain;
// Domain class coordinating the use case using collaborators
// to achieve a business need.
//
public class ManageUsersUseCase {
// interfaces inside domain package that don't have the implementation
// in the domain and which implementation are in the infrastructure level
// The concrete implementation is injected in the constructor of this class
private final UsersRepo usersRepo;
private final EmailService emailService;
public ManageUsersUseCase(UsersRepo usersRepo, EmailService emailService) {
this.usersRepo = usersRepo;
this.emailService = emailService;
}
// Here is contained the behaviour of the domain
public User createUser(CandidateUser candidateUser) {
// This method is suppose to persist the user (the domain doesn't care where)
// The returned user contains also the ID generated by the persistence layer
var user = usersRepo.recordUser(candidateUser);
// This method is sending an email calling the interface and
// not caring about the implementation library
//
// The email is also sending the ID of the new user in the email
emailService.sendVerificationEmailTo(user);
return user;
}
}
package domain;
//Interface still present in the domain
public interface UsersRepo {
User recordUser(CandidateUser user);
}
package domain;
// Interface in the domain
public interface EmailService {
void sendVerificationEmailTo(User user);
}
package infrastructure;
// Implementation specific for oracle database
// This is an adapter and implements the UserRepo interface
public class OracleUserRepo implements UsersRepo {
private final Logger logger = LoggerFactory.getLogger("repo|users");
@Override
public User recordUser(CandidateUser user) {
user.roles().forEach(this::checkRoleExists);
String uuid = insertUserInOracle(user);
return new User(uuid, user.email(), user.roles());
}
private void checkRoleExists(Role role) {
if (!selectRoleIdInDb(role)) {
throw new RoleDoesNotExistsException(role);
}
}
private String insertUserInDb(CandidateUser user) {
logger.info("insert the user({}) in the Oracle database and return the user id", user.email());
return "00000000-0000-0000-0000-000000000000";
}
private boolean selectRoleIdInDb(Role role) {
logger.info("select the role with id({}) in the role lists and return true if it present", role.id());
return true;
}
}
@gicappa
Copy link
Author

gicappa commented Mar 4, 2023

Screenshot 2023-03-04 at 16 28 37

Nell'esempio in figura vedi dove posiziono le classi rispetto ai layer di application, domain e infrastructure.
Lo use case nel domain (in questo caso con poche responsabilità) scrive quale sia il comportamento dell'applicazione e può essere testato in completo isolamento.

Il repo ritorna uno user con anche valorizzato l'ID preso dalla persistenza che deve essere utilizzato nell'email per esempio per generare un link per la conferma della registrazione utente.

@gicappa
Copy link
Author

gicappa commented Mar 4, 2023

Per me il domain contiene la business logic (use cases) e i gli oggetti che modellano lo use case (models).
Tutto questo non è nulla di nuovo ed è molto simile a quanto scritto da Steve Freeman e Nat Price (http://www.growing-object-oriented-software.com/)

@gicappa
Copy link
Author

gicappa commented Mar 4, 2023

Per cui ho tre domande:

  1. Come struttureresti il codice?

  2. Tu dove metteresti la business logic che dice:

  • dato un utente
  • devo registrarlo
  • devo mandargli una email?
  1. Come gestiresti il ritorno di un valore dal repo (l'id dell'utente) se dovessi uscire dal domain ogni volta che devi contattare un servizio esterno?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment