¿Debería un método perdonar con los argumentos que se pasan? [cerrado]


21

Supongamos que tenemos un método foo(String bar)que solo opera en cadenas que cumplen ciertos criterios; por ejemplo, debe estar en minúsculas, no debe estar vacío o tener solo espacios en blanco, y debe coincidir con el patrón [a-z0-9-_./@]+. La documentación del método establece estos criterios.

¿Debería el método rechazar todas y cada una de las desviaciones de este criterio, o debería ser más indulgente con algunos criterios? Por ejemplo, si el método inicial es

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
    }
    this.bar = bar;
}

Y el segundo método de perdón es

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        bar = bar.toLowerCase().trim().replaceAll(" ", "_");
        if (!bar.matches(BAR_PATTERN_STRING) {
            throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
        }
    }
    this.bar = bar;
}

¿Debería cambiarse la documentación para indicar que se transformará y establecerá el valor transformado si es posible, o debería mantenerse el método lo más simple posible y rechazar todas y cada una de las desviaciones? En este caso, barpodría ser configurado por el usuario de una aplicación.

El caso de uso principal para esto sería que los usuarios accedan a objetos desde un repositorio mediante un identificador de cadena específico. Cada objeto en el repositorio debe tener una cadena única para identificarlo. Estos repositorios podrían almacenar los objetos de varias maneras (servidor sql, json, xml, binario, etc.) y por eso traté de identificar el mínimo común denominador que coincidiría con la mayoría de las convenciones de nombres.


1
Esto probablemente depende en gran medida de su caso de uso. Cualquiera de los dos podría ser razonable, e incluso he visto clases que proporcionan ambos métodos y hacen que el usuario decida. ¿Podría dar más detalles sobre lo que se supone que debe hacer este método / clase / campo, para que podamos ofrecer algunos consejos reales?
Ixrec

1
¿Conoces a todos los que llaman al método? Como en, si lo cambia, ¿puede identificar de manera confiable a todos los clientes? Si es así, iría tan permisivo y tolerante como lo permitan las preocupaciones de rendimiento. También podría eliminar la documentación. Si no es así, y es parte de una API de biblioteca, me aseguraría de que el código implementara exactamente la API anunciada, ya que de lo contrario cambiar el código para que coincida con la documentación en el futuro puede generar informes de errores.
Jon Chesterfield

77
Podría argumentar que Separation of Concerns dice que si es necesario, debe tener una foofunción estricta que sea rigurosa en los argumentos que acepta, y tener una segunda función auxiliar que puede tratar de "limpiar" un argumento para ser utilizado foo. De esta manera, cada método tiene menos que hacer por sí mismo y se pueden administrar e integrar de manera más limpia. Si va por esa ruta, probablemente también sería útil alejarse de un diseño pesado de excepción; puede usar algo como en su Optionallugar, y luego tener las funciones que consumen fooexcepciones de lanzamiento si es necesario.
gntskn

1
Esto es como preguntar "alguien me hizo daño, ¿debería perdonarlo?" Obviamente, hay circunstancias en que uno u otro es apropiado. La programación puede no ser tan complicada como las relaciones humanas, pero definitivamente es lo suficientemente compleja como para que una receta general como esta no funcione.
Kilian Foth

2
@Boggin También te señalaría El principio de robustez reconsiderado . La dificultad surge cuando necesita expandir la implementación y la implementación indulgente conduce a un caso ambiguo con la implementación expandida.

Respuestas:


47

Su método debe hacer lo que dice que hace.

Esto evita errores, tanto de uso como de mantenedores que cambian el comportamiento más adelante. Ahorra tiempo porque los encargados del mantenimiento no necesitan dedicar tanto tiempo a descubrir qué está sucediendo.

Dicho esto, si la lógica definida no es fácil de usar, tal vez debería mejorarse.


8
Esta es la clave Si su método hace exactamente lo que dice que hace, el codificador que usa su método compensará su caso de uso específico. Nunca hagas algo indocumentado con el método solo porque creas que es útil. Si necesita cambiarlo, escriba un contenedor o cambie la documentación.
Nelson

Agregaría al comentario de @ Nelson que el método no debe diseñarse en el vacío. Si los programadores dicen que lo usarán pero compensarán y sus compensaciones tienen un valor de propósito general, considere hacerlos parte de la clase. (Por ejemplo, tener fooy fooForUncleanStringmétodos donde este último hace las correcciones antes de pasarlo al primero.)
Blrfl

20

Hay algunos puntos:

  1. Su implementación debe hacer lo que establece el contrato documentado y no debe hacer nada más.
  2. La simplicidad es importante, tanto para el contrato como para la implementación, aunque más para el primero.
  3. Tratar de corregir entradas erróneas agrega complejidad, de manera contraintuitiva no solo para contratar e implementar sino también para usar.
  4. Los errores solo deben detectarse temprano si eso mejora la depuración y no compromete demasiado la eficiencia.
    Recuerde que existen aserciones de depuración para diagnosticar errores lógicos en modo de depuración, lo que alivia principalmente cualquier inquietud de rendimiento.
  5. La eficiencia, siempre que el tiempo y el dinero disponibles lo permitan sin comprometer demasiado la simplicidad, siempre es un objetivo.

Si implementa una interfaz de usuario, los mensajes de error amigables (incluyendo sugerencias y otra ayuda) son parte de un buen diseño.
Pero recuerde que las API son para programadores, no para usuarios finales.


