¿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 la Zona, move(Zone, Card) , que mueve una Tarjeta de la Zona dada a sí misma (realizada mediante técnicas de paquetes privados). De esta manera, puedo asegurar que ninguna tarjeta se saque de una zona y simplemente desaparezca; Solo se pueden mover a otra zona.

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

¿Hasta dónde debo llevar esta idea de Zona? ¿Alguien me puede dar 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?

    
pregunta codebreaker 04.12.2013 - 20:14

6 respuestas

72

No voy a abordar el problema de 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 haya programadores que recuerden los matices de cada clase y método que escribieron y nunca los llaman por error con un contrato incorrecto. Yo no soy uno de ellos. A menudo olvido cómo el código que escribí se supone que funciona a las pocas horas de escribirlo. Después de pensar que lo has entendido 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 (en ningún orden en particular) convenciones, pruebas unitarias y otras pruebas automatizadas, verificación de condiciones previas y documentación. Yo mismo he comprobado que las pruebas unitarias son invaluables porque ambas lo obligan a pensar en cómo se usará su contrato y luego brindan documentación sobre cómo se diseñó la interfaz.

    
respondido por el Michael K 04.12.2013 - 21:30
25

Normalmente sigo algunas reglas simples:

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

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

En cuanto a su caso específico, si no se supone que Zone sea usado y / o accedido por personas externas, haga que la clase sea un paquete privado (y posiblemente final ), o preferiblemente, use las colecciones Java ya te proporciona Están probados y no tienes que reinventar la rueda. Tenga en cuenta que esto no le impide utilizar aserciones en todo su código para asegurarse de que todo funciona como se esperaba.

    
respondido por el afsantos 04.12.2013 - 21:22
16

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

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

Parece que estás al borde 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.

Como ejemplo de la API de tu juego de cartas, hay algunos entornos en los que todo lo que se puede hacer para evitar las trampas es fundamental. Puede haber grandes cantidades de dinero real involucradas, por lo que tiene sentido colocar una gran cantidad de cheques para asegurarse de que no se pueda hacer trampa.

Por otro lado, debe tener presente los principios de SOLID, especialmente la responsabilidad única. Pedirle a la clase contenedora que audite efectivamente a 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, vulnerables) frente a qué es privado y menos expuesto. No soy un defensor total de un "revestimiento exterior duro con un interior suave", pero lo mejor de su esfuerzo es endurecer el exterior de su API.

No creo que el usuario final previsto de una biblioteca sea tan crítico para determinar cuánta programación defensiva implementó. Incluso con los módulos que escribo para mi propio uso, sigo poniendo una medida de verificación para asegurarme de que en el futuro no cometí ningún error involuntario al llamar a la biblioteca.

    
respondido por el GlenH7 04.12.2013 - 21:32
13

La codificación defensiva no es solo una buena idea para el código público. Es una idea excelente para cualquier código que no se deseche de inmediato. Claro, ya sabe cómo debe llamarse now , pero no tiene idea de lo bien que recordará esto dentro de seis meses cuando vuelva al proyecto.

La sintaxis básica de Java te da mucha defensa al azar en comparación con un lenguaje de nivel inferior o interpretado como C o Javascript respectivamente. Suponiendo que nombre sus métodos claramente y no tenga una "secuenciación de métodos" externa, es probable que se salga con la suya simplemente especificando argumentos como un tipo de datos correcto e incluyendo un comportamiento sensible si los datos escritos correctamente todavía no son válidos.

(En un lado, si las Cartas siempre tienen que estar en la zona, creo que obtienes una mejor relación calidad-precio al hacer que todas las cartas en juego sean referenciadas por una colección global a tu objeto de Juego, y hacer que la Zona sea es una propiedad de cada tarjeta. Pero como no sé qué hacen sus Zonas además de tener tarjetas, es difícil saber si eso es apropiado.)

    
respondido por el DougM 04.12.2013 - 21:32
1

Primero crea una clase que mantenga una lista de Zonas para que no pierdas una Zona o las cartas que contiene. Luego, puedes verificar que una transferencia esté dentro de tu ZoneList. Esta clase probablemente será una especie de singleton, ya que solo necesitarás una instancia, pero quizás desees conjuntos de Zonas más adelante, así que mantén tus opciones abiertas.

Segundo, no haga que Zone o ZoneList implementen la Colección o cualquier otra cosa a menos que espere que la necesite. Es decir, si una Zona o Lista de zonas se pasará a algo que espera una Colección, luego lo implementa. Puede deshabilitar un montón de métodos haciendo que lancen una excepción (UnimplementedException, o algo así) o haciendo que simplemente no hagan nada. (Piense muy duro antes de usar la segunda opción. Si lo hace porque es fácil, encontrará que le faltan errores que podría haber detectado antes).

Hay preguntas reales sobre lo que es "correcto". Pero una vez que averigües qué es lo que quieres hacer las cosas de esa manera. En dos años se habrá olvidado de todo esto, y si intenta usar el código, se enojará mucho con el tipo que lo escribió de manera tan poco intuitiva y no explicó nada.

    
respondido por el RalphChapin 04.12.2013 - 21:20
1

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

Esto no es realmente de lo que trata tu ejemplo. Usted está limitando su superficie API, por una razón muy específica. Como menciona GlenH7 , cuando el conjunto de cartas se va a usar en un juego real, con un ('utilizado' y 'no utilizado') un mazo, una mesa y manos, por ejemplo, definitivamente desea colocar cheques para asegurarse de que cada carta del conjunto esté presente una vez y solo una vez.

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

Sin embargo, esa implementación suena como un tipo derivado de un conjunto de tarjetas similar a Collection<Card> , con una API menos restrictiva. Por ejemplo, cuando desea crear una calculadora de valor de mano o una IA, seguramente querrá ser libre de elegir cuál y cuántas de las cartas que itera.

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

    
respondido por el CodeCaster 04.12.2013 - 22:09

Lea otras preguntas en las etiquetas