CodelyTV

Entrevistas a desarrolladores y vídeos sobre buenas prácticas de programación y arquitectura de Software

Sube de nivel con CodelyTV Pro 🚀

SOLID, Domain-Driven Design, Arquitectura Hexagonal, Docker, CQRS, Microservicios, Kubernetes, Testing…

Finder kata en Scala – Dejando los bucles atrás ƛ

Ya vimos cómo enfocar esta misma kata de refactorización en PHP. Básicamente a través de pequeños procesos de refactorización, fuimos puliendo el código para aportarle semántica del dominio y mayor legibilidad.

Hoy os traemos otro enfoque de solución partiendo del repositorio con el código inicial que preparamos para el dojo de la Software Craftsmanship Barcelona. Os animamos a proponer vuestra solución vía Pull Request, ¡Seguro que salen alternativas interesantes y aprendemos más!

Estado inicial

En este enfoque veremos cómo partimos del estado inicial del código (coged aire antes de seguir con el scroll 😅):

// Post: http://codely.tv/screencasts/finder-kata-scala/
// Repo: https://github.com/CodelyTV/incomprehensible-finder-refactoring-kata-scala
package tv.codely.finderKata.algorithm
import java.util
import java.util.ArrayList
import scala.collection.JavaConverters._
import tv.codely.finderKata.algorithm.FT.FT
class Finder(private val _p: util.List[Thing]) {
def Find(ft: FT): F = {
val tr = new ArrayList[F]()
for (i < 0 until _p.size 1; j < i + 1 until _p.size) {
val r: F = new F()
if (_p.get(i).birthDate.getMillis < _p.get(j).birthDate.getMillis) {
r.P1 = _p.get(i)
r.P2 = _p.get(j)
} else {
r.P1 = _p.get(j)
r.P2 = _p.get(i)
}
r.D = r.P2.birthDate.getMillis r.P1.birthDate.getMillis
tr.add(r)
}
if (tr.size < 1) {
return new F()
}
var answer: F = tr.get(0)
for (result: F < tr.asScala) ft match {
case FT.One => if (result.D < answer.D) {
answer = result
}
case FT.Two => if (result.D > answer.D) {
answer = result
}
}
answer
}
}

Refactor semántico

Primeramente pasamos a una Iteración de Chapa y Pintura™. Nos centramos en los nombres de clases y variables, y mover algunos métodos para fomentar la cohesión, y quitar responsabilidades al método principal. Introduciremos así conceptos de dominio que hasta ahora no se veían reflejados en el código.

De esta forma conseguimos sentirnos más cómodos a la hora de trabajar con el código ya que vamos conociendo lo que hace. Esto es algo perfectamente extrapolable a nuestro día a día: Primero un poco de semántica para tantear el terreno e ir cogiendo contexto, y luego añadir la funcionalidad/bugfix que nos ha motivado a abrir esa clase 👌 (refactoring oportunista, o regla del Boy Scout).

Aquí tenéis la rama de esta primera iteración por si queréis hacer una PR con vuestra solución partiendo de ella 🙂

