23 января 2010 г.

MVC в примере на С++ (консольное приложение)

Знаменитый шаблон проектирования MVC за свои 30 с лишним лет истории стал настоящим эталоном, «классикой жанра». При поиске в сети обнаруживается масса примеров реализаций MVC на PHP, C# и Java. Обычно это большие программы с листингами на 2-3 экрана, массивными UML диаграммами, в которых и так новичку трудно разобраться, не говоря уже о каких-то там шаблонах. Я предлагаю свой, максимально упрощенный вариант реализации MVC на примере консольного приложения на С++.

Сначала, как водится, немного о самом MVC.

Model-View-Controller

Шаблон Model-View-Controller — это методология разделения структуры приложения на специализированные компоненты. Это не готовая библиотека классов, не фреймворк, а просто, грубо говоря, сборник советов о том, как лучше организовать классы и взаимосвязи между ними. Тут стоит оговориться, что речь идет только об объектро-ориентированных программах и соответствующих языках. Схема MVC предполагает разделение всей системы на 3 взаимосвязанных компонента (подсистемы): так называемую модель (Model), представление (View) и контроллер (Controller). У каждого компонента своя цель, а главная особенность в том, что любой из них можно с легкостью заменить на другой или модифицировать, практически не затронув другие подсистемы. Преимущества такого подхода очевидны: модульность, расширяемость, простота поддержки и тестирования.

На следующей диаграмме показана структура шаблона MVC:

  • Представление (вид) отвечает за отображение информации, поступающей из системы или в систему.
  • Модель является сутью системы и отвечает за непосредственные алгоритмы, расчёты и тому подобное внутреннее устройство системы.
  • Контроллер является связующим звеном между представлением и моделью системы, посредством которого и существует возможность произвести разделение между ними. Контроллер получает данные от пользователя и передает их в модель. Кроме того, он получает сообщения от модели, а также может изменять текущий режим представления.

MVC в действии

Наилучший способ вникнуть в суть шаблона проектирования — «пощупать» его на практике. Я выбрал язык С++, так как, наверное, с ним знакомы практически все, кто занимается программированием. Чтобы не отвлекаться на WinAPI и тонкости построения GUI приложений, мы напишем простейшее консольное приложение — конвертер температуры, используя классический MVC. В подробности С++ кодинга вдаваться не будем, а сосредоточимся на реализации.

Модель

Начнем с модели. Создадим класс TemperatureModel, задача которого — инкапсулировать всю логику конвертации градусов из шкалы Цельсия в шкалу Фаренгейта, и наоборот.

class TemperatureModel
{
public:
   TemperatureModel(float tempF)
   {
      _temperatureF = tempF;
   }
   float getF()
   {
      return _temperatureF;
   }
   float getC()
   {
      return (_temperatureF - 32.0) * 5.0 / 9.0;
   }
   void setF(float tempF)
   {
      _temperatureF = tempF;
   } 
   void setC(float tempC)
   {
      _temperatureF = tempC * 9.0 / 5.0 + 32.0;
   }
}

Температура будет храниться во внутреннем поле _temperatureF, а установить новую или получить её значение в любой из двух систем измерения помогут методы setF(), setC() и getF(), getC() соответственно.

Теперь наша задача — адаптировать этот класс под шаблон Observer.

Шаблон «Наблюдатель» (Observer) определяет зависимость типа «один ко многим» между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом событии.

Это нужно для того, чтобы остальные классы приложения «знали» о любых изменениях в модели. Для этого создадим еще два класса, которые будут базовыми для остальных — это Observable (определяет методы для добавления, удаления и оповещения «наблюдателей») и Observer (класс, с помощью которого наблюдаемый объект оповещает наблюдателей).

class Observer
{
public:
    virtual void update() = 0;
};

class Observable
{
public:
   void addObserver(Observer *observer)
   {
      _observers.push_back(observer);
   }
   void notifyUpdate()
   {
      int size = _observers.size();
      for (int i = 0; i < size; i++)
      {
         _observers[i]->update();
      }
   }
private:
   std::vector<Observer*> _observers;
};

Обратите внимание, что использован шаблонный STL класс vector. Для его использования не забудьте приписать директиву #include <vector>.

