c++ - estructural - patrones de diseño
¿Patrón de diseño decorador vs. herencia? (5)
En orden inverso:
2) Con, digamos, 10 extensiones independientes diferentes, cualquier combinación de las cuales podría ser necesaria en el tiempo de ejecución, 10 clases de decoradores harán el trabajo. Para cubrir todas las posibilidades por herencia, se necesitarían 1024 subclases. Y no habría manera de sortear la redundancia masiva de código.
1) Imagina que tienes esas 1024 subclases para elegir en el tiempo de ejecución. Trate de esbozar el código que se necesitaría. Tenga en cuenta que es posible que no pueda dictar el orden en que se seleccionan o rechazan las opciones. También recuerde que es posible que tenga que usar una instancia por un tiempo antes de extenderla. Adelante, inténtalo. Hacerlo con decoradores es trivial en comparación.
He leído el patrón de diseño de decorador de Wikipedia y el ejemplo de código de este sitio .
Veo el punto de que la herencia tradicional sigue un patrón ''is-a'' mientras que el decorador sigue un patrón ''has-a''. Y la convención de decoración del decorador parece una ''piel'' sobre ''piel'' ... sobre ''núcleo''. p.ej
I* anXYZ = new Z( new Y( new X( new A ) ) );
como se muestra en el enlace del ejemplo del código anterior.
Sin embargo, todavía hay un par de preguntas que no entiendo:
¿Qué quiere decir wiki con ''El patrón decorador se puede usar para extender (decorar) la funcionalidad de un determinado objeto en tiempo de ejecución ''? el ''nuevo ... (nuevo ... (nuevo ...))'' es una llamada en tiempo de ejecución y es bueno pero un ''AwithXYZ anXYZ;'' ¿Es una herencia en tiempo de compilación y es mala?
En el enlace de ejemplo de código puedo ver que el número de definición de clase es casi el mismo en ambas implementaciones. Recuerdo en otros libros de patrones de diseño como ''Head first design patterns''. Utilizan el café Starbuzz como ejemplo y dicen que la herencia tradicional causará una "explosión de clase" porque, para cada combinación de café, se le ocurrirá una clase.
¿Pero no es lo mismo para el decorador en este caso? Si una clase decoradora puede tomar CUALQUIER clase abstracta y decorarla, supongo que previene la explosión, pero del ejemplo de código, tiene el número exacto de definiciones de clase, no menos ...
¿Alguien lo explicaría?
En primer lugar, soy una persona C # y no he tratado con C ++ en un tiempo, pero espero que llegues a donde vengo.
Un buen ejemplo que viene a la mente es un DbRepository
y un CachingDbRepository
:
public interface IRepository {
object GetStuff();
}
public class DbRepository : IRepository {
public object GetStuff() {
//do something against the database
}
}
public class CachingDbRepository : IRepository {
public CachingDbRepository(IRepository repo){ }
public object GetStuff() {
//check the cache first
if(its_not_there) {
repo.GetStuff();
}
}
Entonces, si solo usara la herencia, tendría un DbRepository
y un CachingDbRepository
. El DbRepository
consultaría desde una base de datos; CachingDbRepository
verificará su caché y si los datos no estuvieran allí, consultaría una base de datos. Así que hay una posible implementación duplicada aquí.
Al usar el patrón decorador, todavía tengo el mismo número de clases, pero mi CachingDbRepository
toma un IRepository
y llama a su GetStuff()
para obtener los datos del repositorio subyacente si no está en el caché.
Entonces, el número de clases es el mismo, pero el uso de las clases está relacionado. CachingDbRepo
llama al Repo que se le pasó ... así que es más como una composición sobre una herencia.
Me parece subjetivo cuándo decidir cuándo usar solo la herencia sobre la decoración.
Espero que esto ayude. ¡Buena suerte!
Para abordar la segunda parte de su pregunta (que a su vez podría abordar su primera parte), utilizando el método de decoración, tiene acceso al mismo número de combinaciones, pero no tiene que escribirlas . Si tiene 3 capas de decoradores con 5 opciones en cada nivel, tiene 5*5*5
clases posibles para definir el uso de la herencia. Usando el método decorador necesitas 15.
Tienes razón en que pueden ser muy similares a veces. La aplicabilidad y los beneficios de cualquiera de las soluciones dependerán de su situación.
Otros me han superado con respuestas adecuadas a tu segunda pregunta. En resumen, es que puedes combinar decoradores para lograr más combinaciones que no puedes hacer con la herencia.
Como tal me enfoco en el primero:
No se puede decir estrictamente que el tiempo de compilación es malo y el tiempo de ejecución es bueno, es simplemente una flexibilidad diferente. La capacidad de cambiar las cosas en tiempo de ejecución puede ser importante para algunos proyectos porque permite cambios sin recompilación, lo que puede ser lento y requiere que esté en un entorno donde pueda compilar.
Un ejemplo en el que no puede usar la herencia es cuando desea agregar funcionalidad a un objeto instanciado. Supongamos que se le proporciona una instancia de un objeto que implementa una interfaz de registro:
public interface ILog{
//Writes string to log
public void Write( string message );
}
Ahora suponga que comienza una tarea complicada que involucra muchos objetos y que cada uno de ellos realiza el registro de manera que pase el objeto de registro. Sin embargo, usted desea que todos los mensajes de la tarea tengan un prefijo con el nombre de la tarea y el ID de la tarea. Puede pasar por una función, o pasar el Nombre y la Id. Y confiar en que cada persona que llama siga la regla de pre-pendiente de esa información, o puede decorar el objeto de registro antes de pasarlo sin tener que preocuparse por los otros objetos. bien
public class PrependLogDecorator : ILog{
ILog decorated;
public PrependLogDecorator( ILog toDecorate, string messagePrefix ){
this.decorated = toDecorate;
this.prefix = messagePrefix;
}
public void Write( string message ){
decorated.Write( prefix + message );
}
}
Lo siento por el código C #, pero creo que todavía comunicará las ideas a alguien que sepa C ++
Tomemos algunos flujos abstractos, por ejemplo, e imaginemos que desea proporcionar servicios de cifrado y compresión sobre ellos.
Con decorador tienes (pseudo código):
Stream plain = Stream();
Stream encrypted = EncryptedStream(Stream());
Stream zipped = ZippedStream(Stream());
Stream zippedEncrypted = ZippedStream(EncryptedStream(Stream());
Stream encryptedZipped = EncryptedStream(ZippedStream(Stream());
Con la herencia, usted tiene:
class Stream() {...}
class EncryptedStream() : Stream {...}
class ZippedStream() : Stream {...}
class ZippedEncryptedStream() : EncryptedStream {...}
class EncryptedZippedStream() : ZippedStream {...}
1) con decorador, usted combina la funcionalidad en tiempo de ejecución, dependiendo de sus necesidades. Cada clase solo se ocupa de una faceta de la funcionalidad (compresión, cifrado, ...)
2) en este ejemplo simple, tenemos 3 clases con decoradores y 5 con herencia. Ahora agreguemos algunos servicios más, por ejemplo, filtrado y recorte. Con decorator solo necesita 2 clases más para admitir todos los escenarios posibles, por ejemplo, filtrado -> recorte -> compresión -> inscripción. Con la herencia, debe proporcionar una clase para cada combinación para que termine con decenas de clases.