Principio de mínimo asombro (POLA) e interfaces

17

Hace un cuarto de siglo, cuando estaba aprendiendo C ++, me enseñaron que las interfaces deberían ser indulgentes y, en la medida de lo posible, no preocuparse por el orden en que se llamaron los métodos, ya que el consumidor puede no tener acceso a la fuente o la documentación en lugar de esta.

Sin embargo, cada vez que he sido mentor de programadores junior y desarrolladores senior me han escuchado, reaccionan con asombro, lo que me hace preguntarme si esto realmente fue una cosa o si simplemente se ha pasado de moda.

¿Tan claro como el barro?

Considere una interfaz con estos métodos (para crear archivos de datos):

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

Ahora, por supuesto, podría ir a través de estos en orden, pero decir que no le importaba el nombre del archivo (pensar a.out) o qué encabezado y cadena de avance se incluían, simplemente podía llamar AddDataLine.

Un ejemplo menos extremo podría ser omitir los encabezados y trailers.

Otro podría estar configurando las cadenas de encabezado y avance antes de que se abra el archivo.

¿Es este un principio de diseño de interfaz reconocido o solo POLA antes de que se le pusiera un nombre?

NB no se atasque en las minucias de esta interfaz, es solo un ejemplo en aras de esta pregunta.

Robbie Dee
fuente
10
El principio de "menos asombro" prevalece mucho más en el diseño de la interfaz de usuario que en el diseño de "interfaz de programador de aplicaciones". La razón es que no se puede esperar que un usuario de un sitio web o programa lea ninguna instrucción antes de usarlo, mientras que se espera que un programador, al menos en principio, lea los documentos API antes de programar con ellos.
Kilian Foth
77
@KilianFoth: Estoy bastante seguro de que Wikipedia está equivocado al respecto: POLA no se trata solo del diseño de la interfaz de usuario, sino que Bob Martin también utiliza el término "principio de la menor sorpresa" (que es lo mismo) para el diseño de funciones y clases. Libro "Código limpio".
Doc Brown
2
A menudo, una interfaz inmutable es mejor de todos modos. Puede especificar todos los datos que desea establecer en el momento de la construcción. No quedan ambigüedades y la clase se vuelve más simple de escribir. (A veces, este esquema no es posible, por supuesto.)
usr
44
No estoy de acuerdo por completo acerca de que POLA no se aplica a las API. Se aplica a todo lo que un humano crea para otros humanos. Cuando las cosas actúan como se espera, son más fáciles de conceptualizar y, por lo tanto, crean una carga cognitiva más baja, lo que permite a las personas hacer más cosas con menos esfuerzo.
Gort the Robot

Respuestas:

25

Una forma en la que puede apegarse al principio de menor asombro es considerar otros principios como ISP y SRP , o incluso DRY .

En el ejemplo específico que ha dado, la sugerencia parece ser que existe una cierta dependencia de ordenar para manipular el archivo; pero su API controla tanto el acceso a los archivos como el formato de datos, lo que huele un poco a una violación de SRP.

Editar / Actualizar: también sugiere que la API en sí misma le está pidiendo al usuario que viole DRY, ya que deberán repetir los mismos pasos cada vez que usen la API .

Considere una API alternativa donde las operaciones de E / S están separadas de las operaciones de datos. y donde la propia API 'posee' el pedido:

ContentBuilder

SetHeader( ... )
AddLine( ... )
SetTrailer ( ... )

FileWriter

Open(filename) 
Write(content) throws InvalidContentException
Close()

Con la separación anterior, ContentBuilderno es necesario "hacer" nada aparte de almacenar las líneas / encabezado / avance (tal vez también un ContentBuilder.Serialize()método que conoce el orden). Al seguir otros principios SÓLIDOS, ya no importa si configura el encabezado o el avance antes o después de agregar líneas, porque nada en el ContentBuilderarchivo se escribe realmente en el archivo hasta que se pasa a FileWriter.Write.

También tiene el beneficio adicional de ser un poco más flexible; por ejemplo, podría ser útil escribir el contenido en un registrador de diagnóstico, o tal vez pasarlo a través de una red en lugar de escribirlo directamente en un archivo.

Al diseñar una API, también debe considerar la notificación de errores, ya sea un estado, un valor de retorno, una excepción, una devolución de llamada u otra cosa. El usuario de la API probablemente esperará poder detectar mediante programación cualquier violación de sus contratos, o incluso otros errores que no pueda controlar, como errores de E / S de archivos.

