¿Qué tan necesario es seguir las prácticas de programación defensiva para el código que nunca se pondrá a disposición del público?

45

Estoy escribiendo una implementación Java de un juego de cartas, así que creé un tipo especial de Colección que llamo Zona. Todos los métodos de modificación de la Colección de Java no son compatibles, pero hay un método en la API de Zona move(Zone, Card), que mueve una Tarjeta de la Zona dada a sí misma (lograda por técnicas de paquete privado). De esta manera, puedo asegurarme de que no se saquen cartas de una zona y simplemente desaparezcan; solo se pueden mover a otra zona.

Mi pregunta es, ¿qué tan necesario es este tipo de codificación defensiva? Es "correcto", y se siente como la práctica correcta, pero no es como si la API de Zone fuera a formar parte de alguna biblioteca pública. Es solo para mí, por lo que es como si estuviera protegiendo mi código de mí mismo cuando probablemente podría ser más eficiente simplemente usando Colecciones estándar.

¿Hasta dónde debo llevar esta idea de Zona? ¿Alguien puede darme algún consejo sobre cuánto debería pensar en preservar los contratos en las clases que escribo, especialmente para aquellos que realmente no estarán disponibles públicamente?

descifrador de códigos
fuente
44
= ~ s / necesario / recomendado / gi
GrandmasterB
2
Los tipos de datos deben ser correctos por construcción, o de lo contrario, ¿sobre qué está construyendo? Deben encapsularse de tal manera que, mutables o no, solo puedan estar en estados válidos. Solo si es imposible aplicar esto de forma estática (o si es excesivamente difícil), debe generar un error de tiempo de ejecución.
Jon Purdy
1
Nunca digas nunca. A menos que su código nunca se use, nunca podrá saber con certeza dónde terminará su código. ;)
Izkata
1
El comentario de @codebreaker GrandmasterB es una expresión de reemplazo. Significa: reemplazar "necesario" con "recomendado".
Ricardo Souza
1
El Código Codeless # 116 Trust No One es particularmente apropiado aquí.

Respuestas:

72

No voy a abordar el problema del diseño, solo la cuestión de si hacer las cosas "correctamente" en una API no pública.

es solo para mí, así que es como si estuviera protegiendo mi propio código de mí mismo

Ese es exactamente el punto. Tal vez hay codificadores que recuerdan los matices de cada clase y método que escribieron y nunca los llaman por error con el contrato incorrecto. No soy uno de ellos A menudo olvido cómo se supone que el código que escribí funciona unas horas después de haberlo escrito. Después de pensar que lo has hecho bien una vez, tu mente tenderá a cambiar de marcha al problema en el que estás trabajando ahora .

Tienes herramientas para combatir eso. Estas herramientas incluyen (sin un orden particular) convenciones, pruebas unitarias y otras pruebas automatizadas, verificación de precondiciones y documentación. Yo mismo he encontrado que las pruebas unitarias son invaluables porque ambas te obligan a pensar en cómo se usará tu contrato y a proporcionar documentación más adelante sobre cómo se diseñó la interfaz.

Michael K
fuente
Bueno saber. En el pasado solía programar tan eficientemente como podía, por lo que a veces me cuesta mucho acostumbrarme a ideas como esta. Me alegro de ir en la dirección correcta.
Codebreaker
15
¡"Eficientemente" puede significar muchas cosas diferentes! En mi experiencia, los novatos (no es que esté diciendo que eres uno) a menudo pasan por alto la eficacia con la que podrán apoyar el programa. El código generalmente pasa mucho más tiempo en la fase de soporte de su ciclo de vida del producto que en la fase de "escribir código nuevo", por lo que creo que es una eficiencia que debe considerarse con cuidado.
Charlie Kilian
2
Definitivamente estoy de acuerdo. Sin embargo, en la universidad nunca tuve que pensar en eso.
Codebreaker
25

Usualmente sigo algunas reglas simples:

  • Intenta programar siempre por contrato .
  • Si un método está disponible públicamente o recibe información del mundo exterior , aplique algunas medidas defensivas (por ejemplo IllegalArgumentException).
  • Para todo lo demás que solo es accesible internamente, use aserciones (por ejemplo assert input != null).