Класс Observevable содержит список всех «наблюдателей». Нового «наблюдателя» можно будет добавить с помощью метода addObserver(). При вызове метода notifyUpdate() класс Observable пройдется по списку «наблюдателей» и вызовет их методы update(), а они, в свою очередь, смогут каким-то образом на это отреагировать.

Теперь нужно сделать класс TemperatureModel «оповещателем», чтобы у него в последствии могли быть «слушатели», следящие за его изменениями. Так как мы уже создали для этого все необходимые классы, нет ничего проще, чем сделать класс TemperatureModel наследником класса Observable.

class TemperatureModel : public Observable
{ 
public:
   float getF()
   {
      return _temperatureF;
   }
   float getC()
   {
      return (_temperatureF - 32.0) * 5.0 / 9.0;
   }
   void setF(float tempF)
   {
      _temperatureF = tempF;
      notifyUpdate();
   } 
   void setC(float tempC)
   { 
      _temperatureF = tempC * 9.0 / 5.0 + 32.0;
      notifyUpdate();
   }
private:
   float _temperatureF;
};

Обратите внимание, что добавились вызовы notifyUpdate() в методах setF() и setC(). Таким образом мы достигаем нашей цели: «слушатели» будут оповещены в случае любых изменений в модели.

Представление

Теперь нам нужно создать View — класс, выводящий изменения модели на консоль. Назовем его ConsoleView.

class ConsoleView: public Observer
{
public:
   ConsoleView(TemperatureModel *model)
   {
      _model = model;
      _model->addObserver(this);
   }
   virtual void update()
   {
      system("cls");
      printf("Temperature in Celsius: %.2f\n", _model->getC());
      printf("Temperature in Farenheit: %.2f\n", _model->getF());
      printf("Input temperature in Celsius: ");
   }
private:
   TemperatureModel *_model;
};

Класс ConsoleView является наследником класса Observer, потому он сможет получать сообщения от модели. ConsoleView хранит в себе указатель на модель, который передается в конструкторе. Обратите внимание, что там же ConsoleView «подписывает» себя на изменения модели вызовом метода addObserver(). Метод update() очищает экран консоли, выводит текущие данные, а также текст «Введите температуру в цельсиях» (для простоты мы ограничимся только этим режимом).

Контроллер

Осталось «оживить» созданные классы и добавить Controller — управляющий класс, который будет отслеживать введенные пользователем данные и соответственно изменять модель.

class Controller
{
public:
   Controller(TemperatureModel *model)
   {
      _model = model;
   }
   void start()
   {
      _model->setC(0);

      float temp;
      do
      {
         scanf("%f", &temp);
         _model->setC(temp);
      }
      while (temp != 0);
   }
private:
   TemperatureModel *_model;
};

Класс Controller, так же как и ConsoleView, получает ссылку на модель в конструкторе. Метод start() сбрасывает модель в первоначальное состояние и запускает цикл ввода данных до тех пор, пока пользователь не введет 0.

Конечная реализация

А теперь пришло время собрать все вместе и написать функцию main()!

int main()
{
   TemperatureModel model;
   ConsoleView view(&model);
   Controller controller(&model);
   controller.start();
   return 0;
}

Ее содержание, как и предполагалось, оказалось максимально простым. Все что нужно, — это создать экземпляры классов TemperatureModel (модель), ConsoleView (представление) и Controller (управление), а затем запустить управление вызовом метода controller.start().

Вывод на консоль будет выглядеть так:

Temperature in Celsius: 36.60
Temperature in Farenheit: 97.88
Input temperature in Celsius: 36.6_

Заключение

Вы можете модифицировать написанную программу, добавив режим ввода температуры в шкале Фаренгейта: для этого понадобится дополнительный метод setMode() в классе ConsoleView, который будет устанавливать текущий режим отображения (их должно быть два: с предложением ввести градусы в цельсиях и в фаренгейтах), а сам режим будет устанавливать контроллер. Вы можете изменить поведение модели, или способ ввода данных, либо добавить графический интерфейс — каждое из этих изменений затронет только соответствующий слой, и вам не придется каждый раз переписывать приложения заново.

Надеюсь, кому то этот пример поможет быстро освоить основные принципы MVC и позволит создавать намного более сложные системы. Удачи!

1 комментарий:

  1. Одна из лучших статей для начинающих. Спасибо.

    ОтветитьУдалить