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

Продолжение статьи про структурные паттерны проектирования.
В этой части я расскажу про паттерны:
«Фасад» (Facade) — позволяет скрыть сложность системы путем сведения всех возможных внешних вызовов к одному объекту, делегирующему их соответствующим объектам системы.
Шаблон «Приспособленец» (Flyweight) - используется для облегчения работы с большим числом мелких объектов.
«Заместитель» (Proxy) — позволяет контролировать доступ к объекту, перехватывая все вызовы к нему.




Паттерн Facade, «Фасад»

Этот шаблон проектирования позволяет скрыть сложность системы путем направления всех возможных внешних вызовов через один объект-фасад, который просто делегирует эти вызовы соответствующим объектам системы.
Известно, что одна из основных характиристик сложности кода - это степень связанности (coupling).
Степень связанности — это мера, определяющая насколько жестко один элемент кода связан с другими элементами, либо каким количеством данных о других элементах он обладает.
Элемент с низкой степенью связанности (слабо связанный, low coupling) зависит от не очень большого числа других элементов. Выражение “очень много” сильно зависит от контекста и в каждом проекте это свое число.


Класс с высокой степенью связанности (или жестко связанный) зависит от множества других классов. Однако наличие таких классов нежелательно, поскольку оно приводит к возникновению многих проблем, например, изменения в связанных классах приводят к локальным изменениям в данном классе. Также затрудняется понимание каждого класса в отдельности, и усложняется повторное использование, поскольку для этого требуется дополнительный анализ классов, с которыми связан данный класс.
Именно для уменьшения степени связанности с целой подсистемой и применяется паттерн Фасад. Если у вас есть подсистема, которая часто меняется и из-за этого вы хотите оградить всех клиентов этой системы от знания деталей ее реализации (уменьшить связность), то логичное решение - это создать специальный класс-фасад, который будет предоставлять весь набор функций данной подсистемы. Таким образом все клиенты будут общаться с подсистемой только посредством этого класса - фасада.


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

const int InvalidCarId = -1;
const int InvalidEngineId = -1;
...

class CarSystemFacade
{
...
typedef int CarId;
typedef int EngineId;

  // Добавит машину в систему и вернет её идентификатор
  CarId createCar(const std::string& carType)
  {
    Car* car = CarFactory::create(carType);
    if (!car)
      return InvalidCarId;
    CarManager::instance().addCar(car);
    return car->getId();
  }

  // Удалит машину 
  bool removeCar(CarId carId)
  {
    Car* car = CarManager::instance().getCar(carId);
    if (!car)
      return false;
    CarManager::instance().removeCar(car);
    delete car;
    return true;
  }
  // Вернет стоимость автомобиля с идентификатором car
  float getCarCost(CarId carId)
  {
    Car* car = CarManager::instance().getCar(carId);
    if (!car)
      return 0;
    return CarManager::instance().getCostCalculator()->calculateCost(car);
  }

  // Добавит двигатель engineType к автомобилю с идентификатором car
  EngineId addEngineToCar(CarId carId, const std::string& engineType)
  {
    Car* car = CarManager::instance().getCar(carId);
    if (!car)
      return InvalidEngineId;
    Engine* engine = EngineFactory::instance().create(engineType);
    if (!engine)
      return InvalidEngineId;
    car->addEngine(engine);
    return engine->getId();
  }

  // Вернет полный список параметров двигателя
  bool getEngineParameters(EngineId engine, EngineParams& params)
  {
    Engine* engine = EngineFactory::instance().create(engineType);
    if (!engine)
      return false;
    params.power = engine->getPower();
    params.fuelConsumption = engine->getFuelConsumption();
    ...
    return true;
  }
...
};


Как вы видите, класс-фасад должен защищать клиента от любого знания о подсистеме, поэтому он вводит новые понятия. Такие, как CarId и EngineId. Это сделано для того, чтобы клиенты ничего не знали про реальные иерархии классов Car и Engine, а работали с подсистемой посредством этих идентификаторов. Это также снижает уровень связности и создает устойчивый интерфейс.
Очевидно, что для большой подсистемы такой класс-фасад тоже окажется огромным, но зато это будет единственная точка связывания подсистемы с клиентским кодом. А значит все внутренние изменения подсистемы будут проходить незаметно и безболезненно, без возможности разрушить интерфейс, который используют клиенты.
Кроме того, наличие такого класса-Фасада дает возможность полностью изменить (например, переписать с нуля) реализацию подсистемы, причем сделать это становится гораздо проще за счет слабого связывания с клиентским кодом.




Паттерн Flyweight, «Приспособленец»


