¿La documentación en OOP debe evitar especificar si un "getter" realiza o no algún cálculo?

39

El programa de CS de mi escuela evita cualquier mención de programación orientada a objetos, por lo que he estado leyendo un poco para complementarlo, específicamente, Construcción de software orientado a objetos por Bertrand Meyer.

Meyer señala repetidamente que las clases deben ocultar tanta información sobre su implementación como sea posible, lo cual tiene sentido. En particular, argumenta repetidamente que los atributos (es decir, las propiedades estáticas, no computadas de las clases) y las rutinas (propiedades de las clases que corresponden a llamadas a funciones / procedimientos) deben ser indistinguibles entre sí.

Por ejemplo, si una clase Persontiene el atributo age, él afirma que debería ser imposible decir, a partir de la notación, si Person.agecorresponde internamente a algo así return current_year - self.birth_dateo simplemente return self.age, dónde self.agese ha definido como un atributo constante. Esto tiene sentido para mí. Sin embargo, continúa afirmando lo siguiente:

La documentación estándar del cliente para una clase, conocida como la forma abreviada de la clase, se diseñará para no revelar si una característica dada es un atributo o una función (en los casos en que podría serlo).

es decir, él afirma que incluso la documentación para la clase debería evitar especificar si un "getter" realiza o no algún cálculo.

Esto no lo sigo. ¿No es la documentación el único lugar donde sería importante informar a los usuarios de esta distinción? Si Persontuviera que diseñar una base de datos llena de objetos, ¿no sería importante saber si Person.agees una llamada costosa o no , para poder decidir si implementar o no algún tipo de caché? ¿He entendido mal lo que está diciendo, o es solo un ejemplo particularmente extremo de la filosofía de diseño de OOP?

Patrick Collins
fuente
1
Interesante pregunta. Hace poco pregunté acerca de algo muy similar: ¿cómo diseñaría una interfaz de modo que quede claro qué propiedades pueden cambiar su valor y cuáles permanecerán constantes? . Y obtuve una buena respuesta apuntando hacia la documentación, es decir, exactamente contra lo que Bertrand Meyer parece argumentar.
stakx
No he leído el libro. ¿Meyer da algún ejemplo del estilo de documentación que recomienda? Me resulta difícil imaginar lo que describiste trabajando para cualquier idioma.
user16764
1
@PatrickCollins Te sugiero que leas 'ejecución en el reino de los sustantivos' y te pongas detrás del concepto de verbos y sustantivos aquí. En segundo lugar, OOP NO es acerca de getters y setters, sugiero Alan Kay (inventor de OOP): programación y escala
AndreasScheinert
@AndreasScheinert: ¿te refieres a esto ? Me reí entre dientes de "todo por falta de un clavo de herradura", pero parece ser una queja sobre los males de la programación orientada a objetos.
Patrick Collins el
1
@PatrickCollins sí esto: steve-yegge.blogspot.com/2006/03/… ! Da algunos puntos para reflexionar, los otros son: debe convertir sus objetos en estructuras de datos (ab) usando setters.
AndreasScheinert

Respuestas:

58

No creo que el punto de Meyer sea que no deberías decirle al usuario cuando tienes una operación costosa. Si su función va a llegar a la base de datos, o hace una solicitud a un servidor web, y pasa varias horas computando, otro código necesitará saber eso.

Pero el codificador que usa su clase no necesita saber si ha implementado:

return currentAge;

o:

return getCurrentYear() - yearBorn;

Las características de rendimiento entre esos dos enfoques son tan mínimas que no debería importar. Al codificador que usa tu clase realmente no debería importarle lo que tienes. Ese es el punto de Meyer.

Pero ese no es siempre el caso, por ejemplo, suponga que tiene un método de tamaño en un contenedor. Eso podría implementarse:

return size;

o

return end_pointer - start_pointer;

o podría ser:

count = 0
for(Node * node = firstNode; node; node = node->next)
{
    count++
}
return count

La diferencia entre los dos primeros realmente no debería importar. Pero el último podría tener serias ramificaciones de rendimiento. Es por eso que el TEL, por ejemplo, dice que .size()es O(1). No documenta exactamente cómo se calcula el tamaño, pero me da las características de rendimiento.

Entonces : documentar problemas de rendimiento. No documente los detalles de implementación. No me importa cómo std :: sort ordena mis cosas, siempre que lo haga de manera adecuada y eficiente. Su clase tampoco debería documentar cómo calcula las cosas, pero si algo tiene un perfil de rendimiento inesperado, documente eso.

Winston Ewert
fuente
44
Además: documente primero la complejidad del tiempo / espacio, luego dé una explicación de por qué una función tiene esas propiedades. Por ejemplo:// O(n) Traverses the entire user list.
Jon Purdy
2
= (Algo tan trivial como Python lenno puede hacer esto ... (al menos en algunas situaciones, es O(n), como aprendimos en un proyecto en la universidad cuando sugerí almacenar la longitud en lugar de volver a calcularla en cada iteración del bucle)
Izkata
@Izkata, curioso. ¿Recuerdas qué estructura era O(n)?
Winston Ewert
@ WinstonEwert Lamentablemente no. Eso fue hace más de 4 años en un proyecto de minería de datos, y solo se lo sugerí a mi amigo por una corazonada porque había estado trabajando con C en otra clase ...
Izkata
1
@ JonPurdy Yo agregaría que en el código comercial normal, probablemente no tenga sentido especificar la complejidad big-O. Por ejemplo, un acceso a la base de datos O (1) probablemente será mucho más lento que el recorrido de la lista en memoria O (n), así que documente lo que importa. Pero ciertamente hay casos en los que la complejidad de la documentación es muy importante (colecciones u otro código con algoritmos pesados).
svick
16

Desde el punto de vista académico o de los puristas de CS, por supuesto es un fracaso describir en la documentación cualquier cosa sobre los aspectos internos de la implementación de una característica. Esto se debe a que el usuario de una clase idealmente no debe hacer suposiciones sobre la implementación interna de la clase. Si la implementación cambia, lo ideal es que ningún usuario se dé cuenta: la característica crea una abstracción y las partes internas deben mantenerse completamente ocultas.

Sin embargo, la mayoría de los programas del mundo real sufren la "Ley de abstracciones permeables" de Joel Spolsky , que dice

"Todas las abstracciones no triviales, hasta cierto punto, tienen fugas".

Eso significa que es prácticamente imposible crear una abstracción completa de recuadro negro de características complejas. Y un síntoma típico de esto son los problemas de rendimiento. Por lo tanto, para los programas del mundo real, puede ser muy importante qué llamadas son caras y cuáles no, y una buena documentación debe incluir esa información (o debe decir dónde se permite al usuario de una clase hacer suposiciones sobre el rendimiento, y dónde no )

Por lo tanto, mi consejo es: incluya la información sobre posibles llamadas costosas si escribe documentos para un programa del mundo real, y excluya para un programa que está escribiendo solo para fines educativos de su curso de CS, dado que cualquier consideración de rendimiento debe mantenerse intencionalmente fuera de alcance.

Doc Brown
fuente
+1, además de que la mayoría de la documentación que se crea es para que el próximo programador mantenga su proyecto, no el próximo programador para usarlo .
jmoreno
12

Puede escribir si una llamada determinada es costosa o no. Mejor, use una convención de nomenclatura como getAgeacceso rápido y / loadAgeo fetchAgebúsqueda costosa. Definitivamente desea informar al usuario si el método está realizando alguna IO.

Cada detalle que proporcione en la documentación es como un contrato que la clase debe cumplir. Debe informar sobre comportamientos importantes. A menudo, verá la indicación de complejidades con una notación O grande. Pero generalmente quieres ser breve y al grano.

Simon Bergot
fuente
1
+1 por mencionar que la documentación es tan parte del contrato de una clase como su interfaz.
Bart van Ingen Schenau
Yo apoyo esto. Además, en general, tratar de minimizar la necesidad de captadores proporcionando métodos con comportamiento.
sevenforce
9

Si tuviera que diseñar una base de datos llena de objetos Person, ¿no sería importante saber si Person.age es una llamada costosa?

Sí.

Es por eso que a veces uso Find()funciones para indicar que llamarlo puede llevar un tiempo. Esto es más una convención que otra cosa. El tiempo que toma para una función o atributo de retorno hace ninguna diferencia para el programa (aunque podría para el usuario), aunque entre los programadores no es una expectativa de que, si se declara como un atributo, el costo para llamar Cabe bajo.

En cualquier caso, debería haber suficiente información en el código en sí para deducir si algo es una función o atributo, por lo que realmente no veo la necesidad de decir eso en la documentación.

Robert Harvey
fuente
44
+1: esa convención es idiomática en bastantes lugares. Además, la documentación debe hacerse a nivel de la interfaz; en ese momento, no sabe cómo se implementa Person.Age.
Telastyn
@Telastyn: Nunca pensé en la documentación de esta manera; es decir, que debe hacerse a nivel de interfaz. Parece obvio ahora. +1 por ese valioso comentario.
stakx
Me gusta mucho esta respuesta. Un ejemplo perfecto de lo que describe que el rendimiento no es una preocupación para el programa en sí mismo sería si la Persona fuera una entidad recuperada de un servicio RESTful. GET es inherente, pero no es evidente si será barato o costoso. Por supuesto, esto no es necesariamente POO, pero el punto es el mismo.
maple_shaft
+1 para usar Getmétodos sobre atributos para indicar una operación más pesada. He visto suficiente código en el que los desarrolladores suponen que una propiedad es solo un descriptor de acceso y la usan varias veces en lugar de guardar el valor en una variable local, y así ejecutar un algoritmo muy complejo más de una vez. Si no existe una convención para no implementar tales propiedades y la documentación no insinúa la complejidad, entonces deseo que quien tenga que mantener dicha aplicación tenga buena suerte.
enzi
¿De dónde viene esta convención? Pensando en Java, lo esperaría al revés: el getmétodo es equivalente a un acceso de atributo y, por lo tanto, no es costoso.
sevenforce
3

Es importante tener en cuenta que la primera edición de este libro fue escrita en 1988, en los primeros días de OOP. Estas personas estaban trabajando con lenguajes más puramente orientados a objetos que se usan ampliamente en la actualidad. Nuestros lenguajes OO más populares hoy en día, C ++, C # y Java, tienen algunas diferencias bastante significativas con respecto a la forma en que funcionaban los primeros lenguajes, más puramente OO.

En un lenguaje como C ++ y Java, debe distinguir entre acceder a un atributo y una llamada a método. Hay un mundo de diferencia entre instance.getter_methody instance.getter_method(). Uno realmente obtiene su valor y el otro no.

Cuando se trabaja con un lenguaje más puramente OO, de la persuasión Smalltalk o Ruby (que parece ser que el lenguaje Eiffel utilizado en este libro es), se convierte en un consejo perfectamente válido. Estos idiomas implícitamente llamarán métodos para usted. No hay diferencia entre instance.attributey instance.getter_method.

No sudaría este punto ni lo tomaría demasiado dogmáticamente. La intención es buena: no desea que los usuarios de su clase se preocupen por detalles de implementación irrelevantes, pero no se traduce claramente a la sintaxis de muchos idiomas modernos.

Sean McSomething
fuente
1
Punto muy importante acerca de considerar el año en que se hizo la sugerencia. Nit: Smalltalk y Simula se remontan a los años 60 y 70, por lo que 88 es apenas "los primeros días".
luser droog
2

Como usuario, no necesita saber cómo se implementa algo.

Si el rendimiento es un problema, hay que hacer algo dentro de la implementación de la clase, no a su alrededor. Por lo tanto, la acción correcta es corregir la implementación de la clase o presentar un error al mantenedor.

Mouviciel
fuente
3
Sin embargo, ¿es siempre el caso que un método computacionalmente costoso es un error? Como ejemplo trivial, digamos que me preocupa resumir las longitudes de una serie de cadenas. Internamente, no sé si las cadenas en mi lenguaje son de estilo Pascal o estilo C. En el primer caso, dado que las cadenas "conocen" su longitud, puedo esperar que mi ciclo de suma de longitud tome tiempo lineal dependiendo de la cantidad de cadenas. También debería saber que las operaciones que cambian la longitud de las cadenas tendrán una sobrecarga asociada, ya string.lengthque se recalcularán cada vez que cambie.
Patrick Collins el
3
En el último caso, dado que la cadena no "conoce" su longitud, puedo esperar que mi ciclo de suma de longitud tome tiempo cuadrático (eso depende tanto del número de cadenas como de sus longitudes), pero las operaciones que cambian la longitud de cadenas será más barato. Ninguna de estas implementaciones es incorrecta, y ninguna de ellas merecería un informe de error, pero requieren estilos de codificación ligeramente diferentes para evitar contratiempos inesperados. ¿No sería más fácil si el usuario tuviera al menos una vaga idea de lo que estaba pasando?
Patrick Collins el
Entonces, si sabe que la clase de cadena implementa el estilo C, elegirá una forma de codificación teniendo en cuenta ese hecho. Pero, ¿qué pasa si la próxima versión de la clase de cadena implementa la nueva representación de estilo Foo? ¿Cambiará su código en consecuencia o aceptará la pérdida de rendimiento causada por suposiciones falsas en su código?
Mouviciel
Veo. Entonces, la respuesta OO a "¿Cómo puedo exprimir un poco de rendimiento adicional de mi código, confiando en una implementación específica?" es "No puedes". Y la respuesta a "Mi código es más lento de lo que esperaría, ¿por qué?" es "Necesita ser reescrito". ¿Es más o menos la idea?
Patrick Collins el
2
@PatrickCollins La respuesta OO se basa en interfaces, no en implementaciones. No utilice una interfaz que no incluya garantías de rendimiento como parte de la definición de la interfaz (como el ejemplo de C ++ 11 List.size que se garantiza O (1)). No requiere incluir detalles de implementación en la definición de la interfaz. Si su código es más lento de lo que quisiera, ¿hay alguna otra respuesta que tendrá que cambiarlo para que sea más rápido (después de perfilarlo para determinar los cuellos de botella)?
stonemetal
2

Cualquier pieza de documentación orientada al programador que no informa a los programadores sobre el costo de la complejidad de las rutinas / métodos es defectuosa.

  • Estamos buscando producir métodos sin efectos secundarios.

  • Si la ejecución de un método tiene una complejidad de tiempo de ejecución y / o complejidad de memoria que no sea O(1), en entornos con memoria o con restricción de tiempo, se puede considerar que tiene efectos secundarios .

  • El principio de la menor sorpresa se viola si un método hace algo completamente inesperado, en este caso, acaparando la memoria o perdiendo el tiempo de la CPU.

Cazador de ciervos
fuente
1

Creo que lo entendiste correctamente, pero también creo que tienes un buen punto. si Person.agese implementa con un cálculo costoso, creo que también me gustaría ver eso en la documentación. Podría marcar la diferencia entre llamarlo repetidamente (si es una operación económica) o llamarlo una vez y guardar el valor en caché (si es caro). No estoy seguro, pero creo que en este caso Meyer podría aceptar que se incluya una advertencia en la documentación.

Otra forma de manejar esto podría ser introducir un nuevo atributo cuyo nombre implica que podría llevarse a cabo un cálculo largo (como Person.ageCalculatedFromDB) y luego Person.agedevolver un valor que se almacena en caché dentro de la clase, pero esto puede no ser siempre apropiado y parece complicarse demasiado cosas, en mi opinión.

FrustratedWithFormsDesigner
fuente
3
También se podría argumentar que si necesita conocer el agede a Person, debe llamar al método para obtenerlo independientemente. Si las personas que llaman comienzan a hacer cosas demasiado ingeniosas para esquivar la necesidad de hacer el cálculo, corren el riesgo de que sus implementaciones no funcionen correctamente porque cruzaron un límite de cumpleaños. Las implementaciones costosas en la clase se manifestarán como problemas de rendimiento que pueden eliminarse mediante la creación de perfiles y mejoras como el almacenamiento en caché se pueden hacer en clase, donde todos los que llaman verán los beneficios (y los resultados correctos).
Blrfl
1
@Blrfl: bueno, sí, el almacenamiento en caché debe hacerse en la Personclase, pero creo que la pregunta fue más general y eso Person.agefue solo un ejemplo. Probablemente hay algunos casos en los que tendría más sentido elegir: la persona que llama tiene dos algoritmos diferentes para calcular el mismo valor: uno rápido pero impreciso, uno mucho más lento pero más preciso (la representación 3D viene a la mente como un lugar donde eso puede suceder), y la documentación debe mencionar esto.
FrustratedWithFormsDesigner
Dos métodos que brindan resultados diferentes es un caso de uso diferente que cuando se espera la misma respuesta cada vez.
Blrfl
0

La documentación para las clases orientadas a objetos a menudo implica una compensación entre darles a los mantenedores de la clase flexibilidad para cambiar su diseño, en lugar de permitir que los consumidores de la clase hagan pleno uso de su potencial. Si una clase inmutable tendrá una serie de propiedades que tendrán una cierta relación exacta entre sí (por ejemplo Left, el Right, yWidthpropiedades de un rectángulo alineado a la cuadrícula de coordenadas enteras), se podría diseñar la clase para almacenar cualquier combinación de dos propiedades y calcular la tercera, o se podría diseñar para almacenar las tres. Si nada sobre la interfaz deja en claro qué propiedades están almacenadas, el programador de la clase puede cambiar el diseño en caso de que hacerlo resulte útil por alguna razón. Por el contrario, si, por ejemplo, dos de las propiedades están expuestas como finalcampos y la tercera no, entonces las versiones futuras de la clase siempre tendrán que usar las mismas dos propiedades como "base".

Si las propiedades no tienen una relación exacta (por ejemplo, porque lo son floato doubleno int), puede ser necesario documentar qué propiedades "definen" el valor de una clase. Por ejemplo, aunque se supone que Leftmás Widthes igual Right, las matemáticas de coma flotante a menudo son inexactas. Por ejemplo, suponga Rectangleque un que usa el tipo Floatacepta Lefty Widthcomo los parámetros del constructor se construyen con Leftdado como 1234567fy Widthcomo 1.1f. La mejor floatrepresentación de la suma es 1234568.125 [que puede mostrarse como 1234568.13]; el siguiente más pequeño floatsería 1234568.0. Si la clase realmente almacena LeftyWidth, puede informar el valor del ancho tal como se especificó. Sin embargo, si el constructor calculó en Rightfunción de la transferencia Lefty Width, y luego calculó en Widthfunción de Lefty Right, informaría el ancho en 1.25flugar de la transferencia 1.1f.

Con las clases mutables, las cosas pueden ser aún más interesantes, ya que un cambio en uno de los valores interrelacionados implicará un cambio en al menos otro, pero no siempre está claro cuál. En algunos casos, puede ser mejor para evitar tener métodos que "set" una sola propiedad como tal, pero en su lugar o bien tienen métodos para por ejemplo SetLeftAndWidtho SetLeftAndRight, o bien dejar claro qué propiedades se están especificados y que están cambiando (por ejemplo MoveRightEdgeToSetWidth, ChangeWidthToSetLeftEdgeo MoveShapeToSetRightEdge) .

A veces puede ser útil tener una clase que haga un seguimiento de qué valores de propiedades se han especificado y cuáles se han calculado a partir de otros. Por ejemplo, una clase de "momento en el tiempo" podría incluir una hora absoluta, una hora local y un desplazamiento de zona horaria. Al igual que con muchos de estos tipos, dada cualquiera de las dos piezas de información, una puede calcular la tercera. Saber cualSin embargo, a veces se puede calcular información importante. Por ejemplo, suponga que un evento se registra como ocurrido a las "17:00 UTC, zona horaria -5, hora local 12:00 pm", y luego se descubre que la zona horaria debería haber sido -6. Si se sabe que el UTC se registró en un servidor, el registro debe corregirse a "18:00 UTC, zona horaria -6, hora local 12:00 pm"; si alguien ingresó la hora local de un reloj, debería ser "17:00 UTC, zona horaria -6, hora local 11:00 am". Sin saber si la hora global o local debe considerarse "más creíble", sin embargo, no es posible saber qué corrección debe aplicarse. Sin embargo, si el registro lleva un registro de la hora especificada, los cambios en la zona horaria podrían dejarlo solo mientras se cambia el otro.

Super gato
fuente
0

Todas estas reglas sobre cómo ocultar información en las clases tienen mucho sentido en el supuesto de que necesiten protegerse contra esa persona entre los usuarios de la clase que cometerá el error de crear una dependencia en la implementación interna.

Está bien incorporar esa protección, si la clase tiene tal audiencia. Pero cuando el usuario escribe una llamada a una función en su clase, confía en usted con su cuenta bancaria en tiempo de ejecución.

Este es el tipo de cosas que veo mucho:

  1. Los objetos tienen un bit "modificado" que dice si están, en cierto sentido, desactualizados. Es bastante simple, pero luego tienen objetos subordinados, por lo que es sencillo dejar que "modificado" sea una función que suma todos los objetos subordinados. Entonces, si hay varias capas de objetos subordinados (a veces compartiendo el mismo objeto más de una vez), los simples "Obtener" de la propiedad "modificada" pueden terminar tomando una fracción saludable del tiempo de ejecución.

  2. Cuando un objeto se modifica de alguna manera, se supone que otros objetos dispersos alrededor del software deben ser "notificados". Esto puede tener lugar en múltiples capas de estructura de datos, ventanas, etc. escritas por diferentes programadores y, a veces, repitiéndose en infinitas recursiones contra las que es necesario protegerse. Incluso si todos los escritores de esos manejadores de notificaciones son razonablemente cuidadosos de no perder el tiempo, toda la interacción compuesta puede terminar usando una fracción de tiempo de ejecución impredecible y dolorosamente grande, y se asume alegremente que es simplemente "necesario".

Entonces, me gusta ver clases que presenten una interfaz abstracta agradable y limpia para el mundo exterior, pero me gusta tener una idea de cómo funcionan, aunque solo sea para comprender qué trabajo me están ahorrando. Pero más allá de eso, tiendo a sentir que "menos es más". Las personas están tan enamoradas de la estructura de datos que piensan que más es mejor, y cuando hago el ajuste del rendimiento, la razón masiva universal para los problemas de rendimiento es la servil adhesión a las estructuras de datos infladas construidas de la manera en que se enseña a las personas.

Así que imagínate.

Mike Dunlavey
fuente
0

Agregar detalles de implementación como "calcular o no" o "información de rendimiento" hace que sea más difícil mantener el código y el documento sincronizados .

Ejemplo:

Si tiene un método "costoso para el rendimiento", ¿desea documentar "costoso" también para todas las clases que utilizan el método? ¿Qué pasa si cambia la implementación para que ya no sea costosa? ¿Desea actualizar esta información también para todos los consumidores?

Por supuesto, es bueno que un mantenedor de código obtenga toda la información importante de la documentación del código, pero no me gusta la documentación que dice algo que ya no es válido (no está sincronizado con el código)

k3b
fuente
0

Como la respuesta aceptada llega a la conclusión:

Entonces: documentar problemas de rendimiento.

y se considera que el código auto documentado es mejor que la documentación que se deduce que el nombre del método debe indicar los resultados de rendimiento inusuales.

Así que sigue Person.agepara return current_year - self.birth_datepero si el método utiliza un bucle para calcular la edad (sí):Person.calculateAge()

sevenforce
fuente