Шаблоны проектирования: структурные паттерны ч.1




В журнале PC Magazine опубликовали вторую и третью части моей статьи про классические паттерны проектирования. Первую часть я уже опубликовал здесь в виде скана, но этот формат не понравился большинству читателей - с картинок читать неудобно. Так что я решил остальные 2 статьи разделить на 4 части и опубликовать их тут в виде текста.
Напомню, что основная идея статей - это написать про паттерны как можно короче и проще. Статьи можно назвать очень коротким простым справочником. Кому интересны детали - читайте книгу Паттерны проектирования.


В предыдущей статье мы рассмотрели шаблоны из группы «Порождающих паттернов проектирования».
А в этой изучим теорию и практику применения шаблонов из группы «Структурных паттернов».


Напомню, что в этой группе собраны паттерны, которые позволяют менять структуру взаимодействия классов.
«Адаптер» (Adapter) позволяет адаптировать интерфейс класса к конкретной ситуации,
средствами шаблона «Мост» (Bridge) можно отделить интерфейс класса и его реализацию,
«Компоновщик»(Composite) объединяет объекты в древовидную структуру для представления иерархии от частного к целому. Компоновщик позволяет клиентам единообразно обращаться к отдельным объектам и группам объектов.
Паттерн «Оформитель» (Decorator, также известенкак Wrapper, «Оболочка») позволяет динамически добавлять новое поведение к объекту,
«Фасад» (Facade) — скрыть сложность системы путем сведения всех возможных внешних вызовов к одному объекту, делегирующему их соответствующим объектам системы.
Шаблон «Приспособленец» (Flyweight) используется для облегчения работы с большим числом мелких объектов,
а «Заместитель» (Proxy) — контролировать доступ к объекту, перехватывая все вызовы к нему.



В первой части статьи мы рассмотрим первую половину структурных паттернов.


Паттерн Adapter, «Адаптер»

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

class CarCostCalculatorForAllCarTypes
{
  ...
  float getBMW5CarCost(Car* car) const;
  float getToyotaCarCost(Car* car) const;
  float getFordCarCost(Car* car) const;
  float getVolvoCarCost(Car* car) const;
  ...
};



В какой-то момент вы поняли, что эту систему надо рефакторить (переделать) и решили сделать специальную иерархию классов с всего лишь одним виртуальным методом, который будет считать стоимость:

class CostCalculator
{
public:
  virtual ~CostCalculator() {};
  virtual float calculateCost(Car* car) const = 0;
};



Далее вы используете какой-нибудь из порождающих шаблонов проектирования, чтобы порождать конкретный класс CostCalculator из конкретного класса машины, например, для рассчета стоимости BMW5 будет использоваться класс BMW5CostCalculator, порожденный от CostCalculator. Но как написать все эти новые классы? Их можно написать заново, но это время, деньги и новые ошибки.
Тут на помощь приходит паттерн Адаптер. Мы просто вызываем методы из старого класса CarCostCalculatorForAllCarTypes в новых классах. Например, вот весь необходимый код для класса BMW5CostCalculator:

class BMW5CostCalculator : public CostCalculator
{
  CarCostCalculatorForAllCarTypes mCalculator;
public:
  virtual ~BMW5CostCalculator() {};
  virtual float calculateCost(Car* car) const {return mCalculator.getBMW5CarCost(car);}
};



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




Паттерн Bridge, «Мост»


Этот шаблон применяется в том случае, если вы хотите разделить реализацию класса и его интерфейс так, чтобы была возможность изменять их независимо друг от друга. Обычно это значит, что этот шаблон стоит применять всегда, когда вы видите класс, который будет изменяться очень часто. Причем неважно, что будет меняться - интерфейс или реализация.
При этом надо отметить, что при использовании наследования реализация жестко привязывается к абстракции, что затрудняет независимую модификацию, так что обычное наследование не подходит в этом случае.
Основа паттерная Мост - это разнесение интерфейса и реализации класса в разные иерархии классов.
Например, предположим, что наша программа по рассчету стоимости автомобиля стала популярна и ее захотели купить в другой стране. Сразу же выяснилось, что алгоритм рассчета стоимости в другой стране совсем другой. К тому же там появляются совсем другие задачи, например, рассчет налоговых вычетов при покупке авто в зависимости от возраста и другие не менее интересные и важные рассчеты.
И мы, естественно, захотели расширить интерфейс CostCalculator. При этом ясно, что в дальнейшем будет изменяться не только интерфейс, но, безусловно, и реализация.
Можно было бы пойти неверным путем и создавать классы вроде BMW5CostCalculatorRussia и BMW5CostCalculatorFrance, но с ростом числа стран и машин программист просто утонет в ненужной работе.
А значит применим паттерн Мост.

class CostCalculator
{
protected:
  CostCalculatorImpl* mImpl;
public:
  CostCalculator(CostCalculatorImpl* Impl) : mImpl(Impl) {};
  virtual ~CostCalculator() {delete mImpl;}
  virtual float calculateCost(Car* car) const = 0;
  virtual void changeImpl(CostCalculatorImpl* Impl) {delete mImpl; mImpl = Impl;}
};

class CostCalculatorRussia : public CostCalculator
{
public:
  CostCalculatorRussia(CostCalculatorImpl* Impl) : CostCalculator(Impl) {};
  virtual ~CostCalculatorRussia() {}
  virtual float calculateCost(Car* car) const
  {
    return mImpl->calculateCost(car);
  }
};

class CostCalculatorFrance : public CostCalculator
{
public:
  CostCalculatorFrance(CostCalculatorImpl* Impl) : CostCalculator(Impl) {};
  virtual ~CostCalculatorFrance() {}
  virtual float getTaxDeduction(Car* car) const
  {
    float carCost = mImpl->calculateCost(car);
    // предположим что по закону всем жителям, покупающим машину, произведенную во Франции и стоящую дороже 20000 дается 20% скидки на нее.
    if (carCost > 20000 && car->getFactoryCountry() == "France")
      return carCost*0.20f;
    return 0;
  }
  virtual float calculateCost(Car* car) const
  {
    float carCost = mImpl->calculateCost(car);
    return carCost - getTaxDeduction(car);
  }
};



При этом вся иерархия классов CostCalculatorImpl может быть просто перенесена из предыдущего кода без изменений (смотри класс BMW5CostCalculator из предыдущего примера). И мы будем иметь возможность изменять реализацию расчета стоимости автомобиля, не изменяя интерфейс и не трогая алгоритм финального рассчета в разных странах.
Одно из самых интересных применений этого паттерна - это возможность подменять алгоритм “на лету”, прямо во время выполнения программы. Это возможно благодаря тому, что мы должны для этого только пересоздать объект класса-реализации, а не сам класс-интерфейс, который везде используется. Например, мы можем создать один объект класса CostCalculatorFrance, а для рассчета стоимости разных автомобилей будем просто подменять у него имплементацию методом changeImpl.




Паттерн Composite, «Компоновщик»


Этот паттерн позволяет объединить объекты в древовидную структуру для представления иерархии от частного к целому. Он позволяет клиентам обращаться к отдельным объектам и к группам объектов одинаково.
Например, рассмотрим алгоритм рассчета стоимости автомобиля. Пусть стоимость вычисляется, как простая сумма стоимостей всех частей. Но некоторые части составные, например, двигатель состоит из множества деталей и эти детали, в свою очередь, тоже могут быть составными. Как посчитать стоимость в таком случае? Можно применить паттерн Компоновщик.
Для этого создадим базовый класс CarPart:

class CarPart
{
protected:
  std::vector<CarPart*> mParts;
public:
  virtual ~CarPart() {};
  virtual float getCost() const = 0;

  virtual void addPart(CarPart* part)
  {
    mParts.push_back(part);
  };
  virtual void removePart(CarPart* part)
  {
    mParts.erase(std::find(mParts.begin(), mParts.end(), part));
  }

  virtual float calculateFullCost() const
  {
    float summary = getCost();
    for(int i = 0; i < (int)mParts.size(); ++i)
      summary += mParts[i]->calculateFullCost();
    return summary;
  }
};



Итак, мы определили класс, который имеет цену и может быть также контейнером для других подобъектов. Теперь рассчет стоимости автомобиля может быть сделан вызовом одной строки car->calculateFullCost(), если car тоже породить от CarPart и все детали породить от CarPart и добавить их в дерево посредством addPart. Короткий пример, как может выглядеть в этом случае инициализация классов Car и Engine:

