Pull to refresh

Comments 35

А это точно шаблонный метод, а не стратегия?
Если я правильно помню, реализация паттерна "Стратегия" заключается в том, что некий класс меняет своё поведение, исходя из настроек, заданных клиентом. Шаблонный метод же направлен на создание иерархии классов, реализующих общий алгоритм с некоторой модификацией.
Это "ответная часть" к стратегии, которая её использует;-)...
Строго говоря, класс, который занимается шифрованием, про файлы сильно знать не должен. Более «чистый» пример бы был про какие-нибудь Reader'ы — буфера, файлов, потока и тд

Ну а по самому паттерну, точнее реализации все отлично, спасибо :)
Согласен, слегка притянуто за уши, ничего эстетичнее на тот момент не пришло в голову :)
В этому паттерне не обязательно использовать классы, например, функция сортировки тоже использует Шаблонный метод, там передаётся функция или указатель на функцию для сравнения значений.
Точно так, в отсутствие классов предикат в качестве параметра функции вполне заменяет переопределяемую виртуальную функцию-член класса из примеров в заметке.
… а вот это как раз стратегия, а не шаблонный метод.
Вот тут показана реализация шаблонного метода для сортировки. Представим, что у нас чистый C, передаём указатель на функцию. Почему это будет стратегия? Вообще, навскидку не придумалась реализация стратегии без классов, буду благодарен за пример.
Почему это будет стратегия?

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

Вообще, навскидку не придумалась реализация стратегии без классов, буду благодарен за пример.

А стратегия (классическая) и реализуется на классах: Dictionary<T,V>(IComparer<T>). Здесь компаратор — это как раз стратегия. А без классов — все ФП к вашим услугам, почти любая ФВП может трактоваться как стратегия.
Аргументы понятны. В случае с сортировкой — это всё же шаблонный метод — налицо (сложный) алгоритм, вариативная часть, изменена лишь реализация "замены" части алгоритма в соответствии с реалиями си.
налицо (сложный) алгоритм, вариативная часть, изменена лишь реализация «замены» части алгоритма

В том-то и дело, что нет. У вас нет вариативной части, алгоритм всегда идет по одному и тому же правилу "меньшее — раньше". Другое дело, что определение "меньше/больше" (которое, в общем-то, и не является обязанностью алгоритма) вынесено за его пределы.

в соответствии с реалиями си.

Понимаете ли, в чем дело… паттерны, на которые вы сейчас смотрите — они для объектно-ориентированных языков (в ФП часть из них будет будет основой парадигмы, а часть перестанет быть актуальна), поэтому реалии Си, не-ООП-языка, к этим шаблонам не применимы.
Теоретически, в функции needSwap могут происходить самые чудесные вещи, измерять вариативность алгоритма семантическими представлениями о сортировке не совсем корректно.
Что касается паттернов и ООП, тут да, разговор ни о чём — классические паттерны "банды четырёх" именно для ООП. Хотя в процессе гуглежа нашлась книга "Patterns in C", реализация паттерна "Стратегия" там довольно занятная.
Теоретически, в функции needSwap могут происходить самые чудесные вещи

Это будет нарушением контракта.

измерять вариативность алгоритма семантическими представлениями о сортировке не совсем корректно.

Как раз корректно. Паттерн в первую очередь определяется семантикой.

Хотя в процессе гуглежа нашлась книга «Patterns in C», реализация паттерна «Стратегия» там довольно занятная.

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

Какого контракта?

Паттерн в первую очередь определяется семантикой.

Это паттерн поведения, какую семантику вы от него ожидаете?
Какого контракта?

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

Это паттерн поведения, какую семантику вы от него ожидаете?

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

Если есть интерес продолжить беседу, давайте синхронизируем часы, похоже мы разные вещи обдумываем.
Мы хотим предоставить пользователю алгоритм сортировки, который он мог бы использовать со своим предикатом (си) или с виртуальной функцией, определяемой в наследующем классе (c++). Что там в этой функции — нам, как составителю алгоритма, вообще неинтересно. Вплоть до того, что класс с алгоритмом сортировки может быть шаблонным и сортировать мы теоретически можем всё, что угодно путями, которые нам, как составителю, неизвестны, неинтересны, и оставлены на откуп пользователю. Какой контракт будет нарушаться, если предикат или функция будет выполнять работу, неочевидную нам, если на выходе всё равно будет сортированная последовательность?
Кстати, в упомянутой Lertmind книге Фримена "Паттерны проектирования" как раз что-то подобное приводится в качестве примера, сортируют уток.

Ту, которая описана в поведении, как ни странно.

Это шаблонный метод, он исполняет алгоритм с вариациями, всё вполне легитимно.
Что там в этой функции — нам, как составителю алгоритма, вообще неинтересно.

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

Какой контракт будет нарушаться, если предикат или функция будет выполнять работу, неочевидную нам, если на выходе всё равно будет сортированная последовательность?

Понимаете ли, алгоритм сортировки — это уже контракт. От него ожидают, что он получит на вход данные, и вернет отсортированные данные. Никто не ожидает, что он, скажем, изменит эти данные, или запишет их в БД, или пошлет на Луну. Соответственно, чтобы удовлетворять этим ожиданиям, алгоритм, в свою очередь, имеет некоторые ожидания к стратегии сортировки, которая в него передана.

