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 Person
tiene el atributo age
, él afirma que debería ser imposible decir, a partir de la notación, si Person.age
corresponde internamente a algo así return current_year - self.birth_date
o simplemente return self.age
, dónde self.age
se 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 Person
tuviera que diseñar una base de datos llena de objetos, ¿no sería importante saber si Person.age
es 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?
fuente
Respuestas:
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:
o:
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:
o
o podría ser:
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()
esO(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.
fuente
// O(n) Traverses the entire user list.
len
no puede hacer esto ... (al menos en algunas situaciones, esO(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)O(n)
?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
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.
fuente
Puede escribir si una llamada determinada es costosa o no. Mejor, use una convención de nomenclatura como
getAge
acceso rápido y /loadAge
ofetchAge
bú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.
fuente
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.
fuente
Get
mé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.get
método es equivalente a un acceso de atributo y, por lo tanto, no es costoso.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_method
yinstance.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.attribute
yinstance.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.
fuente
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.
fuente
string.length
que se recalcularán cada vez que cambie.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.
fuente
Creo que lo entendiste correctamente, pero también creo que tienes un buen punto. si
Person.age
se 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 luegoPerson.age
devolver 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.fuente
age
de aPerson
, 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).Person
clase, pero creo que la pregunta fue más general y esoPerson.age
fue 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.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
, elRight
, yWidth
propiedades 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 comofinal
campos 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
float
odouble
noint
), puede ser necesario documentar qué propiedades "definen" el valor de una clase. Por ejemplo, aunque se supone queLeft
másWidth
es igualRight
, las matemáticas de coma flotante a menudo son inexactas. Por ejemplo, supongaRectangle
que un que usa el tipoFloat
aceptaLeft
yWidth
como los parámetros del constructor se construyen conLeft
dado como1234567f
yWidth
como1.1f
. La mejorfloat
representación de la suma es 1234568.125 [que puede mostrarse como 1234568.13]; el siguiente más pequeñofloat
sería 1234568.0. Si la clase realmente almacenaLeft
yWidth
, puede informar el valor del ancho tal como se especificó. Sin embargo, si el constructor calculó enRight
función de la transferenciaLeft
yWidth
, y luego calculó enWidth
función deLeft
yRight
, informaría el ancho en1.25f
lugar de la transferencia1.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
SetLeftAndWidth
oSetLeftAndRight
, o bien dejar claro qué propiedades se están especificados y que están cambiando (por ejemploMoveRightEdgeToSetWidth
,ChangeWidthToSetLeftEdge
oMoveShapeToSetRightEdge
) .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.
fuente
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:
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.
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.
fuente
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)
fuente
Como la respuesta aceptada llega a la conclusión:
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.age
parareturn current_year - self.birth_date
pero si el método utiliza un bucle para calcular la edad (sí):Person.calculateAge()
fuente