Si un cliente está realmente interesado, siempre encontrará una manera de hacer que su código se comporte mal. Siempre pueden hacerlo a través de la reflexión, al menos. Pero esa es la belleza del diseño por contrato . No aprueba el uso de su código, por lo que no puede garantizar que funcione en tales escenarios.

En cuanto a su caso específico, si Zonese supone que no debe ser utilizado y / o al que no acceden personas ajenas, haga que la clase sea privada (y posiblemente final), o preferiblemente, use las colecciones que Java ya le proporciona. Se prueban y no tiene que reinventar la rueda. Tenga en cuenta que esto no le impide usar aserciones en todo el código para asegurarse de que todo funcione como se espera.

afsantos
fuente
1
+1 por mencionar Diseño por contrato. Si no puede prohibir completamente el comportamiento (y eso es difícil de hacer), al menos deje en claro que no hay garantías sobre el mal comportamiento. También me gusta lanzar una IllegalStateException o una UnsupportedOperationException.
user949300
@ user949300 Claro. Me gusta creer que tales excepciones se introdujeron con un propósito significativo. Honrar los contratos parece encajar en ese papel.
afsantos
16

La programación defensiva es algo muy bueno.
Hasta que comience a interferir en la escritura del código. Entonces no es tan bueno.


Hablando un poco más pragmáticamente ...

Parece que estás a punto de llevar las cosas demasiado lejos. El desafío (y la respuesta a su pregunta) radica en comprender cuáles son las reglas o requisitos comerciales del programa.

Usando su API de juego de cartas como ejemplo, hay algunos entornos donde todo lo que se puede hacer para evitar las trampas es crítico. Pueden estar involucrados grandes cantidades de dinero real, por lo que tiene sentido establecer una gran cantidad de cheques para asegurarse de que no se pueda hacer trampa.

Por otro lado, debe tener en cuenta los principios SÓLIDOS, especialmente la responsabilidad individual. Pedirle a la clase contenedor que audite eficazmente hacia dónde van las tarjetas puede ser un poco demasiado. Puede ser mejor tener una capa de auditoría / controlador entre el contenedor de la tarjeta y la función que recibe las solicitudes de movimiento.


En relación con esas inquietudes, debe comprender qué componentes de su API están expuestos públicamente (y, por lo tanto, son vulnerables) frente a lo que es privado y menos expuesto. No soy un defensor total de un "revestimiento exterior duro con un interior blando", pero el mejor retorno de su esfuerzo es endurecer el exterior de su API.

No creo que el usuario final previsto de una biblioteca sea tan crítico con la determinación sobre la cantidad de programación defensiva que implementa. Incluso con los módulos que escribo para mi propio uso, aún pongo una medida de verificación en el lugar para asegurarme de que en el futuro no haya cometido un error inadvertido al llamar a la biblioteca.


fuente
2
+1 para "Hasta que comience a interferir en la escritura del código". Especialmente para proyectos personales a corto plazo, la codificación defensiva puede llevar mucho más tiempo del que vale.
Corey
2
De acuerdo, aunque me gustaría agregar que es bueno poder / poder / programar a la defensiva, pero también es crucial poder programar en forma de prototipos. La capacidad de hacer ambas cosas le permitirá elegir la acción más adecuada, que es mucho mejor que la de muchos programadores que conozco que solo pueden programar (más o menos) a la defensiva.
David Mulder
13

La codificación defensiva no es solo una buena idea para el código público. Es una gran idea para cualquier código que no se descarte de inmediato. Claro, ya sabe cómo se debe llamar ahora , pero no tiene idea de qué tan bien recordará estos seis meses a partir de ahora cuando regrese al proyecto.

La sintaxis básica de Java le brinda mucha defensa integrada en comparación con un lenguaje de nivel inferior o interpretado como C o Javascript, respectivamente. Suponiendo que nombra sus métodos con claridad y no tiene una "secuencia de métodos" externa, probablemente pueda salirse con la simple especificación de argumentos como un tipo de datos correcto e incluir un comportamiento sensato si los datos escritos correctamente aún pueden ser inválidos.