Этот паттерн используется, если надо уменьшить число объектов в системе. Если объектов становится настолько много, что накладные расходы превышают лимиты, например на память, то надо найти способ, как хранить меньше объектов и иметь ту же функциональность.
Этого можно достичь, если вынести из объектов контекстно-зависимые параметры и параметры состояния. Это позволит во много раз сократить число объектов, но добавит накладные расходы на передачу контекста в эти объекты, так как контекст теперь будет храниться у клиентов.
Например, предположим, что мы в каждом автомобиле моделируем полностью все детали, включая болты. И в итоге уже через несколько минут работы программы в ней имеются миллионы объектов типа Bolt. Через час-два это уже может стать проблемой и даже привести к MemoryOverflow.

class Bolt
{
  BoltType mType;// тип болта
  float mWeight;// вес болта
  Car* mCar;// в какой машине установлен
  bool mWellTwisted;// Хорошо ли закручен
  Position mPosition;// позиция в 3д пространстве относительно центра авто
  ...
public:
  Bolt(BoltType type, float weight, Car* car, bool wellTwisted, const Position& pos) {...};
  virtual void draw()
  {
    Position pos = mCar->getPosition() + mPosition;
    DrawSystem::instance().drawBolt(mType, pos);
  };
  ...
};



Очевидно, что такие параметры, как mCar, mWellTwisted и mPosition в этом примере - это контекстно-зависимые параметры. Их можно вынести из объекта-болт в объект, который хранит болты. Вам может показаться, что это ничего не изменит - просто данные переместятся в другой объект, но на самом деле это не так. Например, объект типа Car может не хранить позиции всех своих болтов, а вычислять позиции по схеме автомобиля.
Применим паттерн Приспособленец и будем создавать только типы болтов, но не конкретные болты. А всю недостающую контекстную информацию будем передавать параметрами.

class Bolt
{
  BoltType mType;// тип болта
  float mWeight;// вес болта
  ...
public:
  Bolt(BoltType type) {...};
  virtual void draw(Car* car, const Position& boltPosition)
  {
    Position pos = car->getPosition() + boltPosition;
    DrawSystem::instance().drawBolt(mType, pos);
  };
  ...
};



Теперь мы будем иметь сотни или тысячи, но никак не миллионы уникальных объектов класса Bolt в системе. Но минус этого шаблона - увеличиваются накладные расходы на расчет и передачу контекстной информации в объекты.




Паттерн Proxy, «Заместитель»


Часто бывает необходимо прозрачно добавить некоторую функциональность объекту. Например, создавать объект только при первом обращении к нему, или добавить кеширование результатов, или сделать “умный” указатель с подсчетом ссылок, или добавить возможность работы с объектом удаленно (например, посредством RPC). При этом хочется сделать это максимально прозрачно, чтобы клиенты, использующие этот объект, не заметили разницы. Для этого применяется паттерн Заместитель.
Реализация этого паттерна очень проста. Например, предположим, что у нас в системе есть класс HighResolutionCarPicture, объекты которого хранятся в каждом объекте Car. При этом объекты HighResolutionCarPicture имеют большой размер, так как хранят текстуры. И в какой-то момент загрузка и инициализация проектов стала очень долгой, так как каждый раз грузятся эти текстуры при инициализации объекта типа Car. Применим паттерн Прокси и отложим инициализацию HighResolutionCarPicture до первого использования.

// Интерфейс. 
class IHighResolutionCarPicture
{
...
  virtual ErrorCode load(const std::string& fileName) = 0;
  virtual Picture& getPicture() = 0;
...
};

// Реальный класс, реализующий функциональность
class HighResolutionCarPicture : public IHighResolutionCarPicture
{
...
  Picture mPicture;
...
public:
...
  virtual ErrorCode load(const std::string& fileName)
  {
    // загрузка текстуры из файла
    ...
  }
  virtual Picture& getPicture()
  {
    return mPicture;
  }
...
};

// Прокси класс, создающий реальный объект только при первом обращении
class HighResolutionCarPictureProxy : public IHighResolutionCarPicture
{
  HighResolutionCarPicture* mHighResolutionCarPicture;
  void create()
  {
    if (!mHighResolutionCarPicture)
      mHighResolutionCarPicture = new HighResolutionCarPicture();
  }

...
public:
...
  virtual ErrorCode load(const std::string& fileName)
  {
    create();
    return mHighResolutionCarPicture->load(fileName);
  }
  virtual Picture& getPicture()
  {
    create();
    return mHighResolutionCarPicture->getPicture();
  }
...
};



Все клиенты, работающие с HighResolutionCarPicture должны теперь использовать указатель на IHighResolutionCarPicture. Это даст нам возможность прозрачно использовать любой прокси так, что клиенты не заметят разницы.


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


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

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


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

Ответить

 

 

 

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

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