// Post: http://codely.tv/screencasts/finder-kata-scala/
// Repo: https://github.com/CodelyTV/incomprehensible-finder-refactoring-kata-scala
package tv.codely.finderKata.algorithm
import java.util
import java.util.ArrayList
import scala.collection.JavaConverters._
import tv.codely.finderKata.algorithm.PeoplePairCriterion.PeoplePairCriterion
class BestPeoplePairFinder(private val allPeople: util.List[Person]) {
def Find(pairCriterion: PeoplePairCriterion): PeoplePair = {
val allPeoplePairs = new ArrayList[PeoplePair]()
for (currentPersonIteration < 0 until allPeople.size 1;
personToPairIteration < currentPersonIteration + 1 until allPeople.size) {
val peoplePair: PeoplePair = new PeoplePair()
if (allPeople.get(currentPersonIteration).birthDate.getMillis
< allPeople.get(personToPairIteration).birthDate.getMillis) {
peoplePair.person1 = allPeople.get(currentPersonIteration)
peoplePair.person2 = allPeople.get(personToPairIteration)
} else {
peoplePair.person1 = allPeople.get(personToPairIteration)
peoplePair.person2 = allPeople.get(currentPersonIteration)
}
peoplePair.birthDatesDistanceInSeconds =
peoplePair.person2.birthDate.getMillis peoplePair.person1.birthDate.getMillis
allPeoplePairs.add(peoplePair)
}
if (allPeoplePairs.size < 1) {
return new PeoplePair()
}
var bestPeoplePair: PeoplePair = allPeoplePairs.get(0)
for (peoplePair: PeoplePair < allPeoplePairs.asScala) pairCriterion match {
case PeoplePairCriterion.ClosestBirthDate =>
if (peoplePair.birthDatesDistanceInSeconds < bestPeoplePair.birthDatesDistanceInSeconds) {
bestPeoplePair = peoplePair
}
case PeoplePairCriterion.FurthestBirthDate =>
if (peoplePair.birthDatesDistanceInSeconds > bestPeoplePair.birthDatesDistanceInSeconds) {
bestPeoplePair = peoplePair
}
}
bestPeoplePair
}
}

Scala idiomático

Finalmente lo que hemos hecho ha sido aplicar elementos más idiomáticos de Scala:

  • Reemplazamos las colecciones de Java por las de Scala (¡Ya no hay ni un import! :P). Con esto ganamos métodos que nos vendrán de lujo, como por ejemplo SeqLike#combinations.
  • Type Class Ordering: Haciendo uso de ella nos podemos llevar la gestión de criterios para definir la mejor pareja de personas a objetos aislados y promover el Open/Closed Principle de SOLID.
  • Option[PeoplePair] como tipo de retorno: De esta forma hacemos explícita la posibilidad de no encontrar una pareja de personas bajo el criterio de búsqueda recibido (None). Esto es una de las principales ganancias con respecto la iteración anterior. Las alternativas a esto serían: devolver null, lanzar excepción, o como se venía haciendo, devolver una instancia de PeoplePair inconsistente (con los atributos de clase sin inicializar).
  • Eliminamos la mutabilidad gracias a las colecciones de Scala (inmutables por defecto), y los métodos Iterator#map y GenTraversableOnce#reduce.
// Post: http://codely.tv/screencasts/finder-kata-scala/
// Repo: https://github.com/CodelyTV/incomprehensible-finder-refactoring-kata-scala
package tv.codely.finderKata.algorithm
final class BestPeoplePairFinder() {
def find(people: Seq[Person], peoplePairCriterion: Ordering[PeoplePair]): Option[PeoplePair] = {
val canFindPeoplePairs = people.size >= 2
if (!canFindPeoplePairs) {
None
} else {
val peoplePairs = people.combinations(PeoplePair.numberOfPeopleInAPair).map { peopleCombination =>
val sortedPeopleCombination = peopleCombination.sorted
PeoplePair(sortedPeopleCombination.head, sortedPeopleCombination(1))
}
val bestPeoplePair = peoplePairs.reduce { (bestPeoplePair, candidatePeoplePair) =>
val isBetterCandidate = peoplePairCriterion.compare(bestPeoplePair, candidatePeoplePair) < 0
if (isBetterCandidate) candidatePeoplePair else bestPeoplePair
}
Some(bestPeoplePair)
}
}
}

Si te ha gustado esta kata y ves margen de mejora, agradeceríamos infinito que lo compartieras a través de un comentario en el vídeoTwitter, o incluso si te animas, una PR al repo 🚀🦄

Próximamente seguiremos metiéndole caña al tema de la programación funcional. En breves publicaremos la entrevista a Juan Manuel Serrano donde hablamos de esto, higher kinded types, y mucho más 😬.

[mc4wp_form]

SHOWHIDE Comments (0)

Leave a Reply

Your email address will not be published.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Sube de nivel con CodelyTV Pro 🚀

SOLID, Domain-Driven Design, Arquitectura Hexagonal, Docker, CQRS, Microservicios, Kubernetes, Testing…