Resumen vs. Interfaz: definición de separación e implementación en Delphi
oop interface (6)
Dudo que se trate de un "mejor enfoque", simplemente tienen diferentes casos de uso .
Si no tienes una jerarquía de clases , y no quieres construir una, y ni siquiera tiene sentido forzar clases no relacionadas en la misma jerarquía, pero quieres tratar algunas clases iguales de todos modos sin tener que saber el nombre específico de la clase ->
Las interfaces son el camino a seguir (piense en Javas Comparable o Iterateable, por ejemplo, si tuviera que derivar de esas clases (siempre que fueran clases =), serían totalmente inútiles.
- Si tiene una jerarquía de clases razonable , puede usar clases abstractas para proporcionar un punto de acceso uniforme a todas las clases de esta jerarquía, con el beneficio de que incluso puede implementar un comportamiento predeterminado y demás.
¿Cuál es el mejor enfoque para separar la definición y la implementación, utilizando interfaces o clases abstractas?
De hecho, no me gusta mezclar objetos contados de referencia con otros objetos. Me imagino que esto puede convertirse en una pesadilla al mantener grandes proyectos.
Pero a veces necesitaría derivar una clase de 2 o más clases / interfaces.
¿Cuál es tu experiencia?
En Delphi hay tres formas de separar la definición de la implementación.
Usted tiene una separación en cada unidad donde puede colocar las clases publicas en la sección de interfaz y su implementación en la sección de implementación. El código aún reside en la misma unidad, pero al menos un "usuario" de su código solo necesita leer la interfaz y no las agallas de la implementación.
Al usar funciones declaradas virtual o dinámicamente en su clase, puede anular aquellas en subclases. Esta es la forma de uso de la mayoría de las bibliotecas de clases. Mire TStream y sus clases derivadas como THandleStream, TFileStream, etc.
Puede usar interfaces cuando necesite una jerarquía diferente que solo la derivación de clase. Las interfaces siempre se derivan de IInterface que se modela a un IUnknown basado en COM: se obtiene el recuento de referencias y la información del tipo de consulta junto con él.
Para 3: - Si deriva de TInterfacedObject, el recuento de referencias se ocupa de la vida útil de sus objetos, pero esto no es cierto. - TComponent por ejemplo también implementa la interfaz II pero SIN recuento de referencia. Esto viene con una GRAN advertencia: asegúrese de que las referencias de su interfaz estén configuradas en cero antes de destruir su objeto. El comiler todavía insertará llamadas decref a su interfaz que parece válida pero no lo es. Segundo: la gente no esperará este comportamiento.
Elegir entre 2 y 3 a veces es bastante subjetivo. Tiendo a usar lo siguiente:
- Si es posible, use virtual y dinámico y anule aquellos en las clases derivadas.
- Al trabajar con interfaces: cree una clase base que acepte la referencia a la instancia de interfase como variable y mantenga sus interfaces lo más simples posible; para cada aspecto intente crear una variable intercae separada. Intente implementar una implementación predeterminada cuando no se especifique ninguna interfaz.
- Si lo anterior es demasiado limitante: comience a usar TInterfacedObject-s y realmente cuide los posibles ciclos y por lo tanto las fugas de memoria.
En mi experiencia con proyectos extremadamente grandes, ambos modelos no solo funcionan bien, incluso pueden coexistir sin ningún problema. Las interfaces tienen la ventaja sobre la herencia de clase ya que puede agregar una interfaz específica a múltiples clases que no descienden de un antecesor común, o al menos sin introducir código tan atrás en la jerarquía que corre el riesgo de introducir nuevos errores en el código que ya se ha demostrado que funciona.
La clave para entender esto es darse cuenta de que se trata de algo más que solo definición vs. implementación . Se trata de diferentes formas de describir el mismo sustantivo:
- La herencia de clase responde a la pregunta: "¿Qué clase de objeto es este?"
- La implementación de la interfaz responde a la pregunta: "¿Qué puedo hacer con este objeto?"
Digamos que estás modelando una cocina. (Disculpas de antemano por las siguientes analogías de comida, acabo de regresar del almuerzo ...) Tienes tres tipos básicos de utensilios: tenedores, cuchillos y cucharas. Todos estos encajan en la categoría de utensilio , así que modelaremos eso (estoy omitiendo algunas cosas aburridas como los campos de respaldo):
type
TMaterial = (mtPlastic, mtSteel, mtSilver);
TUtensil = class
public
function GetWeight : Integer; virtual; abstract;
procedure Wash; virtual; // Yes, it''s self-cleaning
published
property Material : TMaterial read FMaterial write FMaterial;
end;
Todo esto describe datos y funcionalidades comunes a cualquier utensilio: de qué está hecho, qué pesa (que depende del tipo concreto), etc. Pero notará que la clase abstracta realmente no hace nada. Un TFork
y TKnife
realmente no tienen mucho más en común que usted podría poner en la clase base. Técnicamente puede Cut
con un TFork
, pero un TSpoon
puede ser un estiramiento, entonces, ¿cómo reflejar el hecho de que solo algunos utensilios pueden hacer ciertas cosas?
Bueno, podemos comenzar a extender la jerarquía, pero se vuelve desordenado:
type
TSharpUtensil = class
public
procedure Cut(food : TFood); virtual; abstract;
end;
Eso se ocupa de los más agudos, pero ¿y si queremos agruparnos de esta manera?
type
TLiftingUtensil = class
public
procedure Lift(food : TFood); virtual; abstract;
end;
TFork
y TKnife
encajarían bajo TSharpUtensil
, pero TKnife
es bastante pésimo para levantar un trozo de pollo. Terminamos teniendo que elegir una de estas jerarquías, o simplemente colocamos toda esta funcionalidad en el TUtensil
general y las clases derivadas simplemente nos negamos a implementar los métodos que no tienen sentido. En cuanto al diseño, no es una situación en la que queremos encontrarnos atrapados.
Por supuesto, el verdadero problema con esto es que estamos usando la herencia para describir lo que hace un objeto, no lo que es . Para el primero, tenemos interfaces. Podemos limpiar este diseño mucho:
type
IPointy = interface
procedure Pierce(food : TFood);
end;
IScoop = interface
procedure Scoop(food : TFood);
end;
Ahora podemos clasificar lo que hacen los tipos de concreto:
type
TFork = class(TUtensil, IPointy, IScoop)
...
end;
TKnife = class(TUtensil, IPointy)
...
end;
TSpoon = class(TUtensil, IScoop)
...
end;
TSkewer = class(TStick, IPointy)
...
end;
TShovel = class(TGardenTool, IScoop)
...
end;
Creo que todos entienden la idea. El punto (sin juego de palabras) es que tenemos un control muy detallado sobre todo el proceso, y no tenemos que hacer concesiones. Aquí usamos tanto la herencia como las interfaces, las opciones no son mutuamente excluyentes, es solo que solo incluimos la funcionalidad en la clase abstracta que es realmente, realmente común a todos los tipos derivados.
Si elige o no utilizar la clase abstracta o una o más de las interfaces en sentido descendente realmente depende de lo que necesite hacer con ella:
type
TDishwasher = class
procedure Wash(utensils : Array of TUtensil);
end;
Esto tiene sentido, porque solo los utensilios van al lavaplatos, al menos en nuestra cocina muy limitada que no incluye lujos tales como platos o tazas. TSkewer
y TShovel
probablemente no entren allí, aunque técnicamente pueden participar en el proceso de comer.
Por otra parte:
type
THungryMan = class
procedure EatChicken(food : TFood; utensil : TUtensil);
end;
Esto podría no ser tan bueno. No puede comer solo con un TKnife
(bueno, no fácilmente). Y requerir tanto un TFork
como TKnife
tampoco tiene sentido; ¿y si es una ala de pollo?
Esto tiene mucho más sentido:
type
THungryMan = class
procedure EatPudding(food : TFood; scoop : IScoop);
end;
Ahora podemos darle el TFork
, el TSpoon
o el TShovel
, y él está feliz, pero no con el TKnife
, que todavía es un utensilio pero que no ayuda mucho aquí.
También notará que la segunda versión es menos sensible a los cambios en la jerarquía de clases. Si decidimos cambiar TFork
para heredar de TWeapon
, nuestro hombre sigue contento mientras implemente IScoop
.
También he pasado por alto el problema del recuento de referencias aquí, y creo que @Deltics lo dijo mejor; Solo porque tengas AddRef
no significa que necesites hacer lo mismo que TInterfacedObject
. El recuento de referencias de la interfaz es una especie de característica secundaria, es una herramienta útil para los momentos en que lo necesita, pero si va a mezclar la interfaz con la semántica de clase (y muy a menudo lo hace), no siempre lo hace sentido para usar la función de contar referencias como una forma de gestión de la memoria.
De hecho, iría tan lejos como para decir que la mayoría de las veces, probablemente no desee la semántica de conteo de referencias . Sí, allí, lo dije. Siempre sentí que todo el conteo de ref era solo para ayudar a soportar la automatización OLE y tal ( IDispatch
). A menos que tenga una buena razón para querer la destrucción automática de su interfaz, simplemente olvídese, no use TInterfacedObject
en absoluto. Siempre puedes cambiarlo cuando lo necesites, ¡ese es el punto de usar una interfaz! Piense en las interfaces desde un punto de vista de diseño de alto nivel, no desde la perspectiva de la administración de memoria / vida útil.
Entonces la moraleja de la historia es:
Cuando necesite un objeto para admitir alguna funcionalidad particular , intente utilizar una interfaz.
Cuando los objetos pertenecen a la misma familia y desea que compartan características comunes , herede de una clase base común.
Y si ambas situaciones se aplican, ¡entonces usa ambas!
No me gustan las Interfaces COM hasta el punto de nunca usarlas, excepto cuando alguien más ha producido una. Tal vez esto vino de mi desconfianza de las cosas de COM y Type Library. Incluso he "simulado" interfaces como clases con complementos de devolución de llamada en lugar de utilizar interfaces. Me pregunto si alguien más ha sentido mi dolor y ha evitado el uso de interfaces como si fueran una plaga.
Sé que algunas personas considerarán que mi manera de evitar las interfaces es una debilidad. Pero creo que todo el código Delphi que usa Interfaces tiene una especie de "olor a código".
Me gusta usar delegados y cualquier otro mecanismo que pueda, para separar mi código en secciones, y tratar de hacer todo lo que pueda con las clases, y nunca usar interfaces. No digo que eso sea bueno, solo digo que tengo mis razones, y tengo una regla (que a veces puede ser incorrecta, y para algunas personas siempre es incorrecta): evito las interfaces.
Puede tener interfaces sin contar referencias. El compilador agrega llamadas a AddRef y Release para todas las interfaces, pero el aspecto de administración de por vida de esos objetos se debe exclusivamente a la implementación de IUnknown.
Si deriva de TInterfacedObject, la duración del objeto se contará como referencia, pero si deriva su propia clase de TObject e implementa IUnknown sin contar realmente las referencias y sin liberar "self" en la implementación de Release, obtendrá una clase base que admite interfaces pero tiene una vida administrada explícitamente como normal.
Todavía debe tener cuidado con esas referencias de interfaz debido a las llamadas generadas automáticamente a AddRef () y Release () inyectadas por el compilador, pero esto realmente no es muy diferente de tener cuidado con "referencias colgantes" a TObject regular.
Es algo que he utilizado con éxito en proyectos grandes y sofisticados en el pasado, incluso mezclando objetos contados por ref y no contados para soportar interfaces.