void Car::addEngine(Engine* engine)
{
  mEngine = engine;
  addPart(engine);
}

void EngineLada::init()
{
  mSparkPlugs = new SparkPlug[4];

  addPart(&mSparkPlugs[0]);
  addPart(&mSparkPlugs[1]);
  addPart(&mSparkPlugs[2]);
  addPart(&mSparkPlugs[3]);
 ...
}



В итоге получается, что мы всегда можем вычислить стоимость детали любой сложности вызовом всего одного метода calculateFullCost() и неважно, составной это объект или простой.


Паттерн Decorator, «Оформитель»


Этот паттерн предназначен для динамического подключения дополнительного поведения к объекту. Он предоставляет альтернативу созданию подклассов с целью расширения функциональности. Позволяет расширять функциональность объектов без определения подклассов. Известен также под менее распространённым названием Обёртка (Wrapper), которое во многом раскрывает суть реализации шаблона.
Обычно применяется для того, чтобы незначительно изменить поведение объекта или просто для добавления некоторой функциональности к объекту без его изменения.
Например, у нас есть класс Car. И мы хотим добавить возможность вывода в лог информации о вызове каждой функции этого объекта. Мы можем это сделать, изменив код в классе Car, что не всегда возможно и всегда чревато проблемами. Также мы можем породить новый класс от класса Car, перегрузить все функции в нем и добавить лог там. Но это тоже не всегда сработает, так как не все функции могут быть виртуальными. Да и порождать новые объекты не всегда получится. Например, если Car - это базовый класс, от которого порождаются классы вроде BMW5Car и т.п., то надо менять иерархию наследования, что неприемлимо.
Тут нам на помощь приходит паттерн Оформитель. Мы можем создать специальный класс CarDecorator, который будет иметь тот же самый интерфейс, что и Car. А также этот класс будет хранить в себе указатель на объект типа Car, чтобы перенаправлять ему “декорируемые” вызовы. CarDecorator - это базовый класс. Конкретных декораторов может быть сколько угодно и все они должны быть порождены от базового. Например, для декоратора, который записываеть в лог все вызовы функций Car:

class Car
{
...
  float getCost();
  virtual void addEngine(Engine* engine);
...
};

class CarDecorator
{
protected:
  Car* mCar;
public:
  CarDecorator(Car* car) : mCar(car) {};
  void setCar(Car* car) {mCar = car;}
...
  virtual float getCost() {return mCar->getCost();}
  virtual void addEngine(Engine* engine) {mCar->addEngine(engine);}
...
};



Как вы видите, базовый декоратор просто перенаправляет вызовы в объект mCar, который хранит и кажется бесполезным. Но вся суть в классах, порожденных от него.

class CarDecoratorLogger : public CarDecorator
{
...
  virtual float getCost()
  {
    float retValue = CarDecorator::getCost();
    LOG_TO_FILE("getCost for %s returned %d", mCar->getDescription(), retValue);
    return retValue;
  }
  virtual void addEngine(Engine* engine)
  {
    LOG_TO_FILE("addEngine(%s) for %s", engine->getDescription(), mCar->getDescription());
    CarDecorator::addEngine(engine);
  }
...
};



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

class CarDecoratorProgressTracker : public CarDecorator
{
  bool mHaveDoors;
  bool mHaveEngine;
  bool mHaveWheels;
...
  virtual void addEngine(Engine* engine)
  {
    if (engine)
      mHaveEngine = true;
    CarDecorator::addEngine(engine);
  }
  virtual void addDoors(Door* doors, int doorsCount)
  {
    if (doorsCount && doors)
      mHaveDoors = true;
    CarDecorator::addDoors(doors, doorsCount);
  }
...

  virtual bool readyToMove()
  {
    return (mHaveDoors && mHaveEngine && mHaveWheels);
  };
};


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



Следите за обновлениями, скоро будут выложены остальные части статьи.


Понравилась статья? Подпишись на RSS!

Похожие записи:
Шаблоны проектирования: практические примеры. Часть 1.
Потоки и память
Знай свою память
Книги + программисты = деньги
Не будите спящего программиста
2 признака кода с душком: убей его и лови всё молча


Рекомендую книги по этой теме:

3 комментариев к Шаблоны проектирования: структурные паттерны ч.1

Ответить

 

 

 

Вы можете использовать эти HTML тэги

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>