representacion por invariante diseño contrato java ruby oop interface design-by-contract

java - invariante - diseño por contrato python



Rubí y pato escribiendo: ¿diseño por contrato imposible? (8)

Firma del método en Java:

public List<String> getFilesIn(List<File> directories)

similar en ruby

def get_files_in(directories)

En el caso de Java, el sistema de tipos me da información sobre lo que el método espera y ofrece. En el caso de Ruby, no tengo ni idea de qué se supone que debo pasar, o qué esperaré recibir.

En Java, el objeto debe implementar formalmente la interfaz. En Ruby, el objeto que se transfiere debe responder a los métodos que se invocan en el método definido aquí.

Esto parece muy problemático:

  1. Incluso con una documentación 100% precisa y actualizada, el código de Ruby tiene que exponer esencialmente su implementación, rompiendo la encapsulación. "OO pureza" a un lado, esto parecería ser una pesadilla de mantenimiento.
  2. El código de Ruby no me da ninguna pista de lo que se devuelve; Tendría que experimentar esencialmente, o leer el código para descubrir a qué métodos respondería el objeto devuelto.

No buscamos debatir tipeo estático versus tipado de pato, sino buscar entender cómo se mantiene un sistema de producción en el que casi no se tiene la capacidad de diseñar por contrato.

Actualizar

Nadie realmente ha abordado la exposición de la implementación interna de un método a través de la documentación que requiere este enfoque. Como no hay interfaces, si no estoy esperando un tipo particular, ¿no tengo que detallar todos los métodos que pueda llamar para que la persona que llama sepa qué se puede pasar? ¿O es solo un caso extremo que realmente no surge?


Aunque me encanta escribir de forma estática cuando estoy escribiendo código Java, no hay ninguna razón por la que no puedas insistir en precondiciones reflexivas en el código de Ruby (o cualquier tipo de código para el caso). Cuando realmente necesito insistir sobre las condiciones previas para los parámetros del método (en Ruby), me complace escribir una condición que podría arrojar una excepción de tiempo de ejecución para advertir de errores del programador. Incluso me doy una apariencia de tipeo estático al escribir:

def get_files_in(directories) unless File.directory? directories raise ArgumentError, "directories should be a file directory, you bozo :)" end # rest of my block end

No me parece que el lenguaje te impida hacer un diseño por contrato. Por el contrario, me parece que esto depende de los desarrolladores.

(Por cierto, "bozo" se refiere a los tuyos verdaderamente :)


De lo que se trata es de que get_files_in es una mala get_files_in en Ruby , déjame explicarte.

En java / C # / C ++, y especialmente en el objetivo C, los argumentos de la función son parte del nombre . En ruby ​​no lo son.
El término sofisticado para esto es Método de sobrecarga , y es aplicado por el compilador.

Pensando en eso en esos términos, solo está definiendo un método llamado get_files_in y no está diciendo en qué debería obtener los archivos. Los argumentos no son parte del nombre, por lo que no puede confiar en que lo identifiquen.
¿Debería obtener archivos en un directorio? ¿una vuelta? una red compartida? Esto abre la posibilidad de que funcione en todas las situaciones anteriores.

Si desea limitarlo a un directorio, para tener en cuenta esta información, debe llamar al método get_files_in_directory . De forma alternativa, podría convertirlo en un método en la clase Directory , que Ruby ya hace por usted .

En cuanto al tipo de devolución, está implícito desde get_files que está devolviendo una matriz de archivos. No tiene que preocuparse de que sea una List<File> o una ArrayList<File >, o algo así, porque todo el mundo usa solo arreglos (y si han escrito uno personalizado, lo escribirán para heredar de el conjunto integrado).

Si solo deseara obtener un archivo, lo llamaría get_file o get_first_file o más. Si está haciendo algo más complejo, como devolver objetos FileWrapper lugar de simplemente cadenas, entonces hay una solución realmente buena:

# returns a list of FileWrapper objects def get_files_in_directory( dir ) end

De todos modos. No se puede hacer cumplir los contratos en rubí como se puede en Java, pero este es un subconjunto del punto más amplio, que es que no se puede hacer cumplir en rubí nada como se puede en java. Debido a la sintaxis más expresiva de ruby, en su lugar escribes más claramente un código similar al inglés que le dice a otras personas cuál es tu contrato (ahorrándote varios miles de corchetes angulares).

Por mi parte, creo que esto es una ganancia neta. Puede usar su nuevo tiempo libre para escribir algunas especificaciones y pruebas y obtener un producto mucho mejor al final del día.


De ninguna manera es una pesadilla de mantenimiento, solo otra forma de trabajo, que requiere consistencia en la API y buena documentación.

Su preocupación parece estar relacionada con el hecho de que cualquier lenguaje dinámico es una herramienta peligrosa, que no puede hacer cumplir los contratos de entrada / salida API. El hecho es que, si bien la elección de estática puede parecer más segura, lo mejor que puede hacer en ambos mundos es mantener un buen conjunto de pruebas que verifiquen no solo el tipo de datos devueltos (que es lo único que el compilador de Java puede verificar y hacer cumplir), sino también su corrección y funcionamiento interno (prueba de caja negra / caja blanca).

Como nota al margen, no sé acerca de Ruby, pero en PHP puede usar etiquetas @phpdoc para insinuar el IDE (Eclipse PDT) sobre los tipos de datos devueltos por un método determinado.


El diseño por contrato es un principio mucho más sutil que simplemente especificar el tipo de argumento y un tipo de devolución. Otras respuestas aquí se concentran mucho en el buen nombre, lo cual es importante. Podría continuar sobre las muchas formas en que el nombre get_files_in es ambiguo. Pero un buen nombre es solo una consecuencia externa de un principio más profundo de tener buenos contratos y diseñar por ellos. Los nombres son siempre un poco ambiguos, y una buena lingüística pragmática es producto de un buen pensamiento.

Puede considerar los contratos como los principios de diseño, y con frecuencia son difíciles y aburridos de expresar de forma abstracta. Un lenguaje sin tipo requiere que el programador piense en contratos de verdad, que los entienda a un nivel más profundo que solo por restricciones de tipo. Si hay un equipo, los miembros del equipo deben significar y cumplir con los mismos contratos. Deben ser pensadores dedicados y deben pasar tiempo juntos discutiendo ejemplos concretos para establecer una comprensión compartida de los contratos.

Los mismos requisitos se aplican al usuario API: el usuario primero debe memorizar la documentación, y luego ella es capaz de entender gradualmente los contratos, y comenzar a amar la API si los contratos se diseñan cuidadosamente (u odiarlo si no).

Esto está conectado a pato escribir. Un contrato debe dar pistas sobre lo que sucede independientemente del tipo de las entradas de método. Entonces el contrato debe ser entendido de una manera más profunda y más generalizada. Esta respuesta en sí misma puede parecer un tanto intrascendente, o incluso arrogante, por lo que me disculpo. Simplemente estoy tratando de decir que el pato no es una mentira , el pato significa que uno piensa en su problema en un nivel más alto de abstracción. Los diseñadores, los programadores y los matemáticos son todos nombres diferentes para la misma capacidad , y los matemáticos saben que hay muchos niveles de aptitud en matemáticas, donde los matemáticos de un nivel superior siguiente resuelven fácilmente problemas que aquellos en niveles inferiores encuentran demasiado difíciles de resolver. . El pato significa que su programación tiene que ser una buena matemática, y restringe a los desarrolladores y usuarios exitosos solo a aquellos que pueden hacerlo .



Validación del método mediante pato-tipado:

i = {} => {} i.methods.sort => ["==", "===", "=~", "[]", "[]=", "__id__", "__send__", "all?", "any?", "class", "clear", "clone", "collect", "default", "default=", "default_proc", "delete", "delete_if", "detect", "display", "dup", "each", "each_key", "each_pair", "each_value", "each_with_index", "empty?", "entries", "eql?", "equal?", "extend", "fetch", "find", "find_all", "freeze", "frozen?", "gem", "grep", "has_key?", "has_value?", "hash", "id", "include?", "index", "indexes", "indices", "inject", "inspect", "instance_eval", "instance_of?", "instance_variable_defined?", "instance_variable_get", "instance_variable_set", "instance_variables", "invert", "is_a?", "key?", "keys", "kind_of?", "length", "map", "max", "member?", "merge", "merge!", "method", "methods", "min", "nil?", "object_id", "partition", "private_methods", "protected_methods", "public_methods", "rehash", "reject", "reject!", "replace", "require", "respond_to?", "select", "send", "shift", "singleton_methods", "size", "sort", "sort_by", "store", "taint", "tainted?", "to_a", "to_hash", "to_s", "type", "untaint", "update", "value?", "values", "values_at", "zip"] i.respond_to?(''keys'') => true i.respond_to?(''get_files_in'') => false

Una vez que hayas descifrado ese razonamiento, las firmas de métodos son discutibles porque puedes probarlas dinámicamente en la función. (Esto se debe en parte a que no se puede hacer la firma-coincidencia-función-función-despacho, pero esto es más flexible porque se pueden definir combinaciones ilimitadas de firmas)

def get_files_in(directories) fail "Not a List" unless directories.instance_of?(''List'') end def example2( *params ) lists = params.map{|x| (x.instance_of?(List))?x:nil }.compact fail "No list" unless lists.length > 0 p lists[0] end x = List.new get_files_in(x) example2( ''this'', ''should'', ''still'' , 1,2,3,4,5,''work'' , x )

Si desea una prueba más confiable, puede probar RSpec para el desarrollo conducido por Comportamiento.


Yo argumentaría que aunque el método de Java le da más información, no le da suficiente información para programar cómodamente.
Por ejemplo, ¿es esa Lista de cadenas solo nombres de archivo o rutas totalmente calificadas?

Dado que, su argumento de que Ruby no le da suficiente información también se aplica a Java.
Todavía dependes de leer la documentación, mirar el código fuente o llamar al método y ver su resultado (y pruebas decentes por supuesto).


Respuesta corta: pruebas unitarias automatizadas y buenas prácticas de nomenclatura.

El nombre apropiado de los métodos es esencial. Al dar el nombre get_files_in(directory) a un método, también está dando una pista a los usuarios sobre lo que el método espera obtener y lo que devolverá a cambio. Por ejemplo, no esperaría que salga un objeto Potato de get_files_in() ; simplemente no tiene sentido. Solo tiene sentido obtener una lista de nombres de archivos o, más apropiadamente, una lista de instancias de archivos de ese método. En cuanto al tipo concreto de la lista, dependiendo de lo que quería hacer, el tipo real de lista devuelta no es realmente importante. Lo importante es que de alguna manera puede enumerar los elementos en esa lista.

Finalmente, lo explicita escribiendo pruebas unitarias contra ese método, mostrando ejemplos de cómo debería funcionar. De modo que si get_files_in devuelve de repente una papa, la prueba generará un error y sabrá que las suposiciones iniciales ahora son incorrectas.