(Por otro lado, si las Cartas siempre tienen que estar en la zona, creo que obtienes mejores resultados al hacer que todas las cartas en juego sean referenciadas por una colección global a tu objeto de Juego, y que Zone sea una propiedad de cada carta, pero como no sé qué hacen tus zonas además de tener cartas, es difícil saber si eso es apropiado).

DougM
fuente
1
Consideraba que la zona era una propiedad de la carta, pero como mis Cartas funcionan mejor como objetos inmutables, decidí que esta era la mejor. Gracias por el consejo.
Codebreaker
3
@codebreaker una cosa que puede ayudar en ese caso es encapsular la tarjeta en otro objeto. Un as de espadas es lo que es. La ubicación no define su identidad, y una tarjeta probablemente debería ser inmutable. Tal vez tenga una Zona que contenga cartas: tal vez tenga una CardDescriptorque contenga una carta, su ubicación, estado boca arriba / abajo, o incluso rotación para juegos que se preocupan por eso. Esas son todas propiedades mutables que no alteran la identidad de una tarjeta.
1

Primero cree una clase que mantenga una lista de Zonas para que no pierda una Zona o las cartas en ella. Luego puede verificar que hay una transferencia dentro de su ZoneList. Esta clase probablemente será una especie de singleton, ya que solo necesitará una instancia, pero es posible que desee conjuntos de zonas más adelante, así que mantenga abiertas sus opciones.

En segundo lugar, no haga que Zone o ZoneList implemente Collection o cualquier otra cosa a menos que espere necesitarla. Es decir, si una Zona o ZoneList se pasará a algo que espera una Colección, entonces impleméntela . Puede deshabilitar varios métodos haciendo que arrojen una excepción (UnimplementedException, o algo así) o haciendo que simplemente no hagan nada. (Piense mucho antes de usar la segunda opción. Si lo hace porque es fácil, descubrirá que le faltan errores que podría haber detectado desde el principio).

Hay preguntas reales sobre lo que es "correcto". Pero una vez que descubras qué es, querrás hacer las cosas de esa manera. En dos años te habrás olvidado de todo esto, y si intentas usar el código, te enojarás mucho con el tipo que lo escribió de una manera tan contradictoria y no explicó nada.

RalphChapin
fuente
2
Su respuesta se centra demasiado en el problema en cuestión en lugar de las preguntas más generales que el OP hace sobre la programación defensiva en general.
De hecho, paso las zonas a los métodos que toman colecciones, por lo que la implementación es necesaria. Sin embargo, una especie de registro de zonas dentro del juego es una idea interesante.
Codebreaker
@ GlenH7: Creo que trabajar con ejemplos específicos a menudo ayuda más que la teoría abstracta. El OP proporcionó uno bastante interesante, así que fui con eso.
RalphChapin
1

La codificación defensiva en el diseño de API generalmente se trata de validar la entrada y seleccionar cuidadosamente un mecanismo de manejo de errores adecuado. Cosas que otras respuestas mencionan también son dignas de mención.

Esto no es realmente de lo que se trata su ejemplo. Estás limitando tu superficie API, por una razón muy específica. Como GlenH7 menciona, cuando el juego de cartas se va a usar en un juego real, con un mazo ('usado' y 'no usado'), una mesa y manos, por ejemplo, definitivamente querrá poner cheques en su lugar para asegurarse de que cada uno La tarjeta del set está presente una y solo una vez.

Que hayas diseñado esto con "zonas", es una elección arbitraria. Dependiendo de la implementación (una zona solo puede ser una mano, un mazo o una mesa en el ejemplo anterior) podría muy bien ser un diseño completo.

Sin embargo, esa implementación suena como un tipo derivado de un Collection<Card>conjunto de tarjetas más parecido, con una API menos restrictiva. Por ejemplo, cuando desea construir una calculadora de valor de mano, o una IA, seguramente quiere ser libre de elegir qué y cuántas de cada cartas iterará.

Por lo tanto, es bueno exponer una API tan restrictiva, si el único objetivo de esa API es asegurarse de que cada tarjeta siempre esté en una zona.

CodeCaster
fuente