Refactoring en Java, Scala y Clojure

actualización: Asistir a mi presentación en este blog en el 25 de febrero de 2014 DevNexus

Motivación

Personalmente considero que la seguridad de tipos para ser útil en el trabajo a causa de las personas-años de desarrollo que fui en el sistema que yo trabajo en la mayoría. Pero yo no estoy tratando de convertir a la gente de seguridad de tipos, o lejos de ella. Mi objetivo es hacer que los problemas más visibles para que todos podamos escribir mejor código más fácilmente en el futuro. Cuando le di esta charla a la Asheville codificadores Liga, sentí un cierto grado de satisfacción que una persona me dijo después que lo iban a mirar en Clojure y otra que se verían en Scala.

Problema

Para esta comparación, voy a utilizar una clase que modela una combinación año / mes y una función «, AddMonths» que lleva el número de meses para agregar (positivo o negativo) y devuelve una nueva YearMonth. Originalmente se utilizó una estructura de datos con 2 campos (año y mes), pero complicamos las consultas de base de datos para rangos de meses, así que lo cambiamos a un único int del YYYYMM formato. Ahora podemos utilizar> y

A pesar de afirmaciones salvajes que programación orientada a objetos es todo acerca de la mutación, voy utilizar las clases inmutables en los tres idiomas (se utilizan unas pocas variables locales mutables, pero nunca expuestas fuera de la función que los declara).

ejemplos de código completa de este artículo están disponibles en Github, tanto antes (xxxx1 ) y después (xxxx2) refactorización

Aquí está la interfaz Java original, traducido a Scala y Clojure:.

original Interface

interfaz Java

   interfaz pública  YearMonthInterface {
public int getYear ();
public int getMonth ();}

Scala Trait

   rasgo  YearMonthTrait {
def años: Int
def mes: Int
}

Clojure

No es preciso un interfaz, pero el compilador no le dirá si usted no puede coincidir con los datos de sus funciones. Protocolos se podrían utilizar, pero que probablemente no es típico y que sin duda no es necesario para este ejemplo sencillo

Base YearMonth Implementación

unas sencillas pruebas deben proporcionar la mejor visión general:.

Pruebas

de Implementación Base

   / / Java  
YearMonth.addMonths (YearMonth.of (2013, 7), 2);
/ / 2013-9
YearMonth.addMonths (YearMonth.of (2012, 12), 1);
/ / 2013-1
YearMonth.addMonths (YearMonth.of (2013, 1), -1);
/ / 2012-12

/ / Scala
YearMonth.addMonths (YearMonth (2013, 7), 2)
/ / YearMonth (2013,9)
YearMonth.addMonths (YearMonth (2012, 12), 1)
/ / YearMonth (2013,1)
YearMonth.addMonths (YearMonth (2013, 1), -1)
/ / YearMonth (2012,12)

;; Clojure
: año 2013, : mes 7} 2)
;; {: año 2013,: mes 9}
( AddMonths {: año de 2012, : mes 12} 1)
;; {: año 2013,: mes 1}
(AddMonths {: año 2013, : mes 1} -1)
;; {: año 2012,: mes 12}

Java Class

   public final class   implementos  YearMonthInterface {
int años;
int final privado meses;

privada YearMonth ( int y, int m) {año = y; mes = m;}

static YearMonth de (int y, int m) {
si (m> 12) {
/ / convertir al mes de base cero para matemáticas
m -;
/ / Realizar alguna meses adicionales más al año
y = y + (m / 12);
/ / Ajustar mes para estar dentro de un año
m = m% 12;
/ / convertir de nuevo a un mes a base de
m + +;}
else if (m <1) {
/ / Realizar alguna meses adicionales más al año, pero el primer año
/ / en este caso es Todavía año-1

y = y + (m / 12) - 1;.
/ / Ajustar mes negativo para estar dentro de un año
/ / Para obtener el mes positivo, restar de 12

m = 12 + (m% 12);}

volver nueva YearMonth (a, m);}


@ Override
public int getYear () {return año;}

@ Override
public int getMonth () {return class="keyword"> meses;}

static YearMonth AddMonths (YearMonthInterface ym,
int addedMonths) {
de (ym.getYear (), ym.getMonth () + addedMonths);}


@ Override
<. span class = "palabra clave"> públicos
String toString () {
volver nueva StringBuilder () append (años) adjuntar. ("-") ..
append (meses) toString ();}

}

Scala Clase Caso y Compañero de objetos

caso clases en Scala automatiza escribir el código Java similar. Scala no tiene métodos estáticos. En su lugar, todo lo que Java sería llamar a un método de «estática» va en el objeto acompañante en Scala. Un objeto acompañante es una instancia singleton con el mismo nombre y en el mismo archivo que la clase a la que pertenece.

   caso clase  YearMonth ( anular val  años: Int, 
val mes: Int) extiende YearMonthTrait />

