En la programación orientada a objetos , el reenvío significa que el uso de un miembro de un objeto (ya sea una propiedad o un método ) da como resultado el uso del miembro correspondiente de un objeto diferente: el uso se reenvía a otro objeto. El reenvío se utiliza en varios patrones de diseño , donde algunos miembros se reenvían a otro objeto, mientras que otros son manejados por el objeto utilizado directamente. El objeto de reenvío se denomina frecuentemente objeto contenedor y los miembros de reenvío explícitos se denominan funciones contenedoras .
A menudo se confunde el reenvío con la delegación ; formalmente, son conceptos complementarios. En ambos casos, hay dos objetos, y el primer objeto (envío, contenedor) utiliza el segundo objeto (recepción, envoltura), por ejemplo, para llamar a un método. Se diferencian en lo que self
se refiere al objeto receptor (formalmente, en el entorno de evaluación del método sobre el objeto receptor): en delegación se refiere al objeto emisor, mientras que en reenvío se refiere al objeto receptor. Tenga en cuenta que self
a menudo se usa implícitamente como parte del envío dinámico (resolución de método: a qué función se refiere el nombre de un método).
La diferencia entre reenvío y delegación es la vinculación del parámetro self en el wrappee cuando se llama a través del contenedor. Con la delegación, el parámetro self está vinculado al contenedor, con el reenvío está vinculado al wrappee. ... El reenvío es una forma de reenvío automático de mensajes; La delegación es una forma de herencia con vinculación del padre (superclase) en tiempo de ejecución, en lugar de en tiempo de compilación/enlace como ocurre con la herencia "normal". [1]
Por ejemplo, dado el siguiente código:
// Remitente void n () { print ( "n1" ); } // Receptor void m () { print ( "m2, " ); norte (); } vacío n () { imprimir ( "n2" ); }
bajo delegación esto generarám2,n1porque n()
se evalúa en el contexto del objeto original (de envío), mientras que en el reenvío esto generarám2,n2porque n()
se evalúa en el contexto del objeto receptor. [1]
En el uso informal, el reenvío a menudo se denomina "delegación" o se considera una forma de delegación, pero en un uso cuidadoso se distinguen claramente por lo que se self
refiere. Mientras que la delegación es análoga a la herencia , permitiendo la reutilización del comportamiento (y concretamente la reutilización del código ) sin cambiar el contexto de evaluación, el reenvío es análogo a la composición , ya que la ejecución depende sólo del objeto receptor (miembro), no del objeto emisor (original). En ambos casos, la reutilización es dinámica, es decir, determinada en tiempo de ejecución (en función del objeto al que se delega o reenvía el uso), en lugar de estática, es decir, determinada en tiempo de compilación/enlace (en función de la clase de la que se hereda). Al igual que la herencia, la delegación permite que el objeto emisor modifique el comportamiento original, pero es susceptible a problemas análogos a los de la clase base frágil ; mientras que el reenvío proporciona una encapsulación más fuerte y evita estos problemas; ver composición sobre herencia . [1]
Un ejemplo simple de reenvío explícito en Java: una instancia de B
llamadas de reenvío al foo
método de su a
campo:
clase B { A una ; T foo () { devolver a . foo (); } }
Tenga en cuenta que al ejecutar a.foo()
, el this
objeto es a
(un subtipo de A
), no el objeto original (una instancia de B
). Además, a
no es necesario que sea una instancia de A
: puede ser una instancia de un subtipo. De hecho, A
ni siquiera es necesario que sea una clase: puede ser una interfaz/ protocolo .
En contraste con la herencia, en la que foo
se define en una superclase A
(que debe ser una clase, no una interfaz), y cuando se llama a una instancia de una subclase B
, utiliza el código definido en A
, pero el this
objeto sigue siendo una instancia de B
:
clase A { T foo () { /* ... */ }; } la clase B extiende A { }
En este ejemplo de Python, la clase B
reenvía el foo
método y la x
propiedad al objeto en su a
campo: usarlos en b
(una instancia de B
) es lo mismo que usarlos en b.a
(la instancia de A
a la que se reenvían).
clase A : def __init__ ( self , x ) -> Ninguno : self . x = x def foo ( auto ): imprimir ( auto . x )clase B : def __init__ ( self , a ) -> Ninguno : self . un = un def foo ( yo ): yo . a . foo () @property def x ( self ): devuelve self . a . X @X . definidor def x ( self , x ): self . a . x = x @X . eliminar def x ( self ): del self . a . Xa = A ( 42 ) b = B ( a ) b . foo () # Imprime '42'. b . x # Tiene valor '42' b . x = 17 # bax ahora tiene el valor 17 del b . x # Elimina bax
En este ejemplo de Java , la Printer
clase tiene un print
método. Este método de impresión, en lugar de realizar la impresión en sí, reenvía a un objeto de clase RealPrinter
. Para el mundo exterior parece que el Printer
objeto está haciendo la impresión, pero el RealPrinter
objeto es el que realmente hace el trabajo.
Reenviar es simplemente pasar un deber a alguien/algo más. Aquí hay un ejemplo simple:
class RealPrinter { // el "receptor" void print () { System . afuera . println ( "¡Hola mundo!" ); } } class Impresora { // la RealPrinter "remitente" p = nueva RealPrinter (); // crea el receptor void print () { p . imprimir (); // llama al receptor } } public class Main { public static void main ( String [] argumentos ) { // para el mundo exterior parece que la impresora realmente imprime. Impresora impresora = nueva Impresora (); impresora . imprimir (); } }
El caso más complejo es un Patrón Decorador que mediante el uso de interfaces , el reenvío puede hacerse más flexible y seguro . "Flexibilidad" aquí significa que C
no es necesario hacer referencia a él A
o B
de ninguna manera, ya que se abstrae el cambio de reenvío C
. En este ejemplo, la clase C
puede reenviar a cualquier clase que implemente una interfaz I
. La clase C
tiene un método para cambiar a otro reenviador. Incluir las implements
cláusulas mejora la seguridad de tipos , porque cada clase debe implementar los métodos en la interfaz. La principal desventaja es más código.
interfaz I { void f (); vacío g (); } clase A implementa I { public void f () { System . afuera . println ( "A: haciendo f()" ); } public void g () { Sistema . afuera . println ( "A: haciendo g()" ); } } clase B implementa I { public void f () { System . afuera . println ( "B: haciendo f()" ); } public void g () { Sistema . afuera . println ( "B: haciendo g()" ); } } // cambiando el objeto de implementación en tiempo de ejecución (normalmente hecho en tiempo de compilación) clase C implementa I { I i = null ; // reenvío público C ( I i ){ setI ( i ); } público vacío f () { i . f (); } public void g () { i . gramo (); } // atributos normales public void setI ( I i ) { this . yo = yo ; } } public class Main { public static void main ( String [] argumentos ) { C c = new C ( new A ()); C . f (); // salida: A: haciendo f() c . gramo (); // salida: A: haciendo g() c . establecerI ( nuevo B ()); C . F (); // salida: B: haciendo f() c . gramo (); // salida: B: haciendo g() } }
El reenvío se utiliza en muchos patrones de diseño. [2] El reenvío se utiliza directamente en varios patrones:
El reenvío se puede utilizar en otros patrones, pero a menudo se modifica su uso; por ejemplo, una llamada a un método en un objeto da como resultado que se llamen a varios métodos diferentes en otro: