вторник, 26 апреля 2016 г.

Паттерн "Наблюдатель"

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

(Аркадий и Борис Стругацкие "Понедельник начинается в субботу")
До вчерашнего дня мне казалось, что тема паттернов, ненавязчиво затронутая в Паттерн "Стратегия", нашла свое логическое завершение в Паттерн "Стратегия": жаркое из питона. Похоже на то, что я заблуждался, ибо интерес к этой теме продолжал тлеть все это время и проявился в виде просьбы продолжить ее дальше.
Один из моих читателей, интересующийся C++, вернее, пытающийся вглядываться в горизонты C++, опираясь на знания Java, предпринял самостоятельную попытку переписать на C++ код паттерна "Наблюдатель" из книги Head First Design Patterns By Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra, но в ходе этой работы у него возникли затруднения, которые мы сегодня постараемся разрешить.
Изучение C++ таким способом безусловно содержит в себе рациональное зерно и вполне может оказаться, что такой подход к изучению нового языка даст неплохие результаты.
Ну что же, давайте попробуем написать код паттерна "Наблюдатель" на C++, сохраняя, по возможности, некоторую общность с имеющейся реализацией на Java.
#include <iostream>
#include <vector>
#include <algorithm>

class Observer {
public:
    virtual void update(float temp, float humidity, float pressure) = 0;
};


class Subject {
public:
    virtual void registerObserver(Observer *obj) = 0;
    virtual void removeObserver(Observer *obj) = 0;
    virtual void notifyObservers() = 0;
};


class WeatherData: public Subject {
    std::vector<Observer *> observers;
    float temperature;
    float humidity;
    float pressure;
public:
    void registerObserver(Observer *obj) { 
         observers.push_back(obj);
         std::cout << "Observers: " << observers.size() << std::endl;
    }

    void removeObserver(Observer *obj) {
        observers.erase(std::remove(observers.begin(), observers.end(), obj), observers.end());
    }

    void notifyObservers() {
        std::vector<Observer *>::const_iterator cii;
        for (cii=observers.begin(); cii!=observers.end(); cii++) {
            (*cii)->update(temperature, humidity, pressure);
       }
    }

    void measurementsChanged() { notifyObservers(); }

    void setMeasurements(float temperature, float humidity, float pressure) {
        this->temperature = temperature;
        this->humidity = humidity;
        this->pressure = pressure;
        measurementsChanged();
    }

    float getTemperature() { return temperature; }
    float getHumidity() { return humidity; }
    float getPressure() { return pressure; }
};


class DisplayElement {
public:
    virtual void display() = 0;
};


class CurrentConditionsDisplay: public Observer, DisplayElement {
    float temperature;
    float humidity;
    Subject *weatherData;
public:
    CurrentConditionsDisplay(Subject *weatherData) {
        this->weatherData = weatherData;
        weatherData->registerObserver(this);
    }

    void update(float temperature, float humidity, float pressure) {
        this->temperature = temperature;
        this->humidity = humidity;
        display();
    }

    void display() {
        std::cout << "Current conditions: " << temperature 
                  << "F degrees and " << humidity << "% humidity" << std::endl;
    }
};


class StatisticsDisplay: public Observer, DisplayElement {
    float maxTemp;
    float minTemp;
    float tempSum;
    int numReadings;
    WeatherData *weatherData;
public:
    StatisticsDisplay(WeatherData *weatherData) {
        this->weatherData = weatherData;
        weatherData->registerObserver(this);
        maxTemp = 0.0f;
        minTemp = 200;
        tempSum = 0.0f;
        numReadings = 0;
    }

    void update(float temp, float humidity, float pressure) {
        tempSum += temp;
        numReadings++;

        if (temp > maxTemp) { maxTemp = temp; }
        if (temp < minTemp) { minTemp = temp; }

        display();
    }

    void display() {
        std::cout << "Avg/Max/Min temperature = " << (tempSum / numReadings)
                  << "/" << maxTemp << "/" << minTemp << std::endl;
    }
};


class ForecastDisplay: public Observer, DisplayElement {
    float currentPressure;  
    float lastPressure;
    WeatherData *weatherData;
public:
    ForecastDisplay(WeatherData *weatherData) {
        this->weatherData = weatherData;
        weatherData->registerObserver(this);
        currentPressure = 29.92f;
    }

    void update(float temp, float humidity, float pressure) {
        lastPressure = currentPressure;
        currentPressure = pressure;

        display();
    }