def AddMonths (ym: YearMonthTrait, addedMonths: int): YearMonth = {
val NewMonth = ym.month + addedMonths
si (NewMonth> 12) {
/ / convertir a base cero meses para las matemáticas
val m = NewMonth - 1
/ / Realizar cualquier mes adicional a la año
YearMonth (ym.year + (m / 12), (m% 12) + 1)
} = "palabra clave"> else if (NewMonth <1) {
/ / Realizar alguna meses adicionales más al año, pero la
/ / primero años en este caso es aún años-1

val y = ym.year + (NewMonth / 12) - 1
/ / Ajustar mes negativo para estar dentro de un año.
/ / Para obtener el mes positivo, restar de 12

val m = 12 + (NewMonth% 12)
nueva YearMonth (a, m)
demás

nueva YearMonth (ym.year, NewMonth)

}}}

Función Clojure

  (AddMonths DEFN [ym, addedMonths] 
(let [NewMonth (+ (class="symbol"> YM) addedMonths)]
(cond (> NewMonth 12)
;; convertir al mes de base cero para las matemáticas
(vamos [m (- NewMonth 1)]
;; Carry ningún meses adicionales al ejercicio
(asoc ym : año (+ (: año ym) (quot m 12)),
: mes (+ (rem m 12) 1)))
( ;; Llevar a ningún meses adicionales más al año, pero la
;, primer año en este caso sigue siendo año-1

(let [y (diciembre (+ (: año ym) (quot NewMonth 12))),
;; Ajustar mes negativo para ser dentro de un año
;.; Para obtener el mes positivo, restar de 12

m (+ 12 (rem NewMonth 12))]
(asoc ym : año : mes m))
: else (asoc ym : mes NewMonth))))

Añadir una clase que implementa

Prueba de Ejecución Class

   / / Java  
YearMonth.addMonths (MonthlyA.of ( "Uno" , 2013, 7), 2)
/ / 2013-9

/ / Scala
YearMonth.addMonths ( MonthlyA ( "One" , 2013, 7), 2)
/ / YearMonth (2013,9)

;; Clojure
: otherField1 "One" , : año 2013, : mes 7} 2)
;; {: otherField1 "One",: año 2013,: mes 9}

Java

   public class   implementos  YearMonthInterface {
final privado Cadena otherField1;
int final privado años;
int final privado meses;

privada MonthlyA (String s, int y, int m) {
otherField1 = s; año = y; mes = m;
}

static MonthlyA de (String s, int y, int m) {
return new MonthlyA (s, y, m);}


público Cadena getOtherField1 () {return class="keyword"> otherField1 ;}

@ Override
public int getYear () { retorno año;}

@ Override
getMonth () {return class="keyword"> meses;}}

Scala

   Clase caso  MonthlyA (otherField1: String, 
val años: Int,
anular val mes: Int) extiende YearMonthTrait

Clojure

Change Data Representation De Año y mes de YYYYMM

prueba

   / / Java  
YearMonth.addMonths (YearMonth.of (201.307), 2)
/ / 2013-9

/ / Scala
YearMonth.addMonths (YearMonth (201.307), 2)
/ / YearMonth (2013,9)

;; Clojure
(AddMonths { : YYYYMM 201307} 2)
;; {: YYYYMM 201309}

Java

Java requiere que actualice manualmente todo el código viejo para ser compatible con el nuevo formato de datos. Mientras estoy en ello, voy a añadir un método de fábrica estática conveniencia para la implementación base que toma el nuevo formato de datos.

   interfaz pública   /> 
... viejos métodos sin cambios ... />
/ / Nuevo! @ YYYYMM retorno o YearMonth.of (año, mes) getYyyyMm ()

public int getYyyyMm ();.
}

implementos

... viejos métodos sin cambios ...

/ / Nuevo!

YearMonth de ( int YYYYMM) {
volver nueva YearMonth (YYYYMM / 100, YYYYMM% 100);}


/ / Nuevo!
@ Override
public int getYyyyMm () {
(año * 100) + meses;
}
}

implementos />
... viejos métodos sin cambios ...

/ / Nuevo!

@ Override
public int getYyyyMm () {
YearMonth.of (año, mes) getYyyyMm ();.
}
}

Scala

constructores adicionales en Scala se implementan como métodos de fábrica en el objeto compañero. apply () es el nombre predeterminado de un método, por lo que no es necesario especificarlo en su código de cliente. Se puede utilizar igual que una fábrica / constructor normal (excepto para el patrón constructor coincidente) como se muestra en el ejemplo «Scala Test» a continuación.

   rasgo   /> 
