¿Buen estilo de código para introducir comprobaciones de datos en todas partes?

10

Tengo un proyecto que es lo suficientemente grande como para no poder mantener todos los aspectos en mi cabeza. Estoy tratando con una serie de clases y funciones, y estoy pasando datos.

Con el tiempo, noté que seguía recibiendo errores, porque olvidé qué forma precisa deben tener los datos cuando los paso a diferentes funciones ( por ejemplo, una función acepta y genera una matriz de cadenas, otra función, que escribí mucho más tarde, acepta cadenas que se guardan en un diccionario, etc., por lo que tengo que transformar las cadenas con las que estoy trabajando para tenerlas en una matriz y tenerlas en un diccionario ).

Para evitar tener que averiguar siempre qué es lo que se rompió, comencé a tratar cada función y clase como una "entidad aislada" en el sentido de que no puede confiar en que el código externo le dé la entrada correcta y tiene que realizar comprobaciones de entrada por sí mismo (o, en algunos casos, refundir los datos, si los datos se dan en forma incorrecta).

Esto ha reducido en gran medida el tiempo que paso asegurándome de que los datos que paso "encajen" en cada función, porque las clases y las funciones mismas ahora me advierten cuando alguna entrada es mala (y algunas veces incluso corrige eso) y no lo hago. tengo que ir con un depurador a través de todo el código para descubrir dónde algo se volvió loco.

Por otro lado, esto también ha aumentado el código general.
Mi pregunta es, si este estilo de código es apropiado para resolver este problema?
Por supuesto, la mejor solución sería refactorizar completamente el proyecto y asegurarse de que los datos tengan una estructura uniforme para todas las funciones, pero dado que este proyecto está creciendo constantemente, terminaría gastando más y preocupándome por un código limpio en lugar de agregar cosas nuevas. .

(FYI: todavía soy un principiante, así que disculpe si esta pregunta fue ingenua; mi proyecto está en Python).

usuario7088941
fuente
1
Posible duplicado de ¿Cuán defensivos debemos estar?
mosquito
3
@gnat Es similar, pero en lugar de responder mi pregunta, allí proporciona consejos ("sé lo más defensivo posible") para esa instancia específica que mencionó OP, que es diferente de mi consulta más general.
user7088941
2
"pero dado que este proyecto está creciendo constantemente, terminaría gastando más y preocupándome por un código limpio que por agregar cosas nuevas". Esto suena como si necesitara comenzar a preocuparse por el código limpio. De lo contrario, encontrará que su productividad se ralentiza y ralentiza a medida que cada nuevo bit de funcionalidad es cada vez más difícil de agregar debido al código existente. No toda la refactorización debe ser "completa", si agregar algo nuevo es difícil debido al código existente que toca, refactorice solo ese código conmovedor y tome nota de lo que le gustaría volver a visitar más tarde
Matt Freake el
3
Este es un problema que las personas a menudo enfrentan al usar idiomas de tipo débil. Si no desea o puede cambiar a un lenguaje más estrictamente tipado, la respuesta es simplemente "sí, este estilo de código es apropiado para resolver este problema" . ¿Próxima pregunta?
Doc Brown
1
En un lenguaje estrictamente escrito, con los tipos de datos adecuados definidos, el compilador lo habría hecho por usted.
SD

Respuestas:

4

Una mejor solución es aprovechar más las funciones y herramientas del lenguaje Python.

Por ejemplo, en la función 1, la entrada esperada es una matriz de cadenas, donde la primera cadena denota el título de algo y la segunda una referencia bibliográfica. En la función 2, la entrada esperada sigue siendo una matriz de cadenas, pero ahora los roles de las cadenas están invertidos.

Este problema se mitiga con a namedtuple. Es liviano y le da un significado semántico fácil a los miembros de su matriz.

Para aprovechar el beneficio de una verificación de tipo automática sin cambiar de idioma, puede aprovechar las sugerencias de tipo . Un buen IDE puede usar esto para avisarle cuando hace algo tonto.

También parece preocupado por las funciones obsoletas cuando cambian los requisitos. Esto puede detectarse mediante pruebas automatizadas .

Si bien no digo que la verificación manual nunca sea apropiada, un mejor uso de las funciones de idioma disponibles puede ayudarlo a resolver este problema de una manera más fácil de mantener.


fuente
+1 por señalarme namedtupley todas las otras cosas buenas. No lo hice namedtuple, y aunque sabía acerca de las pruebas automatizadas, realmente nunca lo usé mucho y no me di cuenta de cuánto me ayudaría en este caso. Todo esto realmente parece ser tan bueno como un análisis estático. (¡Las pruebas automatizadas podrían incluso ser mejores, ya que puedo captar todas las cosas sutiles que no se detectarían en un análisis estático!) Si conoce alguna otra, hágamelo saber. Voy a mantener la pregunta abierta un poco más, pero si no recibo ninguna otra respuesta, aceptaré la suya.
user7088941
9

OK, el problema real se describe en un comentario debajo de esta respuesta:

Por ejemplo, en la función 1, la entrada esperada es una matriz de cadenas, donde la primera cadena denota el título de algo y la segunda una referencia bibliográfica. En la función 2, la entrada esperada sigue siendo una matriz de cadenas, pero ahora los roles de las cadenas están invertidos

El problema aquí es el uso de una lista de cadenas donde el orden significa semántica. Este es un enfoque realmente propenso a errores. En su lugar, debe crear una clase personalizada con dos campos llamados titley bibliographical_reference. De esa manera no los mezclará y evitará este problema en el futuro. Por supuesto, esto requiere un poco de refactorización si ya usa listas de cadenas en muchos lugares, pero créame, a largo plazo será más barato.

El enfoque común en los lenguajes de tipos dinámicos es "escribir pato", lo que significa que realmente no te importa el "tipo" del objeto pasado, solo te importa si es compatible con los métodos que invocas. En su caso, simplemente leerá el campo llamado bibliographical_referencecuando lo necesite. Si este campo no existe en el objeto pasado, obtendrá un error, y esto indica que se pasó el tipo incorrecto a la función. Esta es una verificación de tipo tan buena como cualquiera.

JacquesB
fuente
A veces, el problema es aún más sutil: estoy pasando el tipo correcto, pero la "estructura interna" de mi entrada desordena la función: por ejemplo, en la función 1, la entrada esperada es una matriz de cadenas, donde la primera cadena denota el título de algo y el segundo una referencia bibliográfica. En la función 2, la entrada esperada sigue siendo una matriz de cadenas, pero ahora los roles de las cadenas están invertidos: la primera cadena debe ser la referencia bibliográfica y la segunda debe ser la referencia bibliográfica. Supongo que para estos controles son apropiados?
user7088941
1
@ user7088941: El problema que describe podría resolverse fácilmente teniendo una clase con dos campos: "título" y "referencia bibliográfica". No vas a mezclar eso. Confiar en el orden en una lista de cadenas parece muy propenso a errores. Tal vez este es el problema subyacente?
JacquesB
3
Esta es la respuesta. Python es un lenguaje orientado a objetos, no un lenguaje orientado a la lista de diccionarios de cadenas a enteros (o lo que sea). Entonces, usa objetos. Los objetos son responsables de administrar su propio estado y hacer cumplir sus propios invariantes, otros objetos no pueden corromperlos, nunca (si están diseñados correctamente). Si los datos no estructurados o semiestructurados ingresan a su sistema desde el exterior, debe validar y analizar una vez en el límite del sistema y convertirlos en objetos ricos lo antes posible.
Jörg W Mittag
3
"Realmente evitaría la refactorización constante" - este bloqueo mental es tu problema. Un buen código solo surge de la refactorización. Mucha refactorización. Apoyado por pruebas unitarias. Especialmente cuando los componentes deben extenderse o evolucionar.
Doc Brown
2
Lo entiendo ahora. +1 por todas las buenas ideas y comentarios. ¡Y gracias a todos por sus comentarios increíblemente útiles! (Mientras usaba algunas clases / objetos, los intercalé con las listas mencionadas, que, como veo ahora, no era una buena idea. La pregunta seguía siendo la mejor manera de implementar esto, donde usé las sugerencias concretas de la respuesta de JETM , que realmente marcó una diferencia radical en términos de velocidad para lograr un estado libre de errores.)
user7088941
3

En primer lugar, lo que está experimentando en este momento es el olor del código : intente recordar lo que lo llevó a ser consciente del olor y trate de perfeccionar su nariz "mental", ya que cuanto antes note un olor del código, más rápido y más fácil. puede solucionar el problema subyacente.

Para evitar tener que averiguar siempre qué se rompió, comencé a tratar cada función y clase como una "entidad aislada" en el sentido de que no puede confiar en que el código externo le dé la entrada correcta y tiene que realizar comprobaciones de entrada.