    void display() {
        std::cout << "Forecast: " << std::endl;
        if (currentPressure > lastPressure) {
            std::cout << "Improving weather on the way!" << std::endl;
        } else if (currentPressure == lastPressure) {
            std::cout << "More of the same" << std::endl;
        } else if (currentPressure < lastPressure) {
            std::cout << "Watch out for cooler, rainy weather" << std::endl;
        }
    }
};


int main() {
    WeatherData weatherData;

    CurrentConditionsDisplay currentDisplay(&weatherData);
    StatisticsDisplay statisticsDisplay(&weatherData);
    ForecastDisplay forecastDisplay(&weatherData);

    weatherData.setMeasurements(80, 65, 30.4f);
    weatherData.setMeasurements(82, 70, 29.2f);
    weatherData.setMeasurements(78, 90, 29.2f);
}


$ g++ -Wall weather.cpp -o weather
$ ./weather.exe
Observers: 1
Observers: 2
Observers: 3
Current conditions: 80F degrees and 65% humidity
Avg/Max/Min temperature = 80/80/80
Forecast:
Improving weather on the way!
Current conditions: 82F degrees and 70% humidity
Avg/Max/Min temperature = 81/82/80
Forecast:
Watch out for cooler, rainy weather
Current conditions: 78F degrees and 90% humidity
Avg/Max/Min temperature = 80/82/78
Forecast:
More of the same
И под занавес замечу, что заслуживают более пристального внимания пара нюансов. А именно, тело метода
void removeObserver(Observer *obj)
Здесь присутствует "Erase–remove idiom", на которую стоит обратить свой взор и ознакомиться с ней поближе. Тело метода
void notifyObservers()
можно написать несколько иначе, если использовать C++ версии 11:
for (const auto& i: observers) { i->update(temperature, humidity, pressure); }
Команда вызова компилятора в этом случае:
g++ -Wall -std=c++11 weather.cpp -o weather
И еще... Ну о "еще" поговорим в другой раз по мере появления новых вопросов.

четверг, 7 апреля 2016 г.

Паттерн "Стратегия": жаркое из питона.

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

(Антуан де Сент-Экзюпери "Маленький принц")
Этот пост является продолжением темы, начатой в Паттерн "Стратегия".
Поводом для его написания стали вопросы, которые возникли у двоих моих читателей после изучения раздела 3.9 "Паттерн Стратегия" книги Марка Саммерфилда "Python на практике" издательства "ДМК Пресс".
Вероятно, что еще одной причиной для продолжения обсуждения также послужила моя фраза, позаимствованная у И.Ильфа и Е.Петрова, слегка переиначенная, и как-то оброненная в ходе разговора, "Саммерфилд - это голова!.."
Для тех, кто еще не читал этой книги и этого раздела, я очень кратко изложу суть дела. В самом начале раздела Саммерфилд справедливо утверждает: "Паттерн Стратегия позволяет инкапсулировать набор взаимозаменяемых алгоритмов, из которых пользователь выбирает тот, что ему нужен."
Далее в разделе 3.9 приведены два разных алгоритма организации списка с произвольным количеством элементов в виде таблицы с указанным числом строк. Один алгоритм выводит фрагмент HTML-разметки, а другой порождает результат в виде простого текста.
Параметризовать алгоритм можно разными способами. Очевидный подход - создать класс Layout, который принимает экземпляр Tabulator, который строит ту или иную таблицу. Пример в файле tabulator1.ру. Это решение можно улучшить, используя табуляторы без состояния, имеющие только статические методы, и передавать алгоритму класс, а не экземпляр табулятора. Пример в файле tabulator2.ру.
В этом разделе показан более простой, но при этом еще более удачный (по словам автора) прием: класс Layout, который принимает в виде параметра функцию табуляции, реализующую нужный алгоритм. Пример в файле tabulator4.ру. Упомянутые py-файлы можно скачать отсюда.

В свете того, о чем шла речь в начальном посте Паттерн "Стратегия", появился вопрос, смысл которого сводился к следующему. Так как общеизвестен факт, что на скриптовом языке, создавать программы проще, чем на языках со статической типизацией, то насколько сложнее воплотить паттерн "Стратегия" для рассматриваемой задачи на C++ по сравнению с реализацией на Python, предложенной в книге Саммерфилда.
В виду того, что реализация алгоритмов вывода списка в виде html или плоского текста в данном конкретном случае является только "гарниром", поэтому на ней останавливаться не буду, а покажу сам каркас - отражу в коде только те элементы решения, которые относятся к сути рассматриваемого вопроса.
#include <iostream>
#include <vector>
#include <string>

