4. Manel López / @mloptor #DrupalcampSpain2015
“El dominio es el mundo del
negocio”
Ideas, conocimiento y datos del
problema que intentamos resolver.
Expertos del dominio
Dominio
5. Manel López / @mloptor #DrupalcampSpain2015
Objeto del negocio
Objetos o entidades abstractas
que conforman el dominio.
6. Manel López / @mloptor #DrupalcampSpain2015
UniversidadPeriódico
Profesor
Alumno Asignatura
Departament
o
Editor
Periodista
Noticia
Anuncio
7. Manel López / @mloptor #DrupalcampSpain2015
Reglas de negocio
8. Manel López / @mloptor #DrupalcampSpain2015
Modelo del dominio
Nuestra solución al problema
Conocimiento estructurado del
problema
Código, prosa o un diagrama.
Refleja el conocimiento que tenemos
del dominio
9. Manel López / @mloptor #DrupalcampSpain2015
Lógica de negocio / Capa de negocio
“La parte de nuestro software que
mantiene el modelo del dominio y
codifica las reglas de negocio”
10. Manel López / @mloptor #DrupalcampSpain2015
Aplicaciones web y Frameworks
11. Manel López / @mloptor #DrupalcampSpain2015
¿Dónde se
implementa la
lógica de
negocio?
12. Manel López / @mloptor #DrupalcampSpain2015
ControladorPetición
Modelo
Vista
Persistencia
de datos
Respuesta
Presentación
de los datos
13. Manel López / @mloptor #DrupalcampSpain2015
ControladorPetición
Modelo
Vista
Persistencia
de datos
Respuesta
Lógica de
negocio
Presentación
de los datos
14. Manel López / @mloptor #DrupalcampSpain2015
Problemas
Alto nivel de dependencia
Difícil testeo
No SRP
HTTP
Tamaño
15. Manel López / @mloptor #DrupalcampSpain2015
Hi! I'm your
controller
25. Manel López / @mloptor #DrupalcampSpain2015
interface log {
public function message($message);
public function error($message);
}
class LogService implements log {
public function __construct($factory) {
$this->loggerFactory = $factory;
}
public function message($message) {
$this->loggerFactory->get('my_module')->error($message);
}
public function error($message) {
$this->loggerFactory->get('my_module')->error($message);
}
}
Adaptador
Puerto
26. Manel López / @mloptor #DrupalcampSpain2015
Application Layer
Domain
Model
27. Manel López / @mloptor #DrupalcampSpain2015
Application Layer
Domain
Model
28. Manel López / @mloptor #DrupalcampSpain2015
Application Layer
Domain
Model
29. Manel López / @mloptor #DrupalcampSpain2015
Application Layer
Domain
Model
36. Manel López / @mloptor #DrupalcampSpain2015
Patrones de diseño
37. Manel López / @mloptor #DrupalcampSpain2015
Entidad
Representan a los objetos del dominio
Estado + Comportamiento
Contener lógica del negocio
Modelo Anémico
Identidad
47. Manel López / @mloptor #DrupalcampSpain2015
Shopping Cart
1. CLIENTE Quiero ver un listado de productos para elegir lo que quiero
comprar
2. CLIENTE Quiero ser capaz de elegir el producto que quiero comprar y
añadirlo a mi cesta de la compra
a. Si un producto está ya en la cesta incrementaremos la cantidad
3. CLIENTE Quiero ser capaz de eliminar productos de mi cesta si he
cambiado de opinión
4. CLIENTE Quero poder ver el coste total de todos los productos de mi
cesta en todo momento.
5. CLIENTE Quiero que se guarde mi cesta de la compra por si quiero
pensarlo mejor
48. Manel López / @mloptor #DrupalcampSpain2015
Repositorios de código
Shopping Cart Business Logic
https://github.com/malotor/basket
Shopping Cart Drupal 8 module
https://github.com/malotor/drupal_ecommerce
55. Manel López / @mloptor #DrupalcampSpain2015
/src/Domain/Entity/Product.php
class Product implements Item
{
private $id;
private $quantity;
private $price;
public function __construct($id, $price, $quantity = 1) {}
public function getId() {}
public function getQuantity() {}
public function getAmount() {}
public function increaseQuantity() {}
}
56. Manel López / @mloptor #DrupalcampSpain2015
/src/Domain/Entity/Product.php
class Product implements Item
{
private $id;
private $quantity;
private $price;
public function __construct($id, $price, $quantity = 1) {}
public function getId() {}
public function getQuantity() {}
public function getAmount() {}
public function increaseQuantity() {}
}
Estado
Comportamiento
57. Manel López / @mloptor #DrupalcampSpain2015
/src/Domain/Entity/Basket.php
class Basket
{
private $items = [];
public function countItems() {}
public function addItem(Item $item) {}
public function getItem($itemId) {}
public function removeItem($itemId){}
public function getItems(){}
public function totalAmount(){}
}
58. Manel López / @mloptor #DrupalcampSpain2015
/src/Domain/Entity/Basket.php
class Basket
{
public function addItem(Item $item)
{
$itemId = $item->getId();
if ($this->containsItem($itemId))
$this->items[$itemId]->increaseQuantity();
else
$this->items[$itemId] = $item;
}
}
“Si un producto ya existe en el carro
incrementaremos su cantidad”
59. Manel López / @mloptor #DrupalcampSpain2015
Capa Dominio - Test de integración
60. Manel López / @mloptor #DrupalcampSpain2015
/tests/Integration/Basket.php
class BasketTest extends PHPUnit_Framework_TestCase {
protected $basket;
public function setUp() {
$this->products[0] = new Product(1,10);
$this->products[1] = new Product(2,5.5);
$this->basket = new Basket();
}
public function testAddAProduct() {
$this->assertEquals(0, $this->basket->countItems());
$this->basket->addItem($this->products[0]);
$this->assertEquals(1, $this->basket->countItems());
}
}
61. Manel López / @mloptor #DrupalcampSpain2015
/tests/Integration/Basket.php
class BasketTest extends PHPUnit_Framework_TestCase {
public function testRemoveAProduct() {
$this->basket->addItem($this->products[0]);
$this->basket->addItem($this->products[1]);
$this->basket->removeItem(1);
$this->assertEquals(1, $this->basket->countItems());
}
public function testIncreaseProductQuantity() {
$this->assertEquals(1, $this->products[0]->getQuantity());
$this->basket->addItem($this->products[0]);
$this->basket->addItem($this->products[0]);
$this->assertEquals(1, $this->basket->countItems());
$this->assertEquals(2, $this->products[0]->getQuantity());
}
}
62. Manel López / @mloptor #DrupalcampSpain2015
/tests/Integration/Basket.php
class BasketTest extends PHPUnit_Framework_TestCase {
public function testCartTotalAmount() {
$this->basket->addItem($this->products[0]);
$this->basket->addItem($this->products[0]);
$this->basket->addItem($this->products[1]);
$this->assertEquals(25.5, $this->basket->totalAmount());
}
}
64. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
use malotorshoppingcartApplicationRepositoryProductRepository;
use malotorshoppingcartApplicationRepositoryBasketRepository;
class ShoppingCartService
{
private $productRepository;
private $basketRepository;
public function __construct(
ProductRepository $productRepository,
BasketRepository $basketRepository
)
{...}
}
65. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
use malotorshoppingcartApplicationRepositoryProductRepository;
use malotorshoppingcartApplicationRepositoryBasketRepository;
class ShoppingCartService
{
private $productRepository;
private $basketRepository;
public function __construct(
ProductRepository $productRepository,
BasketRepository $basketRepository
)
{...}
}
Puertos
66. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
use malotorshoppingcartApplicationRepositoryProductRepository;
use malotorshoppingcartApplicationRepositoryBasketRepository;
class ShoppingCartService
{
private $productRepository;
private $basketRepository;
public function __construct(
ProductRepository $productRepository,
BasketRepository $basketRepository
)
{...}
}
Interfaces
67. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
class ShoppingCartService
{
{...}
public function addProductToBasket($productId, $baskedId)
{...}
public function removeProductFromBasket($productId, $basketId)
{...}
public function getProductsFromBasket($basketId) {...}
public function getBasketTotalAmount($basketId) {...}
}
API para Infraestructura
68. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
class ShoppingCartService
{
public function addProductToBasket($productId, $baskedId)
{
$basket = $this->basketRepository->get($baskedId);
$product = $this->productRepository->get($productId);
$basket->addItem($product);
$this->basketRepository->save($basket);
}
{...}
}
69. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
class ShoppingCartService
{
public function addProductToBasket($productId, $baskedId)
{
$basket = $this->basketRepository->get($baskedId);
$product = $this->productRepository->get($productId);
$basket->addItem($product);
$this->basketRepository->save($basket);
}
{...}
}
Recupera datos al los repositorios
70. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
class ShoppingCartService
{
public function addProductToBasket($productId, $baskedId)
{
$basket = $this->basketRepository->get($baskedId);
$product = $this->productRepository->get($productId);
$basket->addItem($product);
$this->basketRepository->save($basket);
}
{...}
}
Ejecutará la lógica de negocio
71. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
class ShoppingCartService
{
public function addProductToBasket($productId, $baskedId)
{
$basket = $this->basketRepository->get($baskedId);
$product = $this->productRepository->get($productId);
$basket->addItem($product);
$this->basketRepository->save($basket);
}
{...}
}
Persiste los datos
72. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
class ShoppingCartService
{
public function addProductToBasket($productId, $baskedId)
{
$basket = $this->basketRepository->get($baskedId);
$product = $this->productRepository->get($productId);
$basket->addItem($product);
$this->basketRepository->save($basket);
}
{...}
}
Patrón Fachada
73. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Service/ShoppingCartService.php
class ShoppingCartService
{
public function removeProductFromBasket($productId, $basketId)
{
$basket = $this->basketRepository->get($basketId);
$basket->removeItem($productId);
$this->basketRepository->save($basket);
}
public function getProductsFromBasket($basketId)
{
$basket = $this->basketRepository->get($basketId);
return $basket->getItems();
}
public function getBasketTotalAmount($basketId)
{
$basket = $this->basketRepository->get($basketId);
return $basket->totalAmount();
}
}
80. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Factory/Product.php
use malotorshoppingcartDomainEntityProduct;
class ProductFactory
{
static public function create($object) {
return new Product(
$object->id,
$object->price,
$object->quantity
);
}
}
81. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Factory/Product.php
use malotorshoppingcartDomainEntityProduct;
class ProductFactory
{
static public function create($object) {
return new Product(
$object->id,
$object->price,
$object->quantity
);
}
}
stdClass new Product()
82. Manel López / @mloptor #DrupalcampSpain2015
/src/Application/Factory/Basket.php
use malotorshoppingcartDomainEntityBasket;
class BasketFactory
{
static public function create($products = array()) {
$basket = new Basket();
foreach ($products as $object) {
$basket->addItem(
ProductFactory::create($object) );
}
return $basket;
}
}
83. Manel López / @mloptor #DrupalcampSpain2015
Empaquetar y distribuir
129. Manel López / @mloptor #DrupalcampSpain2015
“There are no
dumb
questions”
130. Manel López / @mloptor #DrupalcampSpain2015
Referencias
Hexagonal architecture - Alistair Cockburn
http://alistair.cockburn.us/Hexagonal+architecture
Desdrupalizando la lógica de negocio - Carles Climent
https://vimeo.com/112570854
The Clean Architecture - Robert C. Martin
http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
What is Hexagonal architecture - Philip Brown
http://culttt.com/2014/12/31/hexagonal-architecture/
131. Manel López / @mloptor #DrupalcampSpain2015
Referencias
Hexagonal Architecture - Chris Fidao
http://userscape.com/laracon/2014/chrisfidao.html
http://fideloper.com/hexagonal-architecture
Hexagonal Architecture - Carlos Buenosvinos
http://www.slideshare.net/carlosbuenosvinos/hexagonal-architecture-php-
barcelona
Cuando hablamos de desacoplamr la lógica de negocio
Para comprender el concepto de lógica de negocio me gustaría primer repasar algunos conceptos previos.
Uno de estos primeros conceptos el el de Domino .
“El dominio es el mundo del negocio”.
El dominio son las ideas, conocimiento y datos del problema que intentamos resolver.
El conocimiento que tenemos del dominio suele provenir de los “expertos del dominio”
Los objetos del dominio son los objetos o entidades abstractas que forman el dominio.
Generalmente representan a entidades lógica en el espacio del dominio del problema.
Ejemplos
Las reglas de negocio son aquellas que nos dicen cómo debe se comporta el dominio.
Definen las relaciones , procesos y restricciones entre los objetos del dominio.
El modelo del dominio es “nuestra solución al problema”.
El modelo del dominio es el conocimiento estructurado relacionado a un determinado problema.
El modelo del dominio puede ser código, prosa o un diagrama. Lo importante es que todos los que participen en lo puedan entender.
La lógica de negocio es la parte de nuestro software que mantiene el modelo del dominio y codifica las reglas de negocio.
La lógica de negocio no tiene nada que ver con las presentación de los datos y con su persistencia.
En el ámbito del desarrollo web lo normal es que usemos un framework web.
En ese caso, ¿Donde codificamos la lógica de negocio?
Todos los frameworks ( casi todos ) actuales siguen el patron MVC.
En el MVC realizamos las peticiones a los controladores, este obtiene los datos necesarios del modelo, procesa esos datos y con la vista genera un respuesta para el cliente.
Por tanto, lógica de negocio se implementa en los controladores.
Este patrón genera una serie de problemas:
Alto nivel de acoplamiento.
La codificación de las reglas se entremezcla con el acceso a los datos y la presentación de estos al usuario.
Difícil testeo
Al tener la lógica dentro de los controladores no podemos testearla de forma unitaria.
No Single Responsability Principle
SPR es uno de los principios de la programación orientada a objetos introducido por Robert C. Martin.
SOLID (Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion)
SPR dice que una clase debe tener solo una responsabilidad. Si se asumen mas responsabilidades será mas sensible a los cambios.
Solo accesible mediante HTTP
La lógica del negocio sólo es accesible desde una petición HTTP. En caso de que necesitemos acceder desde cualquier otro medio no podríamos reutilizar nuestro código.
Tamaño de los controladores
Nuestros controladores tienden a crecer de una manera descontrolada. y acabmos teniendo esto.,..
¿Cómo podemos hacer que nuestra lógica esté desaclopada? Es decir, ¿Que la implementación no dependa del framework?
La arquitectura hexagonal es una aquitectura de software que fue introducida por Alistair CockBurn. Tambíen es conocida como arquitectura de puertos y adaptadores.
Esta arquitectura promueve la separación de la diferentes responsabilidades de nuestra aplicación en diferentes capas.
En el centro de la arquitectura reside la capa del Dominio. Aquí es donde implementaremos nuestro modelo del dominio. Toda la lógica de negocio debe residir aquí.
Recubriendo al dominio. Situaremos la capa de aplicación.
Esta capa tiene la función de hacer de API de nuestra lógica y ofrecer sus servicios al mundo exterior.
Para poder conectar con nuestra lógica diferentes puertos en la capa de aplicación. La arquitectura accederá a esos puertos para obtener o enviar información al mundo exterior.
Finalmente tenemos la capa de infraestructura. En esta capa es donde realizamos todos los detalles técnicos y la presentación de los datos.
Aquí encontramos las implementación de los diferentes adaptadores que nos permiten conectar con la capa de la aplicación.
Lo que vamoa a hacer al final es la arquitectura del Mazinger Z
Como hemos visto en las anteriores transparencias. Uno de los puntos clave de esta arquitectura son los Puertos y los adaptadores.
Podemos definir un puerto como una interfaz y un adaptador es una clase que implementa esa interfaz.
En una arquitectura hexagonal podemos encontrar diferentes tipos de puertos.
Pueden haber puertos de entrada, salida o entrada y salida.
Los adaptadores son intercambiables. Podemos sustituir uno por otro sin que nuestra lógica se vea afectada.
En cualquier momento podemos añadir nuevos puertos a nuestra lógica.
Las fronteras entre capas son un punto importante a tener en cuenta.
Para mantener la independencia de la capa es importante tener en cuenta que los servicios de una capa solo accederán a los recursos de una capa inferior. Desde la capa de infraestructura no podemos acceder a un servicio del domino o lanzar un evento, es necesario que esto sea realizado por la capa de aplicación.
Esto nos asegura que podemos realizar los cambios en las capas de aplicación y dominio siempre que su interfaz no cambie.
Nuestra lógica es agnóstica del mundo exterior. Podemos acceder a ella desde una aplicación web, una aplicación de consola o una proceso por lotes.
Ahora es más fácil testear la lógica o al menos tiene un coste menor. Los test unitarios / integración tienen un menor costes que los test de sistema/ end to end.
La lógica está aislada. Debido a esto los cambios que hacemos en una capa no afectan a las demás mientras no se toque la lógica. Esto nos permite poder hacer más refactoring y generar menos deuda técnica.
Antes de revisar el código del ejemplo me gustaría explicar una serie de patrones de diseño.
El primer es el patrón “Adaptar”. Como ya hemos comentado el concepto básico de la arquitectura se basa en el.
Otro patrón importante es el de factoria. Las factorías nos permiten crear objetos de cierto tipo sin conocer detalles concretos de la forma en que se crean.
La función principal de los repositorios es facilitarnos el acceso a la información. Dentro de esta estructura intervienen diferentes elementos.
Un cliente lanza un query a un repositorio ( dame el producto con id=1, o los producto con precio < 10 € )
El respository lanza esta “query” a un proveedor de datos ( base de datos ) que nos los devuelve de una forma “estandar” un stdclass o un array.
Este objeto devuelto se manda a la factoria donde convertira es objeto “sin personalidad” en un objeto concreto en este caso un objeto producto.
El patrón fachada es el que implementan la mayoría del servicios del modelo del dominio. En este patrón el objeto fachada conoce el comportamiento de los distintos componenetes del sistema y los orquesta para conseguir un resultado de forma transparente a la clase cliente.
Pero bueno, la arquitectura hexagonal no es el Santo Grial
Como hemos visto mantener la lógica de negocio separada nos aporta escalabilidad, mantenibilidad y fiabilidad pero por otra parte añade una complejidad al proyecto.
Nuestra misisón como desarrolladores es saber valorar cada proyecto concreto.
El en libro Implementing Domain Driven Design podemos ver una guia.
Si nuestro proyecto está muy centrado a datos y donde solo tenemos operaciones CRUD no vale la peta introducir este nivel de complejida.
A medida que augmentan las historias de usuario y va apareciendo una complejidad en el negocio nos vamos acercando a la zona DDD.
Hay que tener cuidado con los proyectos que pueden tener una cierta complejidad oculta.
Cuando tenemos escenarios inciertos, requisitos no definmos , estros proyectos ya son subceptibles de hacer DDD.
El ultimo caso es cuando no conocemos nada del dominio y debemos ir generando los models con sucesivas iteraciones. En este caso el uso de estas prácticas es muy recomendable.
Cliente tiene una página de cupones de descuento y quiere incorporar un carrito de la compra. La página está desarrolada en Drupal 8 y tiene un tipo de contenido producto y una vista que muestra los últimos cupones.
En el repositorio de la capa del dominio podeis ver como el diseño va emergiendo a partir de los tests. He colocado en cada commit uno paso del proceso TDD, RED, GREEN y REFACTOR.
La capa del dominio está compuesta por solo dos entidades. Una cesta de la compra que contiene productos.
Los archivos de la lógica están dividimos en 2 carpetas de primer nivel, una para las clases de la capa de Aplicación y otra para las clases de las capa de dominio. Dentro he creado una carpeta por cada patrón usado.
La primera entidad que nos encontramos es la clase Producto.
Como podemos ver esta entidad tiene una Identidad, un estado y también contiene comportamiento.
Un productos está compuesto por un id, un precio unitario y la cantidad de esos productos.
Una vez creado podemos incrementar la cantidad de es producto y recuperar el importe total.
La primera entidad que nos encontramos es la clase Producto.
Como podemos ver esta entidad tiene una Identidad, un estado y también contiene comportamiento.
Un productos está compuesto por un id, un precio unitario y la cantidad de esos productos.
Una vez creado podemos incrementar la cantidad de es producto y recuperar el importe total.
La segunda entidad “Basket” que nos permite añadir y eliminar items así como recuperar el importe total de los productos que tenemos en la cesta.
Podemos ver como el dominio encapsula las reglas de negocio. En este caso vemos cómo implementamos una reglas “Si un producto existe en el carro incrementaremos su cantidad”
Podemos ver un test de integración de las dos entidades y vemos que que funcionan perfectamente.
Podemo ver un test de integración de las dos entidades y vemos que que funcionan perfectamente.
Podemo ver un test de integración de las dos entidades y vemos que que funcionan perfectamente.
Una vez revisado las clases del modelo del dominio vamos a crear un servicio ShoppingCart de la capa de la aplicación que nos permita acceder a los recursos del modelo.
No olvidemos que estos servicios son la API publica de nuestra aplicación.
Lo primero que vemos es que la clase ShoppingCart se construye con 2 repositories.
ProductRepository
BasketREpository
Estos son los dos puertos donde vamos a obtener los datos.
Estos repositorios son los que deberemos implementar desde la capa de Infraestructura y pasar a la lógica.
Lo primero que vemos es que la clase ShoppingCart se construye con 2 repositories.
ProductRepository
BasketREpository
Estos son los dos puertos donde vamos a obtener los datos.
Estos repositorios son los que deberemos implementar desde la capa de Infraestructura y pasar a la lógica.
El resto de métdos de la clase son la API pública. Podemos ver que tenemos implementados todos los posibles casos de uso.
Estos métodos hacern uso de los repositorios para obtener datos.
Los servicios de la capa de aplicación implementan el patron fachada. Esto servicios saben a que clases de nuestra arquitectura deben llamar para devolver un resultado.
Aquí el servicio accederá a los dos repositorios para recuperar el producto y la cesta de compra la compra.
Un vez los tenga realizará las acciones adecuadas.
Devolverá la cesta al repositorio para que realice la persistencia de este.
Ejecutará la lógica de negocio
Un procedimientos similiar siguen los otros métodos publicos.
Es importante ver que estos repositorios trabajan sobre entidades del dominio.
Es importante ver que estos repositorios trabajan sobre entidades del dominio
Es importante ver que estos repositorios trabajan sobre entidades del dominio
Por último nos queda revisar la factorias. Estas tiene las función de proveer a la capa de infraestructura de las entidades.
En este caso estamos convirtiend un “objeto estandar” en un objeto “Product”
Lo primero que haremos es cargar nuestra lógica de negocio. Entedremos nuestra lógica disponible a partir de el namespace malotor/shoppingcart
Cuando empezamos un nuevo proyecto todos lqueremos que nuestro proyecto sea mantenible, escalable, fiable, que no tenga errores o que tenga los mínimos.