La programación defensiva, como se llama esta técnica, es una herramienta válida y de uso frecuente. Sin embargo, como con todas las cosas, es importante usar la cantidad correcta, cheques muy pequeños y no detectará problemas, demasiados y su código se sobre-hinchará.

(o, en algunos casos, refundir los datos, si los datos se dan en forma incorrecta).

Esa podría ser una idea menos buena. Si nota que una parte de su programa está llamando a una función con datos formateados incorrectamente, CORREE ESA PARTE , no cambie la función llamada para poder digerir datos incorrectos de todos modos.

Esto ha reducido en gran medida el tiempo que paso asegurándome de que los datos que paso "encajen" en cada función, porque las clases y las funciones mismas ahora me advierten cuando alguna entrada es mala (y algunas veces incluso corrige eso) y no lo hago. tengo que ir con un depurador a través de todo el código para descubrir dónde algo se volvió loco.

Mejorar la calidad y la capacidad de mantenimiento de su código es un ahorro de tiempo a largo plazo (en ese sentido, debo advertirle nuevamente contra la funcionalidad de autocorrección que incorporó en algunas de sus funciones: podrían ser una fuente insidiosa de errores. Solo porque su el programa no se bloquea y no significa que funcione correctamente ...)

Para responder finalmente a su pregunta: Sí, la programación defensiva (es decir, verificar la validez de los parámetros proporcionados) es, en buena medida, una buena estrategia. Dicho esto , como usted mismo ha dicho, su código es inconsitent, y me gustaría fuertemente recomiendo que pasar algún tiempo para refactorizar las partes que huelen - usted ha dicho que no quiere que preocuparse de código limpio todo el tiempo, pasar más tiempo en "limpiar" que en las nuevas funciones ... Si no mantiene limpio su código, puede pasar el doble de tiempo "guardar" para no mantener un código limpio en la eliminación de errores Y tendrá dificultades para implementar nuevas funciones: la deuda técnica puede aplastarte.

CharonX
fuente
1

Esta bien. Solía ​​codificar en FoxPro, donde tenía un bloque TRY..CATCH casi en cada gran función. Ahora, codifico en JavaScript / LiveScript y rara vez verifico parámetros en funciones "internas" o "privadas".

"Cuánto comprobar" depende del proyecto / idioma elegido más de lo que depende de su habilidad de código.

Michael Quad
fuente
1
Supongo que fue INTENTAR ... CAPTURAR ... IGNORAR. Hiciste lo contrario de lo que pide el OP. En mi humilde opinión, su punto es evitar inconsistencias, mientras que la suya se aseguraba de que el programa no explote al golpear uno.
maaartinus
1
@maaartinus eso es correcto. Los lenguajes de programación generalmente nos dan construcciones que son fáciles de usar para evitar la aplicación de la explosión, pero los lenguajes de programación de construcciones nos dan para evitar inconsistencias que parecen ser mucho más difíciles de usar: que yo sepa, refactorice constantemente todo y use las clases que mejor contengan. flujo de información en su aplicación. Esto es precisamente lo que estoy preguntando: ¿hay alguna manera más fácil de solucionarlo?
user7088941
@ user7088941 Es por eso que evito los idiomas débilmente escritos. Python es simplemente fantástico, pero para algo más grande, no puedo hacer un seguimiento de lo que hice en otro lugar. Por lo tanto, prefiero Java, que es bastante detallado (no tanto con las características de Lombok y Java 8), tiene una mecanografía estricta y herramientas para el análisis estático. Te sugiero que pruebes algunos tipos de lenguaje estrictamente, ya que no sé cómo resolverlo de otra manera.
maaartinus
No se trata de parámetros estrictos / sueltos. Se trata de saber que el parámetro es correcto. Incluso si usa (entero de 4 bytes) puede que necesite verificar si está en algún rango 0..10, por ejemplo. Si sabe que ese parámetro siempre es 0..10, no necesita verificarlo. FoxPro no tiene matrices asociativas, por ejemplo, es muy difícil de operar con sus variables, su alcance y así sucesivamente .. es por eso que usted tiene que comprobar cheque cheque ..
Michael Quad
1
@ user7088941 No es OO, pero existe la regla de "falla rápida". Cada método no privado tiene que verificar sus argumentos y lanzar cuando algo está mal. No intente atrapar, no intente arreglarlo, solo vuele por las nubes. Claro, en algún nivel superior, la excepción se registra y se maneja. Como sus pruebas encuentran la mayoría de los problemas de antemano y no se ocultan problemas, el código converge a una solución libre de errores mucho más rápido que cuando es tolerante a errores.
maaartinus