class Tabulator {
public:
    virtual void tabulate (int,  std::vector&) = 0;
};

class HtmlTabulator: public Tabulator {
public:
    void tabulate(int rows,  std::vector& items ) {.
        std::cout << "\thtml:" << rows << std::endl;.
        std::vector::const_iterator cii;
        for (cii=items.begin(); cii!=items.end(); cii++) {
            std::cout << *cii << std::endl;
        }
    }
};

class TextTabulator: public Tabulator {
public:
    void tabulate(int rows,  std::vector& items) {.
        std::cout << "\ttext:" << rows << std::endl;.
        std::vector::const_iterator cii;
        for (cii=items.begin(); cii!=items.end(); cii++) {
            std::cout << *cii << std::endl;
        }
    }
};

class Layout {
    Tabulator *tabulator;
public:
    Layout(Tabulator *pt) { tabulator = pt; }
    void tabulate(int rows,  std::vector& items) { tabulator->tabulate(rows, items); }
};

int main() {
    std::vector WINNERS = {
        "Nikolai Andrianov", "Matt Biondi", "Bjørn Dæhlie",
        "Birgit Fischer", "Sawao Kato", "Larisa Latynina", "Carl Lewis",
        "Michael Phelps", "Mark Spitz", "Jenny Thompson"};

    HtmlTabulator htmltab;
    TextTabulator texttab;

    /* возможный вариант:
    Tabulator *ptab;
    ptab = &htmltab;
    ptab->tabulate(12, WINNERS);
    ptab = &texttab;
    ptab->tabulate(14, WINNERS);
    */

    Layout htmllayout(&htmltab);
    htmllayout.tabulate(12, WINNERS);
    Layout textlayout(&texttab);
    textlayout.tabulate(14, WINNERS);

    return 0;
}


$ g++ -g -Wall -std=c++11 tab4.cpp -o tab4
$ ./tab4
        html:12
Nikolai Andrianov
Matt Biondi
Bjørn Dæhlie
Birgit Fischer
Sawao Kato
Larisa Latynina
Carl Lewis
Michael Phelps
Mark Spitz
Jenny Thompson
        text:14
Nikolai Andrianov
Matt Biondi
Bjørn Dæhlie
Birgit Fischer
Sawao Kato
Larisa Latynina
Carl Lewis
Michael Phelps
Mark Spitz
Jenny Thompson
Вариантов здесь можно предложить несколько. Для полноты картины ниже я приведу еще один, в котором передам функцию в качестве параметра:
#include <iostream>
#include <vector>
#include <string>

void html_tabulator(int rows,  std::vector& items) {
    std::cout << "\thtml:" << rows << std::endl;
    std::vector::const_iterator cii;
    for (cii=items.begin(); cii!=items.end(); cii++) {
        std::cout << *cii << std::endl;
    }
}

void text_tabulator(int rows,  std::vector& items) {
    std::cout << "\ttext:" << rows << std::endl;
    std::vector::const_iterator cii;
    for (cii=items.begin(); cii!=items.end(); cii++) {
        std::cout << *cii << std::endl;
    }
}

class Layout2 {
public:
    void (*tabulate)(int rows, std::vector& items);
    Layout2(void (*items)(int rows, std::vector& items)) {
        tabulate = items;
    }
};

int main() {
    std::vector WINNERS = {
        "Nikolai Andrianov", "Matt Biondi", "Bjørn Dæhlie",
        "Birgit Fischer", "Sawao Kato", "Larisa Latynina", "Carl Lewis",
        "Michael Phelps", "Mark Spitz", "Jenny Thompson"};

    Layout2 htmllayout(html_tabulator);
    htmllayout.tabulate(12, WINNERS);

    Layout2 textlayout(text_tabulator);
    textlayout.tabulate(14, WINNERS);

    return 0;
}
Экранный вывод будет в точности такой же, как и в первом варианте.

Полагаю, что решение на C++ является ничуть не сложнее решения на Python. Книгу Марка Саммерфилда "Python на практике" рекомендую почитать. Интересная и стоящая книга.
"Саммерфилд - это голова!"