Slides de la charla "Acercándonos a la Programación Funcional a través de la Arquitectura Hexagonal" en el meetup de Software Crafters Madrid conjuntamente con Scala Madrid el 21/11/2018. Descuento en cursos CodelyTV Pro por verla: http://bit.ly/codelytv19e
4. Session goal
🤔
Rethinking time
Functional Programming intro based on Hexagonal Architecture
Reviewing different technics to solve the same problems with more modularity
14. Acceptance test
Unit test Integration test
CONTROLLER
REPOS
MODELS
SERVICES
APPLICATION SERVICE
D
A
I
Request flow
IMPLEMENTATION
🗺 Design principles > 🎯 Hexagonal Architecture
Port
Adapter
21. What is a functional architecture?
DSL1
DSL2
DSLN
…
🗺 Design principles > 🦄 Functional Architectures
A FUNCTIONAL LANGUAGE IS A DOMAIN-
SPECIFIC LANGUAGE FOR DEFINING
DOMAIN-SPECIFIC LANGUAGES
22. The DSLs of your application
• Infrastructure DSLs
• HTTP
• Databases
• Messaging
• Application-specific DSLs
• Use cases
• Repos
• …
HTTP
SERVICE DSL
SQL
REPO DSL
🗺 Design principles > 🦄 Functional Architectures
23. Are hexagonal and functional architectures like apples and oranges?
HTTP
SERVICE DSL
SQL
REPO DSL
CONTROLLER
SERVICE IMPL.
ORM
DB
ADAPTER/INTERP.
PORT/DSL
🗺 Design principles > 🦄 Functional Architectures
24. There are no exceptions • Everything is either a port or an adapter
• Every adapter implements a given port
• Adapters are implemented on top of other ports
VideoPostController
VideoRepositoryVideoRepoC
DoobieMySqlVideoRepo
VideoCreator
HTTP
🗺 Design principles > 🦄 Functional Architectures
28. 🗺 Design principles > 🤔 Rethinking time
Final goals
• Decouple from infrastructure
• Testing easiness (test doubles when testing out use cases)
• Change tolerance
• ¡Same goals!
29. 🗺 Design principles > 🤔 Rethinking time
Differences: More decoupling and indirection levels
• Controllers:
• Consistency (APIs for all DSLs) vs. Specific needs
• Conclusion: We need industry standards. PHP-FIG as example to follow
• Application Services:
• Consistency (APIs for all DSLs) vs. Specific needs
• Reuse use case APIs in clients (e.g. user clients)
34. final class VideoPostController extends ApiController
{
private $creator;
public function __construct(VideoCreator $creator)
{
parent::__construct();
$this->creator = $creator;
}
public function __invoke(Request $request)
{
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoctrineMySqlVideoRepo
35. final class VideoPostController extends ApiController
{
private $creator;
public function __construct(VideoCreator $creator)
{
parent::__construct();
$this->creator = $creator;
}
public function __invoke(Request $request)
{
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoctrineMySqlVideoRepo
36. final class VideoPostController extends ApiController
{
private $creator;
public function __construct(VideoCreator $creator)
{
parent::__construct();
$this->creator = $creator;
}
public function __invoke(Request $request)
{
$id = new VideoId($request->get('id'));
$type = new VideoType($request->get('type'));
$title = new VideoTitle($request->get('title'));
$url = new VideoUrl($request->get('url'));
$courseId = new CourseId($request->get('course_id'));
$this->creator->create(
$id, $type, $title, $url, $courseId
);
return new ApiHttpCreatedResponse();
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoctrineMySqlVideoRepo
37. final class VideoPostController extends ApiController
{
private $creator;
public function __construct(VideoCreator $creator)
{
parent::__construct();
$this->creator = $creator;
}
public function __invoke(Request $request)
{
$id = new VideoId($request->get('id'));
$type = new VideoType($request->get('type'));
$title = new VideoTitle($request->get('title'));
$url = new VideoUrl($request->get('url'));
$courseId = new CourseId($request->get('course_id'));
$this->creator->create(
$id, $type, $title, $url, $courseId
);
return new ApiHttpCreatedResponse();
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoctrineMySqlVideoRepo
38. final class VideoCreator
{
private $repository;
private $publisher;
public function __construct(
VideoRepository $repository,
DomainEventPublisher $publisher
) {
$this->repository = $repository;
$this->publisher = $publisher;
}
public function create(
VideoCreator - Application Service/Use case
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepoDoctrineMySqlVideoRepo
39. final class VideoCreator
{
private $repository;
private $publisher;
public function __construct(
VideoRepository $repository,
DomainEventPublisher $publisher
) {
$this->repository = $repository;
$this->publisher = $publisher;
}
public function create(
VideoCreator - Application Service/Use case
VideoPostController VideoRepositoryVideoCreator
DoctrineMySqlVideoRepo
45. interface VideoRepository
{
public function save(Video $video): void;
public function search(VideoId $id): ?Video;
public function searchByCriteria(Criteria $criteria): Videos;
}
VideoRepository - Domain contract/Port
VideoPostController VideoRepositoryVideoCreator
DoctrineMySqlVideoRepo
46. DoobieMySqlVideoRepository - Infrastructure implementation/Adapter
VideoPostController VideoRepositoryVideoCreator
final class DoctrineMySqlVideoRepository implements VideoRepository
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function save(Video $video): void
{
$this->entityManager()->persist($entity);
DoctrineMySqlVideoRepo
47. final class DoctrineMySqlVideoRepository implements VideoRepository
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function save(Video $video): void
{
$this->entityManager()->persist($entity);
$this->entityManager()->flush($entity);
}
// ...
}
DoobieMySqlVideoRepository - Infrastructure implementation/Adapter
VideoPostController VideoRepositoryVideoCreator
DoctrineMySqlVideoRepo
48. final class DoctrineMySqlVideoRepository implements VideoRepository
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function save(Video $video): void
{
$this->entityManager()->persist($entity);
$this->entityManager()->flush($entity);
}
// ...
}
DoobieMySqlVideoRepository - Infrastructure implementation/Adapter
VideoPostController VideoRepositoryVideoCreator
DoctrineMySqlVideoRepo
50. final class VideoPostController(creator: VideoCreator) {
def post(id: String, title: String): StandardRoute = {
creator.create(VideoId(id), VideoTitle(title))
complete(HttpResponse(Created))
}
}
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
51. final class VideoPostController(creator: VideoCreator) {
def post(id: String, title: String): StandardRoute = {
creator.create(VideoId(id), VideoTitle(title))
complete(HttpResponse(Created))
}
}
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
52. final class VideoPostController(creator: VideoCreator) {
def post(id: String, title: String): StandardRoute = {
creator.create(VideoId(id), VideoTitle(title))
complete(HttpResponse(Created))
}
}
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
53. object VideoQuality {
val maxQuality = 50
val minQuality = 0
}
final case class VideoQuality(value: Int) {
require(value <= maxQuality, s"Video quality value greater than $maxQuality")
require(value >= minQuality, s"Video quality value less than $minQuality")
def increase(): VideoQuality = {
if (value >= maxQuality) this
else copy(value + 1)
}
}
OOP: Data+Behaviour
👤 Domain models > 🎯 OOP > VideoQuality Value Object
54. object VideoQuality {
val maxQuality = 50
val minQuality = 0
}
final case class VideoQuality(value: Int) {
require(value <= maxQuality, s"Video quality value greater than $maxQuality")
require(value >= minQuality, s"Video quality value less than $minQuality")
def increase(): VideoQuality = {
if (value >= maxQuality) this
else copy(value + 1)
}
}
Behaviour
Simpler API
+ domain semantics
+ immutability
👤 Domain models > 🎯 OOP > VideoQuality Value Object
55. case class Video(
id: VideoId,
title: VideoTitle,
quality: VideoQuality
) {
def increaseQuality(): Video =
copy(quality = quality.increase())
}
VideoQualityIncreaser Video
increaseQuality()
Behaviour
Rich domain model - Tell don’t ask
👤 Domain models > 🎯 OOP > Video Entity
56. final class VideoPostController(creator: VideoCreator) {
def post(id: String, title: String): StandardRoute = {
creator.create(VideoId(id), VideoTitle(title))
complete(HttpResponse(Created))
}
}
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
57. final class VideoPostController(creator: VideoCreator) {
def post(id: String, title: String): StandardRoute = {
creator.create(VideoId(id), VideoTitle(title))
complete(HttpResponse(Created))
}
}
VideoPostController - HTTP API Controller
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
58. final class VideoCreator(
repository: VideoRepository,
publisher: MessagePublisher
) {
def create(id: VideoId, title: VideoTitle): Unit = {
val video = Video(id, title)
repository.save(video)
publisher.publish(VideoCreated(video))
}
}
VideoCreator - Application Service/Use case
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
59. final class VideoCreator(
repository: VideoRepository,
publisher: MessagePublisher
) {
def create(id: VideoId, title: VideoTitle): Unit = {
val video = Video(id, title)
repository.save(video)
publisher.publish(VideoCreated(video))
}
}
VideoCreator - Application Service/Use case
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
60. final class VideoCreator(
repository: VideoRepository,
publisher: MessagePublisher
) {
def create(id: VideoId, title: VideoTitle): Unit = {
val video = Video(id, title)
repository.save(video)
publisher.publish(VideoCreated(video))
}
}
VideoCreator - Application Service/Use case
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
65. object VideoQuality {
val maxQuality = 50
val minQuality = 0
}
final case class VideoQuality(value: Int) {
require(value <= maxQuality, s"Video quality value greater than $maxQuality")
require(value >= minQuality, s"Video quality value less than $minQuality")
def increase(): VideoQuality = {
if (value >= maxQuality) this
else copy(value + 1)
}
}
VO: Raise exceptions
for invalid states
⚡Error Handling > 🎯 OOP > Raise exceptions
95. Differences on APIs/layers implementation
• Same goal: Decouple from infrastructure
• 🦄 FP:
• One step forward to avoid infrastructure leaks
• Again, through type classes: a better API
• More complexity: monads, applicatives, etc.
% Implementation > 🤔 Rethinking time > 🏗 APIs
96. Differences on error handling
• 🦄 FP:
• FP tell no lies (or, rather, doesn’t hide anything): more robust contracts using
type system
• Declarative: more abstract about how the error will be raised (exceptions,
Option, Either, etc)
% Implementation > 🤔 Rethinking time > ⚡Error Handling