... ! viejos métodos sin cambios ...

/ / Nuevo

def YYYYMM: Int = (año * 100) + meses
}

YearMonth {
/ / Añade método de fábrica YYYYMM al compañero YearMonth objeto
/ / Esto es como el extra "del" método que acaba de agregar a la versión de Java

class="keyword"> def solicite (YYYYMM:. Int. ) = nueva YearMonth ((YYYYMM / 100), (100 YYYYMM%))

... viejos métodos sin cambios ...

}

Clojure

  (defn ymToOld [ym] (dissoc (asoc ym :. años  (quot (: YYYYMM ym) 100) 
: mes (rem (: YYYYMM ym) 100))
: YYYYMM ))

(defn ymToNew [ym] (dissoc (asoc ym : YYYYMM (+ (* (: año ym) 100) />
: mes ym)))
: año : mes ))

(AddMonths DEFN [ym, addedMonths]
(si (contiene ym :? YYYYMM )
(ymToNew (AddMonths (ymToOld ym), addedMonths))
(let [NewMonth (+ (: mes YM) addedMonths)]

... mismo código de antes ...

agregar una nueva clase de aplicación con el formato de datos nuevo

Prueba New Class

   / / Java  
YearMonth.addMonths (MonthlyB.of (1.1, 201 307), 2);
/ / 2013-9

/ / Scala
YearMonth.addMonths (MonthlyB (1.1, 201 307), 2)
/ / YearMonth (2013,9)

;; Clojure
(AddMonths {: otherField2 : YYYYMM 201307} 2)
;; {: otherField2 1.1: YYYYMM 201309}

Java

   public class   implementos  YearMonthInterface {
última doble otherField2;
int final privado YYYYMM;

privada MonthlyB ( doble d, int YYM) {
otherField2 = d; YYYYMM = YYM;
}

static MonthlyB de ( doble d, int YYM) {
volver nueva MonthlyB (d, YYM);}


public double getOtherField2 () {return class="keyword"> otherField2;}

@ Override
public int getYear () {return class="keyword"> YYYYMM / 100;}

@ Override
public int getMonth () {return class="keyword"> (YYYYMM% 100);}

@ Override
public int getYyyyMm () {return class="keyword"> YYYYMM;}}

Scala

   rasgo   extiende  YearMonthTrait {
def YYYYMM: Int
def años: Int = YYYYMM / 100
def mes: Int = (YYYYMM% 100)
}

MonthlyB (otherField1: Doble,
val YYYYMM: Int) se extiende YearMonthNew

Clojure

Consideraciones adicionales

  • Compilación Scala es lento, teniendo 2-3 veces, siempre y Java. Sbt, por otro lado, es muy inteligente, lo que maximiza el uso del procesador y la decisión de no compilar todo a menos que necesite, por lo que la compilación de Scala con SBT puede ser efectivamente más rápido que la compilación de Java con la hormiga.
  • Compilado código Clojure ejecuta aproximadamente un 50% más lento que el código Scala / Java comparable
  • Tanto Scala y Clojure requieren unos jar pequeños . compile que mantienen sus APIs específicas

Conclusiones

En Java, más trabajo se gasta definición y actualización de los tipos que definir funciones que trabajan en ellos. En Scala, el trabajo por adelantado de la definición de tipos es pequeño y elegante;.. una menor distracción del «trabajo real» de transformar esos datos en Clojure, se pone toda la atención en las funciones de las estructuras de datos, mientras que casi desaparecen por completo Esta es una manera muy bonita y rápida de código que se obtiene un sistema muy sencillo, pero carece de la garantía de seguridad que tipo de seguridad proporciona los otros idiomas. cobertura de prueba unitaria exhaustiva puede mitigar este riesgo, pero eso es otra forma de complejidad con su propio coste de mantenimiento.

En el principio, Java resuelve prácticamente todos los asuntos importantes con C + + y creó la JVM que estos otros dos idiomas se basan en. Pero está mostrando su edad. Realmente tengo problemas para encontrar una situación en la que Java iba a ganar. Supongo que si un pequeño archivo jar El tamaño fue crítico … Realmente, la mayor ventaja de Java tiene más de Scala es más rápido los tiempos de compilación. Tal vez si usted escribe en Clojure, podría utilizar pequeñas cantidades de Java para el desempeño en áreas críticas? Aún así, prefiero usar Scala para que de Java.

Tanto Scala y Clojure parecen eliminar una gran cantidad de trabajo que se requiere en Java, pero que ahora cobran enfoques fundamentalmente diferentes para hacerlo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *