Подтвердить что ты не робот

Модель дизайна декоратора против наследования?

Я прочитал шаблон дизайна декоратора из wiki http://en.wikipedia.org/wiki/Decorator_pattern и пример кода из http://www.vincehuston.org/dp/decorator.html.

Я вижу, что традиционное наследование следует за шаблоном "is-a", тогда как декоратор следует за шаблоном "has-a". И вызывающая конвенция декоратора выглядит как "скин" поверх "кожи".. над "ядром". например.

I* anXYZ = new Z( new Y( new X( new A ) ) );

как показано в приведенной выше ссылке на пример кода.

Однако есть еще несколько вопросов, которые я не понимаю:

  • Что означает wiki: "Шаблон декоратора может использоваться для расширения (украшения) функциональности определенного объекта во время выполнения?" new... (new... (new...)) "- это вызов во время выполнения, и он хорош, но" AwiспасибоYZ anXYZ;" является наследованием во время компиляции и является плохим?

  • из ссылки на пример кода я вижу, что число определения класса почти одинаковое в обеих реализациях. Я вспоминаю в некоторых других книгах по дизайну, таких как "Head first design patterns". В качестве примера они используют кофе Starbuzz и говорят, что традиционное наследование приведет к "взрыву класса" , потому что для каждой комбинации кофе вы получите класс для этого. Но разве это не то же самое для декоратора в этом случае? Если класс декоратора может взять ЛЮБЫЙ абстрактный класс и украсить его, то я предполагаю, что он предотвращает взрыв, но из примера кода у вас есть точное определение классов, не менее...

Может ли кто-нибудь помочь объяснить?

Большое спасибо,

4b9b3361

Ответ 1

Возьмем некоторые абстрактные потоки, например, и представьте, что вы хотите предоставлять им службы шифрования и сжатия.

С декоратором у вас есть (псевдокод):

Stream plain = Stream();
Stream encrypted = EncryptedStream(Stream());
Stream zipped = ZippedStream(Stream());
Stream zippedEncrypted = ZippedStream(EncryptedStream(Stream());
Stream encryptedZipped = EncryptedStream(ZippedStream(Stream());

С наследованием у вас есть:

class Stream() {...}
class EncryptedStream() : Stream {...}
class ZippedStream() : Stream {...}
class ZippedEncryptedStream() : EncryptedStream {...}
class EncryptedZippedStream() : ZippedStream {...}

1) с декоратором, вы комбинируете функциональность во время выполнения, в зависимости от ваших потребностей. Каждый класс только заботится об одном аспекте функциональности (сжатие, шифрование,...)

2) в этом простом примере мы имеем 3 класса с декораторами и 5 с наследованием. Теперь добавьте еще несколько сервисов, например. фильтрации и отсечения. С декоратором вам нужно всего еще 2 класса для поддержки всех возможных сценариев, например. filtering → clipping → compression → encription. С наследованием вам нужно предоставить класс для каждой комбинации, чтобы вы получили десятки классов.

Ответ 2

В обратном порядке:

2) С помощью, например, 10 различных независимых расширений, любая комбинация которых может понадобиться во время выполнения, 10 классов декоратора будут выполнять эту работу. Чтобы охватить все возможности по наследованию, вам понадобится 1024 подкласса. И не будет возможности обойти массивную избыточность кода.

1) Представьте, что вы использовали эти 1024 подкласса во время выполнения. Попробуйте набросать код, который потребуется. Имейте в виду, что вы не сможете диктовать порядок, в котором выбираются или отклоняются варианты. Также помните, что вам, возможно, придется использовать экземпляр некоторое время, прежде чем расширять его. Давай, попробуй. Выполнение этого с декораторами тривиально по сравнению.

Ответ 3

Вы правы, что иногда они могут быть очень похожи. Применимость и преимущества любого решения будут зависеть от вашей ситуации.

Другие избили меня адекватными ответами на ваш второй вопрос. Короче говоря, вы можете комбинировать декораторов для достижения большего количества комбинаций, которые вы не можете сделать с наследованием.

Как таковой, я фокусируюсь на первом:

Вы не можете строго сказать, что время компиляции плохое, а время выполнения - хорошее, это просто отличная гибкость. Возможность изменять вещи во время выполнения может быть важна для некоторых проектов, поскольку она позволяет изменять без перекомпиляции, которая может быть медленной, и требует, чтобы вы находились в среде, в которой вы можете скомпилировать.

Примером, где вы не можете использовать наследование, является то, когда вы хотите добавить функциональность к экземпляру объекта. Предположим, вам предоставлен экземпляр объекта, который реализует интерфейс ведения журнала:

public interface ILog{
    //Writes string to log
    public void Write( string message );
}

Теперь предположим, что вы начинаете сложную задачу, которая включает в себя множество объектов, и каждый из них выполняет запись, поэтому вы проходите вдоль объекта ведения журнала. Однако вы хотите, чтобы каждое сообщение из задачи начиналось с имени задачи и идентификатора задачи. Вы можете передать функцию или передать имя и идентификатор и доверять каждому вызывающему абоненту, чтобы следовать правилу предварительной обработки этой информации, или вы могли бы украсить объект регистрации, прежде чем передавать его, и не беспокоиться о других объектах, делающих это правильно

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 );
     }
}

Извините за код С#, но я думаю, что он все равно передаст идеи кому-то, кто знает С++

Ответ 4

Чтобы решить вторую часть вашего вопроса (которая может в свою очередь адресовать вашу первую часть), используя метод декоратора, вы получаете доступ к одному и тому же количеству комбинаций, но не должны их записывать. Если у вас есть 3 слоя декораторов с 5 вариантами на каждом уровне, у вас есть 5*5*5 возможные классы для определения с использованием наследования. Используя метод декоратора, вам нужно 15.

Ответ 5

Во-первых, я человек С# и не занимаюсь С++ в то время, но надеюсь, что вы получите, откуда я.

Хорошим примером, который приходит на ум, является DbRepository и 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();
    }
}

Итак, если бы я просто использовал наследование, у меня были бы DbRepository и CachingDbRepository. DbRepository будет запрашивать базу данных; CachingDbRepository проверит его кеш, и если данных не будет, он будет запрашивать базу данных. Таким образом, здесь возможно дублирование реализации.

Используя шаблон декоратора, у меня все еще есть такое же количество классов, но мой CachingDbRepository занимает IRepository и вызывает его GetStuff(), чтобы получить данные из базового репо, если он не находится в кеше.

Таким образом, количество классов одинаково, но использование классов связано. CachingDbRepo вызывает Repo, который был передан в него... так что это больше похоже на композицию над наследованием.

Я нахожу это субъективным, когда нужно решать, когда использовать только наследование над украшением.

Надеюсь, это поможет. Удачи!