Es muy interesante para mí qué ventajas da el enfoque de "clase raíz global" para framework. En palabras simples, las razones que dieron como resultado .NET Framework fue diseñado para tener una clase de objeto raíz con funcionalidad general adecuada para todas las clases.
Hoy en día estamos diseñando un nuevo marco para uso interno (el marco bajo la plataforma SAP) y todos nos dividimos en dos campos: primero, quién piensa que ese marco debe tener una raíz global, y el segundo, quién piensa en sentido contrario.
Estoy en el campamento de "raíz global". Y mis razones por las cuales dicho enfoque produciría una buena flexibilidad y una reducción de los costos de desarrollo hacen que ya no desarrollemos una funcionalidad general.
Por lo tanto, estoy muy interesado en saber qué razones realmente empujan a los arquitectos .NET a diseñar el marco de esta manera.
fuente
object
en el marco .NET en parte porque proporciona algunas capacidades básicas en todos los objetos, comoToString()
yGetHashCode()
wait()
/notify()
/notifyAll()
yclone()
.Respuestas:
La causa más apremiante
Object
son los contenedores (antes de los genéricos) que podrían contener cualquier cosa en lugar de tener que ir al estilo C "Escríbelo nuevamente para todo lo que necesites". Por supuesto, podría decirse que la idea de que todo debería heredar de una clase específica y luego abusar de este hecho para perder por completo cada mota de seguridad de tipo es tan terrible que debería haber sido una gran luz de advertencia sobre el envío del idioma sin genéricos, y también significa esoObject
es completamente redundante para el nuevo código.fuente
Object
que no se pueda escribirvoid *
en C ++?void*
era mejor? No lo es Si algo. Es aún peor .void*
es peor en C con todos los lanzamientos de puntero implícitos, pero C ++ es más estricto a este respecto. Al menos tienes que lanzar explícitamente.Recuerdo que Eric Lippert dijo una vez que heredar de la
System.Object
clase proporcionaba "el mejor valor para el cliente". [editar: sí, lo dijo aquí ]:Tener todo derivado de la
System.Object
clase proporciona una fiabilidad y utilidad que he llegado a respetar mucho. Sé que todos los objetos tendrán un tipo (GetType
), que estarán en línea con los métodos de finalización de CLR (Finalize
), y que puedo usarlosGetHashCode
cuando se trate de colecciones.fuente
GetType
, simplemente puede extendertypeof
para reemplazar su funcionalidad. En cuanto aFinalize
, entonces, en realidad, muchos tipos no son completamente finalizables y no poseen un método de Finalización significativo. Además, muchos tipos no son intercambiables ni convertibles en cadenas, especialmente los tipos internos que nunca están destinados a tal cosa y nunca se dan para uso público. Todos estos tipos tienen sus interfaces innecesariamente contaminadas.GetType
,Finalize
oGetHashCode
para cadaSystem.Object
. Simplemente dije que estaban allí si los querías. En cuanto a "sus interfaces están contaminadas innecesariamente", me referiré a mi cita original de elippert: "el mejor valor para el cliente". Obviamente, el equipo .NET estaba dispuesto a lidiar con las compensaciones.Creo que los diseñadores de Java y C # agregaron un objeto raíz porque era esencialmente gratuito para ellos, como en el almuerzo gratis .
Los costos de agregar un objeto raíz son muy similares a los costos de agregar su primera función virtual. Dado que tanto CLR como JVM son entornos de recolección de basura con finalizadores de objetos, debe tener al menos uno virtual
java.lang.Object.finalize
para suSystem.Object.Finalize
método o para él . Por lo tanto, los costos de agregar su objeto raíz ya están pagados por adelantado, puede obtener todos los beneficios sin pagar por ello. Esto es lo mejor de ambos mundos: los usuarios que necesitan una clase raíz común obtendrían lo que desean, y los usuarios a los que no les importaría menos podrían programar como si no estuvieran allí.fuente
Me parece que tiene tres preguntas aquí: una es por qué se introdujo una raíz común en .NET, la segunda es cuáles son las ventajas y desventajas de eso, y la tercera es si es una buena idea que un elemento del marco tenga un valor global. raíz.
¿Por qué .NET tiene una raíz común?
En el sentido más técnico, tener una raíz común es esencial para la reflexión y para los contenedores pre-genéricos.
Además, que yo sepa, tener una raíz común con métodos básicos como
equals()
yhashCode()
fue recibido de manera muy positiva en Java, y C # fue influenciado por (entre otros) Java, por lo que también querían tener esa característica.¿Cuáles son las ventajas y desventajas de tener una raíz común en un idioma?
La ventaja de tener una raíz común:
equals()
yhashCode()
. Eso significa, por ejemplo, que cada objeto en Java o C # se puede usar en un mapa hash: compare esa situación con C ++.printf
método similar.object
. Quizás no sea muy común, pero sin una raíz común no sería posible.La desventaja de tener una raíz común:
¿Deberías ir con una raíz común en tu proyecto de marco?
Muy subjetivo, por supuesto, pero cuando miro la lista pro-con anterior, definitivamente respondería que sí . Específicamente, la última viñeta en la lista profesional, la flexibilidad de cambiar más tarde el comportamiento de todos los objetos al cambiar la raíz, se vuelve muy útil en una plataforma, en la que los clientes tienen más probabilidades de aceptar los cambios en la raíz que los cambios en un todo idioma.
Además, aunque esto es aún más subjetivo, el concepto de raíz común me parece muy elegante y atractivo. También facilita el uso de algunas herramientas, por ejemplo, ahora es fácil pedirle a una herramienta que muestre a todos los descendientes de esa raíz común y recibir una descripción rápida y buena de los tipos del marco.
Por supuesto, los tipos tienen que estar al menos ligeramente relacionados para eso, y en particular, nunca sacrificaría la regla "es-una" solo para tener una raíz común.
fuente
Matemáticamente, es un poco más elegante tener un sistema de tipos que contenga top , y le permite definir un lenguaje un poco más completamente.
Si está creando un marco, las cosas necesariamente serán consumidas por una base de código existente. No puede tener un supertipo universal ya que existen todos los otros tipos en lo que sea que consuma el marco.
En ese momento, la decisión de tener una clase base común depende de lo que esté haciendo. Es muy raro que muchas cosas tengan un comportamiento común y que sea útil hacer referencia a estas cosas solo a través del comportamiento común.
Pero sucede Si es así, entonces sigue adelante abstrayendo ese comportamiento común.
fuente
Se presionó para un objeto raíz porque querían que todas las clases en el marco admitieran ciertas cosas (obtener un código hash, convertir a una cadena, verificaciones de igualdad, etc.). Tanto C # como Java encontraron útil poner todas estas características comunes de un Objeto en alguna clase raíz.
Tenga en cuenta que no están violando ningún principio de OOP ni nada. Cualquier cosa en la clase Object tiene sentido en cualquier subclase (léase: cualquier clase) en el sistema. Si elige adoptar este diseño, asegúrese de seguir este patrón. Es decir, no incluya cosas en la raíz que no pertenezcan a todas las clases de su sistema. Si sigue esta regla cuidadosamente, no veo ninguna razón por la que no deba tener una clase raíz que contenga código útil y común para todo el sistema.
fuente
Un par de razones, funcionalidad común que todas las clases secundarias pueden compartir. Y también les dio a los escritores del marco la capacidad de escribir muchas otras funcionalidades en el marco que todo podría usar. Un ejemplo sería el almacenamiento en caché y las sesiones de ASP.NET. Casi todo se puede almacenar en ellos, porque escribieron los métodos de agregar para aceptar objetos.
Una clase raíz puede ser muy atractiva, pero es muy, muy fácil de usar mal. ¿Sería posible tener una interfaz raíz en su lugar? ¿Y solo tienes uno o dos métodos pequeños? ¿O eso agregaría mucho más código del que se requiere para su marco?
Le pregunto si tengo curiosidad acerca de qué funcionalidad necesita exponer a todas las clases posibles en el marco que está escribiendo. ¿Será realmente utilizado por todos los objetos? ¿O por la mayoría de ellos? Y una vez que cree la clase raíz, ¿cómo evitará que las personas agreguen funcionalidad aleatoria? ¿O las variables aleatorias quieren ser "globales"?
Hace varios años había una aplicación muy grande donde creé una clase raíz. Después de unos meses de desarrollo, estaba poblado por un código que no tenía nada que ver allí. Estábamos convirtiendo una vieja aplicación ASP y la clase raíz era el reemplazo del viejo archivo global.inc que habíamos usado en el pasado. Tuve que aprender esa lección de la manera difícil.
fuente
Todos los objetos de montón independientes heredan de
Object
; eso tiene sentido porque todos los objetos de montón independientes deben tener ciertos aspectos comunes, como un medio para identificar su tipo. De lo contrario, si el recolector de basura tuviera una referencia a un objeto de montón de tipo desconocido, no tendría forma de saber qué bits dentro del blob de memoria asociado con ese objeto deberían considerarse referencias a otros objetos de montón.Además, dentro del sistema de tipos, es conveniente utilizar el mismo mecanismo para definir los miembros de las estructuras y los miembros de las clases. El comportamiento de las ubicaciones de almacenamiento de tipo de valor (variables, parámetros, campos, ranuras de matriz, etc.) es muy diferente del de las ubicaciones de almacenamiento de tipo de clase, pero tales diferencias de comportamiento se logran en los compiladores de código fuente y el motor de ejecución (incluyendo el compilador JIT) en lugar de expresarse en el sistema de tipos.
Una consecuencia de esto es que definir un tipo de valor define efectivamente dos tipos: un tipo de ubicación de almacenamiento y un tipo de objeto de almacenamiento dinámico. El primero se puede convertir implícitamente en el segundo, y el último se puede convertir en el primero mediante la conversión de texto. Ambos tipos de conversión funcionan copiando todos los campos públicos y privados de una instancia del tipo en cuestión a otra. Además, es posible usar restricciones genéricas para invocar a los miembros de la interfaz en una ubicación de almacenamiento de tipo de valor directamente, sin hacer una copia de ella primero.
Todo esto es importante porque las referencias a objetos de montón de tipo de valor se comportan como referencias de clase y no como tipos de valor. Considere, por ejemplo, el siguiente código:
Si
testEnumerator()
se pasa el método a una ubicación de almacenamiento de tipo de valor,it
recibirá una instancia cuyos campos públicos y privados se copian del valor pasado. La variable localit2
contendrá otra instancia cuyos campos se copian todosit
. LlamandoMoveNext
ait2
no afectaráit
.Si el código anterior se pasa a una ubicación de almacenamiento de tipo de clase, entonces el valor pasado
it
, yit2
, todos se referirán al mismo objeto y, por lo tanto, llamarMoveNext()
a cualquiera de ellos lo llamará efectivamente a todos.Tenga en cuenta que la conversión
List<String>.Enumerator
aIEnumerator<String>
efectivamente lo convierte de un tipo de valor a un tipo de clase. El tipo del objeto de montón esList<String>.Enumerator
pero su comportamiento será muy diferente del tipo de valor del mismo nombre.fuente
List<T>.Enumerator
que se almacena como aList<T>.Enumerator
es significativamente diferente del comportamiento de uno que se almacena en unIEnumerator<T>
, ya que almacenar un valor del tipo anterior en una ubicación de almacenamiento de cualquier tipo hará una copia del estado del enumerador, mientras se almacena un valor del último tipo en una ubicación de almacenamiento que también es del último tipo, no se realizará una copia.Object
tiene nada que ver con ese hecho.System.Int32
también es una instancia deSystem.Object
, pero una ubicación de almacenamiento de tipoSystem.Int32
no contiene ni unaSystem.Object
instancia ni una referencia a una.Este diseño realmente se remonta a Smalltalk, que vería en gran medida como un intento de perseguir la orientación a objetos a expensas de casi cualquier otra inquietud. Como tal, tiende (en mi opinión) a usar la orientación a objetos, incluso cuando otras técnicas son probablemente (o incluso ciertamente) superiores.
Tener una sola jerarquía con
Object
(o algo similar) en la raíz hace que sea bastante fácil (por ejemplo) crear sus clases de colección como coleccionesObject
, por lo que es trivial que una colección contenga cualquier tipo de objeto.Sin embargo, a cambio de esta pequeña ventaja, obtienes una gran cantidad de desventajas. Primero, desde el punto de vista del diseño, terminas con algunas ideas verdaderamente locas. Al menos según la visión Java del universo, ¿qué tienen en común el ateísmo y un bosque? ¡Que ambos tienen códigos hash! ¿Es un mapa una colección? Según Java, no, ¡no lo es!
En los años 70, cuando se estaba diseñando Smalltalk, este tipo de tonterías fueron aceptadas, principalmente porque nadie había diseñado una alternativa razonable. Sin embargo, Smalltalk se finalizó en 1980 y en 1983 se diseñó Ada (que incluye genéricos). Aunque Ada nunca alcanzó el tipo de popularidad que algunos predijeron, sus genéricos fueron suficientes para soportar colecciones de objetos de tipos arbitrarios, sin la locura inherente a las jerarquías monolíticas.
Cuando se diseñó Java (y, en menor medida, .NET), la jerarquía de clases monolítica probablemente se vio como una opción "segura", una con problemas, pero en su mayoría problemas conocidos . La programación genérica, por el contrario, era una que casi todo el mundo (incluso entonces) se dio cuenta de que era al menos teóricamente un enfoque mucho mejor para el problema, pero que muchos desarrolladores con orientación comercial consideraron poco explorado y / o riesgoso (es decir, en el mundo comercial). Ada fue despedida en gran medida como un fracaso).
Sin embargo, déjenme ser claro: la jerarquía monolítica fue un error. Las razones de ese error eran al menos comprensibles, pero de todos modos fue un error. Es un mal diseño, y sus problemas de diseño impregnan casi todo el código que lo usa.
Para un nuevo diseño hoy, sin embargo, no hay una pregunta razonable: usar una jerarquía monolítica es un claro error y una mala idea.
fuente
Lo que quieres hacer es realmente interesante, es difícil probar si está bien o mal hasta que hayas terminado.
Aquí hay algunas cosas para pensar:
Esas son las dos únicas cosas que pueden beneficiarse de una raíz compartida. En un lenguaje se usa para definir algunos métodos muy comunes y le permite pasar objetos que aún no se han definido, pero que no deberían aplicarse a usted.
Además, por experiencia personal:
Utilicé un kit de herramientas una vez escrito por un desarrollador cuyo fondo era Smalltalk. Lo hizo para que todas sus clases de "Datos" extendieran una sola clase y todos los métodos tomaran esa clase, hasta ahora muy bien. El problema es que las diferentes clases de datos no siempre eran intercambiables, por lo que ahora mi editor y compilador no podían darme NINGUNA ayuda sobre qué pasar en una situación dada y tuve que referirme a sus documentos que no siempre ayudaron. .
Esa fue la biblioteca más difícil de usar con la que he tenido que lidiar.
fuente
Porque ese es el camino de la OOP. Es importante tener un tipo (objeto) que pueda referirse a cualquier cosa. Un argumento de tipo objeto puede aceptar cualquier cosa. También hay herencia, puede estar seguro de que ToString (), GetHashCode (), etc. están disponibles en cualquier cosa y en todo.
Incluso Oracle se ha dado cuenta de la importancia de tener ese tipo de base y planea eliminar primitivas en Java cerca de 2017
fuente
ToString()
y estarásGetType()
en cada objeto. Probablemente valga la pena señalar que puede usar una variable de tipoobject
para almacenar cualquier objeto .NET.