groovy - ¿Cuáles son algunas de las ventajas de la tipificación de patos frente a la tipificación estática?
duck-typing (12)
@Chris Bunch
No es que el tipado estático es más productivo que el tipado de pato tanto como simplemente es diferente. Con el tipado de pato siempre debe preocuparse de que sus datos tengan el método correcto y en Javascript o Ruby se muestren a través de muchas pruebas de métodos. Con el tipado estático no importa, siempre y cuando sea la interfaz correcta, por lo que realmente elimina la molestia de las pruebas y las conversiones entre los tipos.
Lo siento pero tuve que hacerlo ...
Estoy investigando y experimentando más con Groovy y estoy tratando de hacerme a la idea de los pros y los contras de implementar cosas en Groovy que no puedo / debo hacer en Java. La programación dinámica sigue siendo solo un concepto para mí, ya que he estado profundamente inmerso en lenguajes estáticos y fuertemente tipados.
Groovy me da la habilidad de duck-type , pero realmente no puedo ver el valor. ¿Cómo es más productivo el tipado de patos que el tipado estático? ¿Qué tipo de cosas puedo hacer en mi práctica de código para ayudarme a comprender los beneficios?
Hago esta pregunta con Groovy en mente, pero entiendo que no es necesariamente una pregunta Groovy así que doy la bienvenida a las respuestas de cada campo de código.
A continuación, ¿qué es mejor: EMACS o vi? Esta es una de las guerras religiosas corrientes.
Piénselo de esta manera: cualquier programa que sea correcto será correcto si el lenguaje está tipado estáticamente. Lo que hace el tipado estático es dejar que el compilador tenga suficiente información para detectar desajustes de tipos en tiempo de compilación en lugar de tiempo de ejecución. Esto puede ser una molestia si haces un tipo incremental de programación, aunque (lo mantengo) si estás pensando claramente sobre tu programa, no importa mucho; por otro lado, si está construyendo un programa realmente grande, como un sistema operativo o un conmutador de teléfono, con docenas o cientos o miles de personas trabajando en él, o con requisitos de confiabilidad realmente altos, entonces tener el compilador puede detectar una gran clase de problemas para usted sin necesidad de un caso de prueba para ejercer solo la ruta correcta del código.
No es que la tipificación dinámica sea algo nuevo y diferente: C, por ejemplo, se tipea de manera dinámica, ya que siempre puedo convertir un foo*
en una bar*
. Simplemente significa que es mi responsabilidad como programador en C nunca utilizar el código que es apropiado en una bar*
cuando la dirección realmente apunta a un foo*
. Pero como resultado de los problemas con los programas grandes, C desarrolló herramientas como lint (1), fortaleció su sistema de tipos con typedef
y finalmente desarrolló una variante fuertemente tipada en C ++. (Y, por supuesto, C ++ a su vez desarrolló formas en torno a la tipificación fuerte, con todas las variedades de moldes y genéricos / plantillas y con RTTI.
Sin embargo, otra cosa --- no confunda la "programación ágil" con los "lenguajes dinámicos". La programación ágil se trata de la forma en que las personas trabajan juntas en un proyecto: ¿puede el proyecto adaptarse a los requisitos cambiantes para satisfacer las necesidades de los clientes mientras se mantiene un entorno humano para los programadores? Se puede hacer con lenguajes de tipado dinámico, y a menudo lo es, porque pueden ser más productivos (por ejemplo, Ruby, Smalltalk), pero se puede hacer, se ha realizado con éxito, en C e incluso en ensamblador. De hecho, Rally Development incluso usa métodos ágiles (SCRUM en particular) para hacer marketing y documentación.
Aquí hay un escenario donde la escritura de pato ahorra trabajo.
Aquí hay una clase muy trivial
class BookFinder {
def searchEngine
def findBookByTitle(String title) {
return searchEngine.find( [ "Title" : title ] )
}
}
Ahora para la prueba unitaria:
void bookFinderTest() {
// with Expando we can ''fake'' any object at runtime.
// alternatively you could write a MockSearchEngine class.
def mockSearchEngine = new Expando()
mockSearchEngine.find = {
return new Book("Heart of Darkness","Joseph Conrad")
}
def bf = new BookFinder()
bf.searchEngine = mockSearchEngine
def book = bf.findBookByTitle("Heart of Darkness")
assert(book.author == "Joseph Conrad"
}
Pudimos sustituir un Search Engine por un Expando, debido a la ausencia de comprobación de tipo estática. Con la comprobación del tipo estático, deberíamos asegurarnos de que SearchEngine fuera una interfaz, o al menos una clase abstracta, y crear una implementación simulada completa de la misma. Eso requiere mucha mano de obra, o puede usar un sofisticado marco de burla de un solo propósito. Pero el tipado de pato es de uso general y nos ha ayudado.
Debido a la tipificación de pato, nuestra prueba unitaria puede proporcionar cualquier objeto antiguo en lugar de la dependencia, siempre que implemente los métodos que se llaman.
Para enfatizar, puede hacer esto en un lenguaje estáticamente tipado, con un uso cuidadoso de interfaces y jerarquías de clase. Pero con el tipado de patos puedes hacerlo con menos pensamiento y menos teclas.
Esa es una ventaja de la tipificación de patos. No significa que el tipado dinámico sea el paradigma correcto para usar en todas las situaciones. En mis proyectos Groovy, me gusta volver a Java en circunstancias en las que siento que las advertencias del compilador sobre los tipos me van a ayudar.
Con, TDD + 100% Code Coverage + herramientas IDE para ejecutar constantemente mis pruebas, ya no siento la necesidad de escribir de forma estática. Sin tipos fuertes, mi prueba de unidad se ha vuelto tan fácil (simplemente use Maps para crear objetos simulados). Especialmente, cuando usas genéricos, puedes ver la diferencia:
//Static typing
Map<String,List<Class1<Class2>>> someMap = [:] as HashMap<String,List<Class1<Class2>>>
vs
//Dynamic typing
def someMap = [:]
El tipado de pato anula la comprobación estática más moderna de IDE, que puede señalar errores a medida que escribe. Algunos lo consideran una ventaja. Quiero que IDE / Compiler me diga que he hecho un estúpido truco de programador tan pronto como sea posible.
Mi argumento favorito más reciente contra el tipado de patos proviene de un DTO del proyecto Grails:
class SimpleResults {
def results
def total
def categories
}
donde los results
resultan ser algo así como Map<String, List<ComplexType>>
, que se puede descubrir solo siguiendo un rastro de llamadas a métodos en diferentes clases hasta que encuentre dónde se creó. Para el terminalmente curioso, total
es la suma de los tamaños de la List<ComplexType>
s y categories
es el tamaño del Map
Puede haber sido claro para el desarrollador original, pero el chico de mantenimiento pobre (ME) perdió mucho pelo siguiendo este.
En mi humilde opinión, la ventaja de la mecanografía de pato se magnifica cuando se cumple con algunas convenciones, como nombrar variables y métodos de manera consistente. Tomando el ejemplo de Ken G , creo que leería mejor:
class SimpleResults {
def mapOfListResults
def total
def categories
}
Digamos que define un contrato en una operación llamada ''calculateRating (A, B)'' donde A y B se adhieren a otro contrato. En pseudocódigo, leería:
Long calculateRating(A someObj, B, otherObj) {
//some fake algorithm here:
if(someObj.doStuff(''foo'') > otherObj.doStuff(''bar'')) return someObj.calcRating());
else return otherObj.calcRating();
}
Si desea implementar esto en Java, tanto A como B deben implementar algún tipo de interfaz que lea algo como esto:
public interface MyService {
public int doStuff(String input);
}
Además, si desea generalizar su contrato para calcular las calificaciones (digamos que tiene otro algoritmo para los cálculos de calificación), también debe crear una interfaz:
public long calculateRating(MyService A, MyServiceB);
Con el tipado de pato, puede deshacerse de sus interfaces y simplemente confiar en que en tiempo de ejecución, tanto A como B responderán correctamente a sus llamadas doStuff()
. No hay necesidad de una definición de contrato específica. Esto puede funcionar para usted, pero también puede funcionar en su contra.
El inconveniente es que debe tener mucho cuidado para garantizar que su código no se rompa cuando otras personas lo cambian (es decir, la otra persona debe conocer el contrato implícito en el nombre y los argumentos del método).
Tenga en cuenta que esto se agrava especialmente en Java, donde la sintaxis no es tan estricta como podría ser (en comparación con Scala, por ejemplo). Un contraejemplo de esto es el marco Lift , donde dicen que el recuento de SLOC del framework es similar a Rails , pero el código de prueba tiene menos líneas porque no necesitan implementar verificaciones de tipos dentro de las pruebas.
Es un poco difícil ver el valor del tipado de patos hasta que lo hayas usado por un tiempo. Una vez que te hayas acostumbrado, te darás cuenta de lo recargado que es no tener que lidiar con interfaces o tener que preocuparte por qué tipo de objeto es exactamente.
Mi opinión:
Los lenguajes tipados dinámicamente o patos son juguetes. No puede obtener Intellisense y pierde tiempo de compilación (o tiempo de edición, al usar un IDE REAL como VS, no es que la basura los demás piensen que son IDE) validación de código.
Manténgase alejado de cada idioma que no está tipado estáticamente, todo lo demás es simplemente masoquismo.
Muchos de los comentarios sobre el tipado de patos realmente no corroboran las afirmaciones. No "tener que preocuparse" por un tipo no es sostenible para el mantenimiento o para hacer una aplicación extensible. Realmente tuve una buena oportunidad para ver a Grails en acción en mi último contrato y es realmente divertido de ver. Todo el mundo está contento con las ganancias de poder "crear-aplicación" y ponerse en marcha, lamentablemente todo te alcanza en la parte de atrás.
Groovy me parece de la misma manera. Claro que puedes escribir un código muy sucinto y definitivamente hay algo de azúcar en cómo trabajamos con propiedades, colecciones, etc. Pero el costo de no saber qué diablos pasa de un lado a otro simplemente empeora cada vez más. En algún punto, se rasca la cabeza preguntándose por qué el proyecto se ha convertido en 80% de prueba y 20% de trabajo. La lección aquí es que "más pequeño" no significa código "más legible". Lo siento amigos, es una lógica simple: cuanto más se tiene que saber intuitivamente, más complejo es el proceso de comprensión de ese código. Es por eso que las GUI se han vuelto cada vez más icónicas a lo largo de los años. Seguro que se ve bien, pero WTH no siempre es obvio.
Las personas de ese proyecto parecían tener problemas para "capturar" las lecciones aprendidas, pero cuando tienes métodos que devuelven un solo elemento de tipo T, una matriz de T, un ErrorResult o un nulo ... se vuelve bastante evidente.
Sin embargo, una cosa que trabajar con Groovy ha sido para mí: ¡horas increíbles facturables!
No es que la mecanografía de pato sea más productiva que la mecanografía estática tanto como simplemente es diferente. Con el tipado estático, siempre debe preocuparse de que sus datos sean del tipo correcto, y en Java se muestra a través del envío al tipo correcto. Con el tipado de pato, el tipo no importa, siempre que tenga el método correcto, por lo que realmente elimina la molestia de la fundición y las conversiones entre los tipos.
No hay nada de malo con el tipado estático si está usando Haskell, que tiene un increíble sistema de tipo estático. Sin embargo, si está utilizando lenguajes como Java y C ++ que tienen sistemas de tipo terriblemente paralizantes, el tipado de patos es definitivamente una mejora.
Imagine intentar usar algo tan simple como " map " en Java (y no, no me refiero a la estructura de datos ). Incluso los genéricos son bastante poco compatibles.
Para mí, no son terriblemente diferentes si ves idiomas tipados dinámicamente como simplemente una forma de tipado estático donde todo hereda de una clase base suficientemente abstracta.
Los problemas surgen cuando, como muchos han señalado, te empiezas a poner extraño con esto. Alguien señaló una función que devuelve un único objeto, una colección o un nulo. Haga que la función devuelva un tipo específico, no múltiple. Usa múltiples funciones para una sola colección.
Lo que se reduce a esto es que cualquiera puede escribir un código incorrecto. El tipado estático es un gran dispositivo de seguridad, pero a veces el casco se interpone cuando desea sentir el viento en su cabello.