Ben Cottrell
fuente
Exactamente lo que estaba buscando, ¡gracias! Del artículo del ISP: "(ISP) afirma que ningún cliente debería verse obligado a depender de métodos que no utiliza"
Robbie Dee
55
Esta no es una mala respuesta, sin embargo, el creador de contenido podría implementarse de manera tal que el orden de las llamadas SetHeadersea AddLineimportante. Para eliminar esta dependencia del orden no es ISP ni SRP, es simplemente POLA.
Doc Brown
Cuando el orden es importante, aún puede satisfacer a POLA definiendo las operaciones de manera que realizar pasos posteriores requiera un valor devuelto de los pasos anteriores, lo que hace cumplir el orden con el sistema de tipos. FileWriterpodría requerir el valor del último ContentBuilderpaso del Writemétodo para garantizar que todo el contenido de entrada esté completo, lo que hace InvalidContentExceptioninnecesario.
Dan Lyons
@DanLyons Creo que está bastante cerca de la situación que el autor de la pregunta intenta evitar; donde el usuario de la API necesita saber o preocuparse por el pedido. Idealmente, la API en sí misma debería hacer cumplir el pedido, de lo contrario, podría estar pidiendo al usuario que viole DRY. Esa es la razón para dividirse ContentBuildery permitir FileWriter.Writeencapsular ese bit de conocimiento. La excepción sería necesaria en caso de que algo esté mal con el contenido (por ejemplo, como un encabezado faltante). Un retorno también podría funcionar, pero no soy fanático de convertir las excepciones en códigos de retorno.
Ben Cottrell
Pero definitivamente vale la pena agregar más notas sobre DRY y ordenar a la respuesta.
Ben Cottrell
12

No se trata solo de POLA, sino también de prevenir un estado no válido como una posible fuente de errores.

Veamos cómo podemos proporcionar algunas restricciones a su ejemplo sin proporcionar una implementación concreta:

Primer paso: no permita que se llame a nada antes de abrir un archivo.

CreateDataFileInterface
  + OpenFile(filename : string) : DataFileInterface

DataFileInterface
  + SetHeaderString(header : string) : void
  + WriteDataLine(data : string) : void
  + SetTrailerString(trailer : string) : void
  + Close() : void

Ahora debería ser obvio que se CreateDataFileInterface.OpenFiledebe llamar para recuperar una DataFileInterfaceinstancia, donde se pueden escribir los datos reales.

Segundo paso: asegúrese de que los encabezados y trailers siempre estén configurados.

CreateDataFileInterface
  + OpenFile(filename : string, header: string, trailer : string) : DataFileInterface

DataFileInterface
  + WriteDataLine(data : string) : void
  + Close() : void

Ahora debe proporcionar todos los parámetros necesarios por adelantado para obtener un DataFileInterface: nombre de archivo, encabezado y avance. Si la cadena de avance no está disponible hasta que se escriben todas las líneas, también puede mover este parámetro aClose() (posiblemente renombrando el método a WriteTrailerAndClose()) para que el archivo al menos no pueda finalizar sin una cadena de avance.


Para responder al comentario:

Me gusta la separación de la interfaz. Pero me inclino a pensar que su sugerencia sobre la aplicación (por ejemplo, WriteTrailerAndClose ()) está al borde de una violación de SRP. (Esto es algo con lo que he luchado en varias ocasiones, pero su sugerencia parece ser un posible ejemplo). ¿Cómo respondería?

Cierto. No quería concentrarme más en el ejemplo de lo necesario para hacer mi punto, pero es una buena pregunta. En este caso, creo que lo llamaría Finalize(trailer)y argumentaría que no hace demasiado. Escribir el tráiler y cerrar son simples detalles de implementación. Pero si no está de acuerdo o tiene una situación similar en la que es diferente, aquí hay una posible solución:

CreateDataFileInterface
  + OpenFile(filename : string, header : string) : IncompleteDataFileInterface

IncompleteDataFileInterface
  + WriteDataLine(data : string) : void
  + FinalizeWithTrailer(trailer : string) : CompleteDataFileInterface

CompleteDataFileInterface
  + Close()

Realmente no lo haría para este ejemplo, pero muestra cómo llevar a cabo la técnica en consecuencia.

Por cierto, supuse que los métodos deben llamarse en este orden, por ejemplo, para escribir secuencialmente muchas líneas. Si esto no es necesario, siempre preferiría un constructor, como lo sugirió Ben Cottrel .

Fabian Schmengler
fuente
1
Por desgracia, has caído en la trampa que te advertí explícitamente que evitaras desde el principio. No se requiere un nombre de archivo; tampoco lo son el encabezado y el avance. Pero el tema general de dividir la interfaz es bueno, así que +1 :-)
Robbie Dee
Oh, entonces te entendí mal, pensé que esto estaba describiendo la intención del usuario, no la implementación.
Fabian Schmengler
Me gusta la separación de la interfaz. Pero me inclino a pensar que su sugerencia sobre la aplicación (por ejemplo WriteTrailerAndClose()) está al borde de una violación de SRP. (Esto es algo con lo que he luchado en varias ocasiones, pero su sugerencia parece ser un posible ejemplo). ¿Cómo respondería?
kmote
1
La respuesta de @kmote fue demasiado larga para un comentario, vea mi actualización
Fabian Schmengler
1
Si el nombre del archivo es opcional, puede proporcionar una OpenFilesobrecarga que no requiera una.
5gon12eder