recorrer - scala introduccion
qué patrón(s) de diseño de GOF tiene una implementación completamente diferente(java vs Scala) (3)
Bien, echemos un breve vistazo a estos patrones. Estoy mirando todos estos patrones puramente desde un punto de vista de programación funcional, y dejando de lado muchas cosas que Scala puede mejorar desde el punto de vista OO. La respuesta de Rex Kerr proporciona un contrapunto interesante a mis propias respuestas (solo leí su respuesta después de escribir la mía).
Con esto en mente, me gustaría decir que es importante estudiar estructuras de datos persistentes (estructuras de datos funcionalmente puras) y mónadas. Si quieres profundizar, creo que los principios básicos de la teoría de categorías son importantes: la teoría de categorías puede describir formalmente todas las estructuras del programa, incluidas las imperativas.
Patrones Creacionales
Un constructor no es más que una función. Un constructor sin parámetros para el tipo T no es más que una función () => T
, por ejemplo. De hecho, el azúcar sintáctico de Scala para las funciones se aprovecha en las clases de casos:
case class T(x: Int)
Eso es equivalente a:
class T(val x: Int) { /* bunch of methods */ }
object T {
def apply(x: Int) = new T(x)
/* other stuff */
}
Para que puedas instanciar T
con T(n)
lugar de new T(n)
. Incluso podrías escribirlo así:
object T extends Int => T {
def apply(x: Int) = new T(x)
/* other stuff */
}
Que convierte a T
en una función formal, sin cambiar ningún código.
Este es el punto importante a tener en cuenta al pensar en patrones creacionales. Así que echémosle un vistazo:
Fábrica abstracta
Es poco probable que esto cambie mucho. Se puede pensar que una clase es un grupo de funciones estrechamente relacionadas, por lo que un grupo de funciones estrechamente relacionadas se implementa fácilmente a través de una clase, que es lo que este patrón hace para los constructores.
Constructor
Los patrones del generador pueden reemplazarse por funciones al curry o aplicaciones de funciones parciales.
def makeCar: Size => Engine => Luxuries => Car = ???
def makeLargeCars = makeCar(Size.Large) _
def makeCar: (Size, Engine, Luxuries) => Car = ???
def makeLargeCars = makeCar(Size.Large, _: Engine, _: Luxuries)
Método de fábrica
Se vuelve obsoleto si descarta subclases.
Prototipo
No cambia; de hecho, esta es una forma común de crear datos en estructuras de datos funcionales. Vea el método de copy
clases de casos, o todos los métodos no mutables en las colecciones que devuelven colecciones.
Semifallo
Los singletons no son particularmente útiles cuando sus datos son inmutables, pero el object
Scala implementa este patrón de una manera segura.
Patrones estructurales
Esto se relaciona principalmente con las estructuras de datos, y el punto importante de la programación funcional es que las estructuras de datos generalmente son inmutables. Sería mejor que buscara estructuras de datos persistentes, mónadas y conceptos relacionados que tratar de traducir estos patrones.
No es que algunos patrones aquí no sean relevantes. Solo digo que, como regla general, debe analizar las cosas anteriores en lugar de tratar de traducir los patrones estructurales en equivalentes funcionales.
Adaptador
Este patrón está relacionado con las clases (tipado nominal), por lo que sigue siendo importante siempre y cuando tenga eso, y es irrelevante cuando no lo haga.
Puente
Relacionado con la arquitectura de OO, entonces lo mismo que arriba.
Compuesto
Lote en lentes y cremalleras.
Decorador
Un decorador es solo composición de funciones. Si está decorando una clase completa, es posible que no se aplique. Pero si proporciona su funcionalidad como funciones, entonces la composición de una función manteniendo su tipo es un decorador.
Fachada
Mismo comentario que para Bridge.
Peso mosca
Si piensa en constructores como funciones, piense en flyweight como memoria de funciones. Además, Flyweight tiene una relación intrínseca con la forma en que se construyen las estructuras de datos persistentes, y se beneficia mucho de la inmutabilidad.
Apoderado
Mismo comentario que para Adapter.
Patrones de comportamiento
Esto está por todos lados. Algunos de ellos son completamente inútiles, mientras que otros son tan relevantes como siempre en un entorno funcional.
Cadena de responsabilidad
Como Decorador, esta es la composición de la función.
Mando
Esta es una función. La parte de deshacer no es necesaria si sus datos son inmutables. De lo contrario, solo mantenga un par de funciones y su reverso. Ver también Lentes.
Interprete
Esto es una mónada
Iterador
Se puede volver obsoleto simplemente pasando una función a la colección. Eso es lo que Traversable
hace con foreach
, de hecho. Además, vea Iteratee.
Mediador
Aun relevante.
Recuerdo
Inútil con objetos inmutables. Además, su objetivo es mantener la encapsulación, que no es una preocupación importante en FP.
Tenga en cuenta que este patrón no es una serialización, que sigue siendo relevante.
Observador
Pertinente, pero vea Programación reactiva funcional.
Estado
Esto es una mónada
Estrategia
Una estrategia es una función.
Método de plantilla
Este es un patrón de diseño OO, por lo que es relevante para diseños OO.
Visitante
Un visitante es solo un método que recibe una función. De hecho, eso es lo que hace el foreach
Traversable
.
En Scala, también se puede reemplazar con extractores.
Recientemente leí la siguiente pregunta ASÍ:
¿Hay algún caso de uso para emplear el patrón de visitante en Scala? ¿Debería usar Pattern Matching en Scala cada vez que usaría Visitor Pattern en Java?
El enlace a la pregunta con el título: patrón de visitante en Scala . La respuesta aceptada comienza con
Sí, probablemente debería comenzar con la coincidencia de patrones en lugar del patrón de visitante. Vea esto http://www.artima.com/scalazine/articles/pattern_matching.html
Mi pregunta (inspirada en la pregunta antes mencionada) es ¿qué patrón (s) de diseño de GOF tiene una implementación completamente diferente en Scala? ¿Dónde debo tener cuidado y no seguir el modelo de programación basado en Java de Design Patterns (Gang of Four), si estoy programando en Scala?
Patrones creacionales
- Fábrica abstracta
- Constructor
- Método de fábrica
- Prototipo
- Singleton: crea directamente un objeto (scala)
Patrones estructurales
- Adaptador
- Puente
- Compuesto
- Decorador
- Fachada
- Peso mosca
- Apoderado
Patrones de comportamiento
- Cadena de responsabilidad
- Mando
- Interprete
- Iterador
- Mediador
- Recuerdo
- Observador
- Estado
- Estrategia
- Método de plantilla
- Visitante: Patten Matching (Scala)
Para casi todos estos, hay alternativas de Scala que cubren algunos, pero no todos, los casos de uso de estos patrones. Todo esto es IMO, por supuesto, pero:
Patrones Creacionales
Constructor
Scala puede hacer esto de forma más elegante con tipos genéricos que Java, pero la idea general es la misma. En Scala, el patrón se implementa más simplemente de la siguiente manera:
trait Status
trait Done extends Status
trait Need extends Status
case class Built(a: Int, b: String) {}
class Builder[A <: Status, B <: Status] private () {
private var built = Built(0,"")
def setA(a0: Int) = { built = built.copy(a = a0); this.asInstanceOf[Builder[Done,B]] }
def setB(b0: String) = { built = built.copy(b = b0); this.asInstanceOf[Builder[A,Done]] }
def result(implicit ev: Builder[A,B] <:< Builder[Done,Done]) = built
}
object Builder {
def apply() = new Builder[Need, Need]
}
(Si prueba esto en REPL, asegúrese de que la clase y el objeto Builder estén definidos en el mismo bloque, es decir, use :paste
). La combinación de tipos de comprobación con <:<
, argumentos de tipo genérico y el método de copia de caso las clases hacen una combinación muy poderosa.
Método de fábrica (y método abstracto de fábrica)
El uso principal de los métodos de fábrica es mantener sus tipos en línea recta; de lo contrario, también puedes usar constructores. Con el poderoso sistema de tipos de Scala, no necesita ayuda para mantener sus tipos derechos, por lo que también puede utilizar el constructor o un método de apply
en el objeto complementario de su clase y crear cosas de esa manera. En el caso del objeto acompañante en particular, no es más difícil mantener esa interfaz constante que mantener la interfaz en el objeto de fábrica consistente. Por lo tanto, la mayor parte de la motivación para los objetos de fábrica se ha ido.
De forma similar, muchos casos de métodos abstractos de fábrica pueden reemplazarse por tener un objeto complementario heredado de un rasgo apropiado.
Prototipo
Por supuesto, los métodos anulados y similares tienen su lugar en Scala. Sin embargo, los ejemplos utilizados para el patrón Prototype en el sitio web Design Patterns son bastante desaconsejables en Scala (o Java IMO). Sin embargo, si desea tener una superclase, seleccione acciones basadas en sus subclases en lugar de dejarlas que decidan por sí mismas, debería usar las pruebas de match
lugar de las cluncadas.
Semifallo
Scala los abarca con object
. Son solteros: ¡utiliza y disfruta!
Patrones estructurales
Adaptador
El trait
de Scala proporciona mucho más poder aquí: en lugar de crear una clase que implemente una interfaz, por ejemplo, puede crear un rasgo que implemente solo una parte de la interfaz, dejando el resto para que usted defina. Por ejemplo, java.awt.event.MouseMotionListener
requiere que complete dos métodos:
def mouseDragged(me: java.awt.event.MouseEvent)
def mouseMoved(me: java.awt.event.MouseEvent)
Quizás quieras ignorar el arrastre. Luego escribes un trait
:
trait MouseMoveListener extends java.awt.event.MouseMotionListener {
def mouseDragged(me: java.awt.event.MouseEvent) {}
}
Ahora puedes implementar solo mouseMoved
cuando heredes de esto. Entonces: patrón similar, pero mucha más potencia con Scala.
Puente
Puedes escribir puentes en Scala. Es una gran cantidad de repetitivo, aunque no tan malo como en Java. No recomendaría rutinariamente usar esto como un método de abstracción; piense en sus interfaces cuidadosamente primero. Tenga en cuenta que con el aumento de poder de los rasgos que a menudo puede utilizar para simplificar una interfaz más elaborada en un lugar donde de lo contrario podría sentirse tentado a escribir un puente.
En algunos casos, es posible que desee escribir un transformador de interfaz en lugar del patrón de puente de Java. Por ejemplo, tal vez desee tratar movimientos y movimientos del mouse utilizando la misma interfaz con solo un indicador booleano que los distinga. Entonces tú puedes
trait MouseMotioner extends java.awt.event.MouseMotionListener {
def mouseMotion(me: java.awt.event.MouseEvent, drag: Boolean): Unit
def mouseMoved(me: java.awt.event.MouseEvent) { mouseMotion(me, false) }
def mouseDragged(me: java.awt.event.MouseEvent) { mouseMotion(me, true) }
}
Esto le permite omitir la mayor parte de la plantilla repetitiva del patrón de puente mientras logra un alto grado de independencia de implementación y sigue permitiendo que sus clases obedezcan a la interfaz original (para que no tenga que seguir envolviéndolas y desenvuélvalas).
Compuesto
El patrón compuesto es particularmente fácil de lograr con clases de casos, aunque hacer actualizaciones es bastante arduo. Es igualmente valioso en Scala y Java.
Decorador
Los decoradores son incómodos. Por lo general, no desea utilizar los mismos métodos en una clase diferente en el caso de que la herencia no sea exactamente lo que desea; lo que realmente quieres es un método diferente en la misma clase que haga lo que quieras en lugar de lo predeterminado. El patrón de enriquecer mi biblioteca a menudo es un sustituto superior.
Fachada
Fachada funciona mejor en Scala que en Java porque puede hacer que los rasgos tengan implementaciones parciales, de modo que no tenga que hacer todo el trabajo usted mismo cuando los combine.
Peso mosca
Aunque la idea del peso mosca es tan válida en Scala como Java, tiene un par de herramientas más para implementar: lazy val
, donde una variable no se crea a menos que realmente se necesite (y luego se reutilice) y by-name parameters
, donde solo hace el trabajo requerido para crear un argumento de función si la función realmente usa ese valor. Dicho esto, en algunos casos, el patrón de Java se mantiene sin cambios.
Apoderado
Funciona de la misma manera en Scala que en Java.
Patrones de comportamiento
Cadena de responsabilidad
En aquellos casos en que puede enumerar a las partes responsables en orden, puede
xs.find(_.handleMessage(m))
asumiendo que todos tienen un método handleMessage
que devuelve true
si el mensaje fue manejado. Si quiere mutar el mensaje tal como está, use un fold en su lugar.
Dado que es fácil colocar a las partes responsables en un Buffer
de algún tipo, el complejo marco utilizado en las soluciones Java rara vez tiene cabida en Scala.
Mando
Este patrón es reemplazado casi por completo por las funciones. Por ejemplo, en lugar de todos
public interface ChangeListener extends EventListener {
void stateChanged(ChangeEvent e)
}
...
void addChangeListener(ChangeListener listener) { ... }
tu simplemente
def onChange(f: ChangeEvent => Unit)
Interprete
Scala proporciona combinadores de analizadores que son mucho más potentes que el simple intérprete sugerido como Patrón de diseño.
Iterador
Scala tiene Iterator
integrado en su biblioteca estándar. Es casi trivial hacer que su propia clase extienda Iterator
o Iterable
; este último suele ser mejor ya que hace que la reutilización sea trivial. Definitivamente una buena idea, pero tan directa que difícilmente lo llamaría un patrón.
Mediador
Esto funciona bien en Scala, pero generalmente es útil para datos variables, e incluso los mediadores pueden caer en conflicto con las condiciones de carrera y, si no se usan con cuidado. En cambio, intente cuando sea posible tener todos los datos relacionados almacenados en una colección inmutable, clase de caso o lo que sea, y cuando realice una actualización que requiera cambios coordinados, cambie todas las cosas al mismo tiempo. Esto no le ayudará a interactuar con javax.swing
, pero de otro modo es ampliamente aplicable:
case class Entry(s: String, d: Double, notes: Option[String]) {}
def parse(s0: String, old: Entry) = {
try { old.copy(s = s0, d = s0.toDouble) }
catch { case e: Exception => old }
}
Guarde el patrón del mediador para cuando necesite manejar múltiples relaciones diferentes (un mediador para cada uno), o cuando tenga datos mutables.
Recuerdo
lazy val
es casi ideal para muchas de las aplicaciones más simples del patrón de recuerdo, por ejemplo
class OneRandom {
lazy val value = scala.util.Random.nextInt
}
val r = new OneRandom
r.value // Evaluated here
r.value // Same value returned again
Es posible que desee crear una clase pequeña específicamente para la evaluación perezosa:
class Lazily[A](a: => A) {
lazy val value = a
}
val r = Lazily(scala.util.Random.nextInt)
// not actually called until/unless we ask for r.value
Observador
Este es un patrón frágil en el mejor de los casos. Favorecer, siempre que sea posible, mantener un estado inmutable (ver Mediador) o usar actores donde un actor envía mensajes a todos los demás sobre el cambio de estado, pero donde cada actor puede hacer frente a una desactualización.
Estado
Esto es igualmente útil en Scala, y en realidad es la forma preferida de crear enumeraciones cuando se aplica a rasgos sin método:
sealed trait DayOfWeek
final trait Sunday extends DayOfWeek
...
final trait Saturday extends DayOfWeek
(a menudo desearía que los días de la semana hicieran algo para justificar esta cantidad de texto repetitivo).
Estrategia
Esto se reemplaza casi por completo al tener los métodos funciones que implementan una estrategia y proporcionan funciones para elegir.
def printElapsedTime(t: Long, rounding: Double => Long = math.round) {
println(rounding(t*0.001))
}
printElapsedTime(1700, math.floor) // Change strategy
Método de plantilla
Los rasgos ofrecen muchas más posibilidades aquí que es mejor simplemente considerarlos como otro patrón. Puede completar la mayor cantidad de código posible con la cantidad de información que tenga en su nivel de abstracción. Realmente no me gustaría llamarlo lo mismo.
Visitante
Entre el tipado estructural y la conversión implícita , Scala tiene una capacidad asombrosamente mayor que el patrón de visitante típico de Java. No tiene sentido usar el patrón original; te distraerás de la manera correcta para hacerlo. Muchos de los ejemplos realmente desean que haya una función definida en la cosa que se visita, que Scala puede hacer por usted de manera trivial (es decir, convertir un método arbitrario en una función).
Supongo que el patrón Command
no es necesario en ningún idioma funcional. En lugar de la función de comando de encapsulado dentro del objeto y luego seleccionar el objeto apropiado, simplemente use la función apropiada.
Flyweight
es solo caché, y tiene implementación predeterminada en la mayoría de los lenguajes funcionales ( memoize in clojure)
Incluso el Template method
, la Strategy
y el State
Template method
se pueden implementar simplemente aprobando la función apropiada en el método.
Por lo tanto, le recomiendo que no profundice en los patrones de diseño cuando se prueba a sí mismo en un estilo funcional, sino que lee algunos libros sobre conceptos funcionales (funciones de alto orden, pereza, currying, etc.)