Это шаблонный метод, он исполняет алгоритм с вариациями, всё вполне легитимно.

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

Впрочем, разница между шаблонным методом и стратегией, как уже говорилось, не столько в семантике, сколько в реализации. Используете наследование и переопределение методов? Шаблонный метод. Используете объект, получающий часть снаружи? Стратегия.
Спасибо, под таким углом я не смотрел. Полезно обсуждать такие штуки с архитектором :)
Что там в этой функции — нам, как составителю алгоритма, вообще неинтересно.
Дополню к ответу lair: на сортируемых элементах, как минимум, требуется наличие частичного порядка, это ключевая часть контракта алгоритма сортировки (иначе случается GIGO). Полное отсутствие побочных эффектов не является необходимым, но желательно. Как пример, отображение в множество, используемое для сравнения может кэшироваться.
А я об этом узнал из книги "Паттерны проектирования" Эрик Фримен, Элизабет Фримен, Кэти Сиерра, Берт Бейтс. Само определение паттерна "Шаблонный метод" больше подходит, к тому же.
Никакого отношения к шаблонам c++ он не имеет

Как фанат, очень расстроился, дочитав до этого места. Попробую исправить это непотребство:

Шаблонный метод на шаблонах
template < typename T_Implementation >
class Crypt : public T_Implementation {
public:
    DiskFileCrypt()
        : T_Implementation() {};

    DiskFileCrypt(const typename T_Implementation::InitParams &inParams)
        : T_Implementation(inParams) {};

private:

    //-------------------------------------
    //Изменяемые функции
    void getString(std::string &outString) {
      T_Implementation::getString(outString);
    }
    void saveString(const std::string &inContent) {
      T_Implementation::saveString(inContent);
    }

    virtual std::string applyEncryption(const std::string& inContent) {
        return T_Implementation::applyEncryption(inContent);
    }

    //-------------------------------------
    //Неизменяемые функции
    void setupRnd() {
        // Некая инициализация алгоритма случайных чисел
        std::cout << "setup rnd\n";
    };

    void setupAlgorithm() {
        // Начальные установки алгоритма шифрования
        std::cout << "setup algorithm\n";
    };

    void wipeSpace() {
        // Удаление следов работы
        std::cout << "wipe\n";
    };

    void encrypt() {
        // Установка начальных параметров
        setupRnd();
        setupAlgorithm();

        // Получаем строку
        std::string fContent; getString(fContent);
        // Применяем шифрование
        std::string enc = applyEncryption(fContent);
        // Сохраняем строку
        saveString(fContent);

        // Подчищаем следы работы алгоритма
        wipeSpace();
    }   
};

class DiskFileImplementation {
public:
    typedef std::string InitParams;

    DiskFileImplementation(const std::string& fName)
        : fileName(fName) { };

private:
    std::string fileName;

protected:
    void getString(std::string &outContent) {
        std::cout << "get disk file named \"" << fileName << "\"\n";
        // Прочитать файл с диска и вернуть содержимое
        return fileContent;
    }
    void saveString(const std::string &inContent) {
        std::cout << "save disk file named \"" << fileName << "\"\n";
        // Записать файл на диск
    }

    std::string applyEncryption(const std::string& content) {
        // Шифрование
        std::string result = someStrongEncryption(content);
        return result;
    }
};

typedef Crypt < DiskFileImplementation > DiskFileCrypt;

Любопытно, нужно только переименовать конструкторы и сделать encrypt открытой :)
Оу, точно… Стоило всё-таки скомпилировать. Приношу свои извинения.
Шаблонный метод на шаблонах называется CRTP.
Это раньше может и имело какой-то смысл, сейчас так или иначе DI подойдет лучше. Код более обособленный, тестируемый. Да и даже стратегии поведения подошли куда лучше бы, особенно в разрезе C++ c его typedef.

P.S. Да и в таких случаях надо использовать protected, а не private.
Да и в таких случаях надо использовать protected, а не private.

Для этого должна быть какая-то необходимость, например, вызов из виртуальной функции наследующего класса одноимённой функции базового. В примере это не используется, и объявление функций закрытыми сделано намеренно.
И как это метод saveString не подходит под ваше описание?
И как это метод saveString не подходит под ваше описание?

Так, что метод saveString напрямую не вызывается из наследующего класса.
Это раньше может и имело какой-то смысл, сейчас так или иначе DI подойдет лучше.

Не всегда. Если у вас реально шаблонная реализация, навроде провайдеров безопасности или чего-то аналогичного — template method прекрасно себя чувствует. Да даже банальные action filters в asp.net mvc.
То есть вы реально на этот вот метод
std::cout << «save disk file named \»" << fileName << "\"\n";
пишите тесты? И ради этого принимаете архитектурное решение не использовать шаблонный метод?

Потому, что на базовый класс тесты пишутся не сложнее.
Немного запутался.

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

но

virtual std::string applyEncryption(const std::string& content)

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

class XorCrypt : public Crypt {
public:
    XorCrypt() = default;
private:
    virtual std::string getString() {
        std::cout << "xor str get\n";
        return "";
    }
    virtual void saveString(const std::string& content) {
        std::cout << "xor str save\n";
    }
    virtual std::string applyEncryption(const std::string& content) {
        // Шифрование
        std::string result = applyXor(content);
        std::cout << "xor\n";
        return result;
    }
};
Sign up to leave a comment.

Articles