Un experimento de la vida real para ser difuso y permisivo con la entrada es HTML.
Lo que resultó en que todos lo hicieran de manera ligeramente diferente, y la especificación, ahora está documentada, es un tomo gigantesco lleno de casos especiales.
Ver la ley de Postel (" Sé conservador en lo que haces, sé liberal en lo que aceptas de los demás ") y un crítico tocando eso ( o uno mucho mejor que MichaelT me hizo saber ).


Otra pieza crítica del autor de sendmail: El principio de robustez reconsiderado

15

El comportamiento de un método debe ser claro, intuitivo, predecible y simple. En general, deberíamos dudar mucho en hacer un procesamiento adicional en la entrada de la persona que llama. Tales suposiciones sobre lo que pretendía la persona que llama invariablemente tienen muchos casos extremos que producen un comportamiento no deseado. Considere una operación tan simple como unir rutas de archivos. ¡Muchas (o quizás incluso la mayoría) de las funciones de unión de ruta de archivo descartarán en silencio cualquier ruta anterior si una de las rutas que se unen parece estar enraizada! Por ejemplo, /abc/xyzunido con /evildará como resultado justo /evil. Esto casi nunca es lo que pretendo cuando me uno a las rutas de archivos, pero debido a que no hay una interfaz que no se comporte de esta manera, me veo obligado a tener errores o escribir código adicional que cubra estos casos.

Dicho esto, hay raras ocasiones en que tiene sentido que un método sea "indulgente", pero siempre debe estar dentro del poder de la persona que llama decidir cuándo y si estos pasos de procesamiento se aplican a su situación. Entonces, cuando haya identificado un paso de preprocesamiento común que desea aplicar a los argumentos en una variedad de situaciones, debe exponer interfaces para:

  • La funcionalidad en bruto, sin ningún preprocesamiento.
  • El preprocesamiento paso por sí mismo .
  • La combinación de la funcionalidad en bruto y el preprocesamiento.

El último es opcional; solo debe proporcionarlo si lo usará una gran cantidad de llamadas.

Al exponer la funcionalidad sin formato, la persona que llama puede usarla sin el paso de preprocesamiento cuando sea necesario. Exponer el paso del preprocesador por sí solo permite a la persona que llama usarlo para situaciones en las que ni siquiera están llamando a la función o cuando desean preprocesar alguna entrada antes de llamar a la función (como cuando primero quieren pasarla a otra función). Proporcionar la combinación permite a las personas que llaman invocar a ambos sin problemas, lo cual es útil principalmente si la mayoría de las personas que llaman lo usarán de esta manera.


2
+1 para predecible. Y otro +1 (deseo) para simple. Prefiero que me ayudes a detectar y corregir mis errores que tratar de ocultarlos.
John M Gant

4

Como otros han dicho, hacer que la cadena coincida con "perdonar" significa introducir una complejidad adicional. Eso significa más trabajo en la implementación de la correspondencia. Ahora tiene muchos más casos de prueba, por ejemplo. Debe realizar un trabajo adicional para asegurarse de que no haya nombres semánticamente iguales en el espacio de nombres. Más complejidad también significa que hay más errores en el futuro. Un mecanismo más simple, como una bicicleta, requiere menos mantenimiento que uno más complejo, como un automóvil.

Entonces, ¿vale la coincidencia de cadena indulgente con todo ese costo adicional? Depende del caso de uso, como han señalado otros. Si las cadenas son algún tipo de entrada externa sobre la que no tiene control, y hay una ventaja definitiva para la coincidencia indulgente, puede valer la pena. Quizás la información proviene de usuarios finales que pueden no ser muy conscientes de los caracteres espaciales y las mayúsculas, y usted tiene un fuerte incentivo para hacer que su producto sea más fácil de usar.

Por otro lado, si la entrada viniera de, digamos, archivos de propiedades ensamblados por técnicos, quienes deberían entender eso "Fred Mertz" != "FredMertz", estaría más inclinado a hacer que la correspondencia sea más estricta y ahorrar el costo de desarrollo.

Sin embargo, creo que, en cualquier caso, es útil recortar y no tener en cuenta los espacios iniciales y finales. He visto demasiadas horas desperdiciadas en la depuración de este tipo de problemas.


3

Mencionas algunos de los contextos de los que proviene esta pregunta.

Dado eso, me gustaría que el método hiciera solo una cosa, afirma los requisitos en la cadena, deja que se ejecute en función de eso: no trataría de transformarlo aquí. Mantenlo simple y claro; documentarlo y tratar de mantener la documentación y el código sincronizados entre sí.

Si desea transformar los datos que provienen de la base de datos de usuarios de una manera más indulgente, coloque esa funcionalidad en un método de transformación separado y documente la funcionalidad vinculada .

En algún momento, los requisitos de la función deben medirse, documentarse claramente y la ejecución debe continuar. El "perdón", en este punto, es un poco mudo, es una decisión de diseño y yo diría que la función no muta su argumento. Hacer que la función mute la entrada oculta parte de la validación que se requeriría del cliente. Tener una función que hace la mutación ayuda al cliente a hacerlo bien.

El gran énfasis aquí es la claridad y documentar lo que hace el código .


-1
  1. Puede nombrar un método de acuerdo con la acción como doSomething (), takeBackUp ().
  2. Para facilitar el mantenimiento, puede mantener los contratos comunes y la validación de los diferentes procedimientos. Llámalos según los casos de uso.
  3. Programación defensiva: su procedimiento maneja una amplia gama de entradas, incluidas (las cosas mínimas que son casos de uso deben cubrirse de todos modos)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.