Despu茅s del intento fallido de hacer un live-coding con este tema, por problemas de mi conexi贸n tuve que cancelarlo. Intente grabarlo en un v铆deo y subirlo, pero es un tema que se estaba haciendo largo y la edici贸n iba a llevar un buen tiempo sumado que odio la edici贸n. As铆 que, lo voy a hacer escrito, a la vieja usanza.
No se preocupen que lo ir茅 haciendo paso a pasa para que se entienda lo mejor posible.
C贸digo de un suscriptor que vamos a trabajar
Este es el correo electr贸nico que me envi贸 Shonen donde me consulta c贸mo podr铆a refactorizar su c贸digo. Y el c贸digo es el siguiente:
La idea de este art铆culo es analizar el c贸digo, ver sus problemas y c贸mo lo podemos encarar a una mejor soluci贸n. Siempre enfoque mis desarrollos a dividirlos en Casos de Uso. As铆 que veremos de que se trata esto, sus beneficios y su implementaci贸n.
Analizando el c贸digo de Shonen
Nos podemos dar cuenta que es un m茅todo de un controlador y que se encarga de actualizar los datos del perfil del usuario logueado.
Si bien este c贸digo cumple con su cometido pero tiene varios problemas:
- No es un c贸digo que escale.
- No es re utilizable.
- Y es dif铆cil de testear de forma unitaria.
Estos problemas convierten al c贸digo en un c贸digo de baja calidad.
Aplicando refactorizaci贸n a Casos de Uso
驴Qu茅 es un Caso de Uso?
Un caso de uso es una acci贸n que puede hacer un usuario con nuestro sistema. Y son independientes del framework o packages que utilicemos.
En este ejemplo, el caso de uso general es Actualizar Perfil. Pero a su vez, vemos que est茅 realiza cierta l贸gica para generar el avatar del usuario y otra cierta l贸gica para generar la contrase帽a.
Entonces podr铆amos decir que estas funcionalidades son dos casos de uso m谩s: generar avatar y generar contrase帽a. Lo podr铆amos diagramar de la siguiente manera:
Para el que no sabe UML, el dibujo de la persona representa cualquier usuario o sistema que llama a la actualizaci贸n de perfil.
Cada circulo representa un caso de uso. Para que se entienda m谩s, al caso de uso general de Actualizar Perfil le puse el mismo nombre que tiene el m茅todo. Esto no ser铆a lo correcto pero lo hice para que se entienda mejor.
Despu茅s vemos que hay dos casos de uso m谩s a los que se lo apunta con una flecha punteada y con la descripci贸n 芦include禄. Esto quiere decir que el caso de uso edit_profile (o UpdateProfile, como lo llamaremos a partir de ahora), utiliza聽los casos de uso Generar Avatar y Generar Contrase帽a.
Dicho esto, vamos a pasarlo a c贸digo.
Refactorizando a Casos de Uso
Siempre hay que tener tests automatizados antes de empezar a refactorizar c贸digo para asegurarnos que las modificaciones que vayamos haciendo no afecten a la funcionalidad.
Pero, lo bueno de refactorizar a casos de uso es que se utilizan t茅cnicas que entran dentro de las safe refactoring.聽Este tipo de t茅cnicas son tan simples que no requieren que tengamos tests previamente.
Veamos los pasos para refactorizar a casos de uso.
Primer paso: Identificar los distintos casos de uso
Esto ya lo hicimos en el an谩lisis. Vimos que tenemos los casos Actualizar Perfil, Generar Avatar y Generar Contrase帽a.
Segundo paso: Crear clases por cada caso de uso
Para nuestro ejemplo vamos a codear tres clases: UpdateProfile.php
, GenerateAvatar.php
y GeneratePassword.php
. Y cada una debe tener un 煤nico m茅todo publico:
final class UpdateProfile { public function execute() { } } final class GenerateAvatar { public function execute() { } } final class GeneratePassword { public function execute() { } }
Tercer paso: Mover el c贸digo a cada caso de uso
Identificamos que porci贸n de c贸digo pertenece a cada caso de uso, as铆 que hagamos un 芦copy&paste禄 y movamos el c贸digo a cada clase:
Y, como el caso de uso de Actualizar Perfil es el incluye los dos anteriores (acu茅rdense el diagrama UML), quedar铆a as铆:
Y el controlador simplemente quedar谩 as铆:
De esta forma estamos cumpliendo con la responsabilidad del controlador que es funcionar como un orquestador, obteniendo los datos, llamar a las clases necesarias que resuelvan la tarea y luego responder 馃挭.
Paso cuarto: Identificar dependencias y datos
Todo muy lindo, pero al copiar y pegar el c贸digo del controlador a cada clase, veremos que el IDE empezar谩 a alertarnos que hay clases que no existen (dependencias) y variables no definidas (datos).
Un claro ejemplo es el caso de uso 芦GenerateAvatar禄 donde utiliza la facade Storage
y el package WebP
(dependencias) y $request->file('avatar')
y Auth::user()->avatar
(datos).
La regla que debemos aplicar para esto es, las dependencias las inyectamos por el constructor de la clase y los datos por el m茅todo publico.
Como vemos, todo lo que son dependencias las pase por el constructor y los datos por el m茅todo execute
. Tambi茅n ya que estaba, mejore los dos IF. Al primero consulto estrictamente por null
(ya que es un objeto). Y al segundo IF tambi茅n puse que la comparaci贸n sea estricta con !==
y pasa el string a una constante.
Como la clase GenerateAvatar
es utilizada por UpdateProfile
, este 煤ltimo va a tener estas mismas dependencias para poder pas谩rselas a GenerateAvatar
, como as铆 tambi茅n los datos que UpdateProfile necesita.
Y a su vez, el controlador debe pasarle las dependencias y datos a UpdateProfile
.
Esto no es la mejor forma de hacerlo, pero eso es cosa de un segundo art铆culo. Por ahora quedara as铆:
Adem谩s de agregar las dependencias y los datos en el execute
en esta clase, tambi茅n le hice una mejora.
Si ven el c贸digo anterior de la clase UpdateProfile
la primer linea del m茅todo execute
era as铆:
$user = User::where('email', Auth::user()->email)->first();
Y esta llamada a base de datos para obtener el usuario esta de m谩s, porque en Auth::user()
ya tenemos el usuario. As铆 que, la reemplace por:
$user = Auth::user();
Por otro lado, el controlador quedo as铆:
Quinto paso: Atacar cada clase
Ahora que tenemos cada caso de uso con su propia responsabilidad es mucho m谩s f谩cil trabajar y refactorizar sobre cada una de ellas.
Empezemos a refactorizar GenerateAvatar:
- Para el primer IF, aplicare una Cl谩usula de Guarda (si no sabes lo que es, podes ver m谩s info haciendo click aqu铆).
if ($avatar === null) { return $authAvatar; }
- La variable $ruta nada mas se utiliza en la 煤ltima linea, as铆 que vamos a eliminarla y hacer lo siguiente.
$this->webp->make($imagen)->save(public_path("/storage/users/{$tiempo}.web"));
- El segundo bloque de IF utiliza variables que no se usan en la continuaci贸n del proceso. As铆 que, lo mejor ser铆a extraer este bloque a un m茅todo privado.
- Tambi茅n aplicaremos una cl谩usula de guarda en este nuevo m茅todo y extraeremos la eliminaci贸n de archivos a un nuevo m茅todo.
- La variable
$image
es una variable temporal, as铆 que la eliminare y directamente utilizare$avatar
. - Extraje algunos valores m谩s a constantes.
- Y por 煤ltimo, en lugar de asignar la ruta del nuevo avatar al modelo user, vamos a retornar la ruta. Porque recordemos que esta caso de uso es genera el avatar, no lo actualiza. El que lo actualizara finalmente ser谩 el caso de uso
UpdateProfile
. Finalmente la clase nos quedar谩 as铆:
Beneficios de los Casos de Uso
Al separar nuestro c贸digo en casos de uso ganamos:
- Orden: toda la logica no esta en el controlador.
- Legibilidad: se entiende mejor lo que hace cada clase.
- Testeabilidad: ahora podemos agregar tests unitarios a cada una de las clases (lo veremos en un pr贸ximo art铆culo).
- Reutilizaci贸n: puede que en otra parte de nuestro sistema tambi茅n necesitemos generar el avatar.
- Aplicamos caracter铆sticas de SOLID: como por ejemplo el Principio de Responsabilidad Unica.
- Alta cohesi贸n: ya que cada una de sus variables, constantes y m茅todos tienen que ver con el prop贸sito de la clase, a esto se lo llama alta cohesi贸n y es una de las caracter铆sticas claves que tiene que tener toda clase para ser una clase de calidad.
Conclusi贸n
Y quedan m谩s cosas por aplicar que mejorar谩n nuestro c贸digo. Y veremos en pr贸ximos art铆culos. Sin duda, encarar nuestros desarrollos por medio de los casos de uso nos dan much铆simos beneficios y nos brindan un c贸digo de alta calidad. Por el momento, lo cortamos ac谩 para no extenderla tanto.
Espero que les haya gustado y les agradecer铆a mucho que compartan este art铆culo en sus redes sociales. Nos vemos 馃槈馃.
Hola excelente tutorial mas bien mi duda seria como la esta la estructura de folders en tu proyectos me imagino que todos las clases de tus casos de uso estan en un folder llamado 芦UseCases禄 Y por cada m贸dulo otro subfolder llamado 芦Profile禄 Como para tu ejemplo no se si estar铆a bien de esa forma o como lo trabajas tu? Tambien por ultimo estoy confundido si casos de uso serian igual que servicios? Ya que yo toda la logica del negocio la coloco en una clase por ejemplo 芦ProfileService禄 Por eso queria saber la diferencia nose si estar铆a bien o no para aprender buenas pr谩cticas
Hola Thiago, c贸mo est谩s?
Agrup贸 por entidades. Por ejemplo, en Profile/ meter铆a todo lo relacionado con este.
Y la segunda pregunta, si, casos de uso y servicios son lo mismo pero en terminolog铆a de Arquitectura Limpia se usa UseCase.
Lo malo del nombre ProfileService es que no sabes que puede haber ah铆 adentro. Cualquier programador que quiera agregar una feature relacionada al Profile, seguramente la agregara adentro de esa clase. Es mejor usar Acci贸n+Entidad como lo hago en este art铆culo.
Me gusto mucho el articulo, y dejo mi comentario aca dado que mi consulta es muy similar a la del amigo Thiago, y siguiendo el hilo, para ese caso recomendarias ordenar la logica que ya esta en services/ProfileService.php pasarla a un subdirectorio organizado por entidad y accion quedando de esta forma: Services/Profile/ UpdateProfile.php y GenerateAvatar.php o hay otra manera de organizar mejor la logica de negocio?
Hola Jose como estas? Me alegro que te haya gustado el art铆culo!
Lo ideal es tener Profile/Services/UpdateProfile.php (o cambiar services por UseCases). Por otro lado tener Avatar/Services/GenerateAvatar.php.
Me parece lo mejor tenerlo as铆, no solo los casos de uso, sino tambi茅n su modelo, sus controllers, etc. De esta forma te ahorras tener las cosas separadas por todos lados.
Si se te hace mucho trabajo hacer esta estructura de carpetas porque es un proyecto heredado, bueno, no esta mal ponerlo como Services/Profiles/[Casos de uso].
Saludos y gracias por visitar el blog.
Seria interesante que Matias muestre un ejemplo de como podrias hacer esa estructura de carpetas y ver como tener asi un proyecto limpio separando los modelos, controladores, casos, etc.
Se te agradeceria mucho!
B谩rbaro, lo voy a agregar cuando haga la segunda parte.
Hola gente, gracias Matias un articulo de lujo es este. Solo me quedo una duda. la carpeta Service, va dentro del app/Http/Services o la ra铆z del proyecto? Muchas gracias!!
Excelente post, espero que esto ayude a los desarrolladores a ser mejores y dejar de un lado la mediocridad, si en verdad les apasiona la programaci贸n, deben hacer el esfuerzo m谩ximo para ser mejor cada d铆a en este campo y que sea un gusto programar, ese c贸digo inicial es un desastre, no lo insulto porque todos comenzamos por ah铆, conoc铆 alguien que trabajo en una empresa donde realizan exactamente ese mismo tipo de c贸digo y no quer铆a mejorar porque as铆 estaban todos sus c贸digos y todos deben programar as铆, 贸sea promoviendo las malas practicas, a compartir este articulo se dijo aver si mejoran.
Si, seguramente varios hemos empezado programando as铆. Por eso esta bueno que se promuevan las buenas practicas cuando uno las aprende.
Muchas gracias por tu comentario.
Muy bueno , algo que vi en algunos repos , es que en lugar de usar controladores , los cuales generalmente abusamos de la cantidad de responsabilidades que tiene (los explotamos con un monton de metodos) , se usan actions , seria hacer una clase por cada metodo por ejemplo CreateUserAction , esta clase tendra sus dependencias, junto con su logica y sera mucho mas facil de leer y manejar . Y estaremos cumpliendo con Single Responsibility Principle
Como va Marcelo? Exacto, son lo mismo. En otro lados lo llaman Services tambi茅n, pero el fin es el mismo.
A m铆 me gusta UseCase porque es el mismo lenguaje que usan otros sectores de la empresa, por ejemplo, an谩lisis o QA. Si a un analista le hablas de Actions o Services no te van a entender. Pero s铆 les hablas de 芦casos de uso禄 sabr谩 exactamente a que te estas refiriendo.
Hola Matias, muy bueno el post.
Ta hago una consulta, estoy reestructurando una app que tuve que hacer a los ponchazos ya que el cliente necesitaba empezar a vender y bueno esta muy atada con alambre, entonces empece a leer un poco sobre como ordenar todo un poco y hacer mejor las cosas, ya que estoy laburando solo en esto por el momento y por ahora yo lo entiendo, pero se me va a hacer mucho lio.
Bueno el tema es el siguiente, estoy tratando de aplicar un poco ddd, no a fondo, siguiente la estructura de laravel, y desacoplando lo que hice como service, que imag铆nate tengo un service por modelo y es un lio barbaro.
La pregunta en si es la siguiente: estoy armado el action para crear una orden, como esta en el ejemplo, con la funci贸n execute que recibe el request y el customer, pero necesito devolver la orden para usarla en el controller y hacer un redirect al show de la orden. Me conviene hacer un return en el execute o podr铆a agregar un m茅todo getOrder() para que una vez creada la llamo as铆 , perd贸n por lo extenso. Muchas gracias.
Como est谩s Mariano?
No te hagas problema porque es bastante com煤n hacer un sistema a los ponchazos, ver si es redituable y reci茅n ah铆 empezar a atender la deuda t茅cnica que dejamos.
Con respecto a la pregunta, por ahora no estar铆a mal que execute devuelva la orden. Cuando empieces a implementar DDD puede que tengas que hacer una modificaci贸n para esto. Pero te va a llevar un segundo porque ya vas a tener todo ordenado. Por ahora anda paso a paso como estas haciendo.
Suerte y cualquier cosa volve a darte una vuelta por ac谩.
Saludos.
Genial Matias.
Muchas gracias.
Hola, al igual que tus otros post, este esta genial muy bueno.
Solo una pregunta en la clase UpdateProfile, en m茅todo publico inyecta el request, esto no genera dependencia de lo que venga en el request, ese decir no se le deber铆a pasar como parametro todo lo que vaya a necesitar el m茅todo?…Por ejm hay una clase StoreVenta, esta me permite guardar una venta desde un erp(guarda venta, factura, comprobante, etc), ahora se esta desarrollando un POS(otro frontend), y desde ah铆 tambi茅n se realiza venta y esta se debe guardar de la misma manera en como se guarda en el erp…Mi pregunta, desde el pos podria llegar la data de una manera distinta, entonces el m茅todo(que guarda la venta del erp) de la clase creada ya no me sirve…o como podr铆a ser la estructura, para tener solo un m茅todo que se encargue de guardar una venta, venga del erp o pos, o desde una app quizas y que vayan a tener maneras distintas en como llega el request.
Saludos y gracias de antemano.
Hola Arturo, como estas? Si, sub铆 un video respondiendo a tu pregunta y a las otras preguntas que est谩n m谩s arriba: https://youtu.be/ip0lxeJOUzk
no se ven bien las carpetas en el video, ojala se pueda mejorar
recien lo veo…uhh buenisimo, muchas gracias
Me uno a los amigos que dicen que ser铆a bueno un articulo sobre estructura de tus aplicaciones y donde poner cada cosa.
Esta p谩gina vale oro :), gracias por todo .
S煤per buena la explicaci贸n muchas gracias, como puedo acceder al articulo que dice ah铆 que contin煤a y que habla sobre testeabilidad.
una pregunta, por ejemplo para un listar de productos….es necesario crear una clase useCase para eso? o en el controller puede inyectar por ejemplo el repositorio y llamar al m茅todo que trae el listado de productos?
Hola Arturo, 驴como estas? Inyectar铆as la clase concreta del repositorio o su interface?
la interfaz, pero es lo correcto eso?, puedo inyectar el repositorio directamente?….es que yo pienso que todo tiene que ir controller -> servicio -> repositorio, pero tambi茅n lo veo innecesario un useCase(o servicio) para el listar…Lo hize asi, pero vi que en el servicio solo pasaba los mismos parametros al metodo del repositorio. Tengo esa duda o como seria en casos donde no hay logica de nogocio, ejm: un listar, no tiene logica todo viene directo de un script sql…Espero se entienda, gracias
Hola Artura, disculpa la demora en contestar.
Si, los casos de uso son l贸gica de negocio. Si solo necesitas obtener los registros y devolverlos, no hace falta que crees un UseCase.
Hola Matias, me encant贸 el articulo. Tengo una duda en mi proyecto: al desarrollar el caso de uso ‘Invitar usuario a grupo de trabajo’, este se subdivide en 3 casos seg煤n si el usuario no est谩 registrado, est谩 registrado pero no tiene perfil adecuado, est谩 registrado y tiene perfil. Deber铆a crear 3 nuevas clases para estos ‘sub-casos de uso’? O realizar toda la logica en el casu de uso principal con metodos privados?
Hola Juan, como estas?
Lo podr铆as hacer como m茅todos privados si son simples. Pero lo ideal ser铆a que los modeles en 3 casos de uso separados.
Perfecto, otra duda que me surgi贸 es como manejar los errores. Si falla algo en los servicios (por ejemplo, usuario ya agregado al grupo de trabajo), como le advierto al controller?
Muy buena pregunta Juan! Fijate que el controller ejecuta los casos de usa dentro de un try/catch, por lo tanto, siempre que haya un error tenes que lanzar una exception y el controller ser谩 el encargado de handlearlo.
Buenas tardes Matias, tengo una peque帽a duda en un hipotetico caso de uso createProduct.
El controller ejecutar铆a el servicio de la siguiente forma: $this->productService->create($data);
Est谩 bien que el servicio tenga un atributo privado de tipo Product inyectado en el constructor?
Es decir, est谩 bien que el servicio haga un $this->product->save() luego de asignar los datos al producto ($this->product->fill($data))? O deber铆a hacer un new Product() en lugar de tener el producto como un atributo del servicio?
Espero se entienda la consulta, saludos!
Siempre que tengas dependencias, tenes que inyectarlas por constructor. Esto vale para cualquier clase.