Skip to content

Latest commit

 

History

History
325 lines (262 loc) · 8.4 KB

index.adoc

File metadata and controls

325 lines (262 loc) · 8.4 KB

Master programming.
Лекция №11 (Некоторые приёмы программирования)

1. Метапрограммирование

1.1. CRTP (Curiously recurring template pattern)

  • Статический полиморфизм

  • Замена виртуальных функций при компиляции

  • Подходит только там, где это возможно

template<class Derived>
class A
{
public:
    void method()
    {
        static_cast<Derived*>(this)->some_method();
    }
};

class B: A<B>
{
public:
    void some_method()
    {
        lala;
    }
};

1.2. Статическая функция для конструктора

  • Необходимость проинициализировать базовый класс сложным образом

  • Нет доступа к переменным класса, так как он ещё не проинициализирован

class A: public vector<int>
{
public:
    A(): vector(init())
    {}

private:
    static vector<int> init()
    {
        return ...
    }
};

1.3. Дополнительный класс для инициализации

  • Нужно переопределить порядок инициализации базового класса и наследника

class A: public vector<double>
{
public:
    // Так не будет работать.
    A(double y): y_(y * y - std::sin(y)), vector({{y_, y_ + 1, y_ + 2}})
    {}

private:
    double y_;
};

class B_helper
{
public:
    B_helper(double y): y_(y * y - std::sqrt(y))
    {}

private:
    double y_;
};

class B: public B_helper, public vector<double>
{
public:
    B(double y): B_helper(y), vector({{y_, y_ + 1, y_ + 2}})
    {}
};

1.4. Расширение функциональности через наследование

  • Изменение функциональности приводит к "порче" класса

  • Расширение — это просто декоратор или адаптор

  • Старый класс можно использовать для ещё одного расширения

  • В базовом классе сохраняется малое количество методов

struct A
{
    void some_method();
    void another_method();
}

struct B: A
{
    void extra_method();
}

1.5. Атовыводимость типов для конструктора

  • Неоднозначность в подстановке типа в конструкторе

    1. Правило вывода

    2. Make-функция

  • Реализация perfect forwarding

template<class V>
class A
{
public:
    template<class B>
    A(B x)
    { ... }
};

template<class B, V = deduce B>
A<V> make(B x)
{
    return A<V>(x);
}

1.6. Константность методов

  • Почти все методы нужно делать константными

  • Можно было бы ввести ключевое слово mutable

template<class T>
class iterator
{
public:
    T& operator*() const
    {
        return *ptr_;
    }

private:
    T* ptr_;
};

1.7. Замена виртуальности

  • Возможна только в тех случаях, когда точно известны все классы

  • Основано на утиной типизации

  • Используется std::variant

  • Минусы: везде надо использовать std::visit, все функции должны быть шаблонизированы, возвращаемый тип должен быть одинаковым

class Lala
{
public:
    void method();
};

class Lala2: public Lala
{
public:
    void method()
    {
        // override
    }
};

class Lala3
{
public:
    void method()
    {
        // new implementation
    }
};

std::variant<Lala, Lala2, Lala3> obj = ...;
std::visit([](auto& x) { x.method(); }, obj)

2. SOLID

2.1. (S) Принцип единственной ответственности

  • Класс должен отвечать за одну сущность

  • Изменение класса должно быть связано только с изменением этой сущности

Пример

Конвертер форматов:

  • Изменился один из выходных форматов — изменяется конвертер

  • Изменился один из входных форматов — изменяется конвертер

  • Изменилось имя для работы с сервером — изменился конвертер (wat??)

Решение

Подход pandoc:

  • На каждый выходной формат свой конвертер

  • На каждый входной формат свой читатель

  • Введение внутреннего формата

2.2. (O) Принцип открытости/закрытости

  • Класс закрыт для изменений

  • Получить новую функциональность можно через расширение класса (наследования, перегрузки)

  • Принцип интерфейса и реализации

  • Модульное тестирование в таком подходе: наследование от класса для предоставления скрытых связей внутри класса

  • Полиморфизм в терминах наследования от интерфейса

  • Множественное наследование от интерфейсов

2.3. (L) Принцип подстановки Барбары Лисков

  • Старое поведение базового класса должно оставаться неизменным в наследнике

  • В программе можно вместо базового класса написать наследника — поведение должно остаться прежним

Список с дублированными элементами.

test<ListType>():
    ListType l;
    l.add(44)
    CHECK(l.size() == 1)

DoubleList(List):
    add(x):
        List::add(x)
        List::add(x)

test<DoubleList>() // fail

Как быть?

  • Разобраться какой из интерфейсов должен остаться прежним

  • Ввести понятие дублированный элемент, а не список с дублированием

2.4. (I) Принцип разделения интерфейса

  • Много мелких интерфейсов лучше, чем один большой

  • Принцип избегания "божественного объекта"

  • Сущности не зависят от методов, которые не используют

Объединённый интерфейс Разделённый интерфейс
driver:
    allocate(size): void*
    deallocate(void*)
    set_program(byte[])
    read(ptr, count, offset): vector<byte>
    write(ptr, count, offset)
    enqueue()
    wait()
allocator:
    allocate(size): void*
    deallocate(void*)

driver_data:
    ptr
    count
    offset

io_handler:
    enqueue(driver_data)
    wait()

driver(allocator):
    set_program(byte[]) -> io_handler

2.5. (D) Принцип инверсии зависимостей

  • Модули верхних уровней не должны зависеть от модулей нижних уровней

  • Абстракции не должны зависеть от деталей

  • Разорвать связи можно введением дополнительного уровня

  • Проверка — написание модульного теста по типу белого ящика

Встроенная зависимость Зависимость вне класса
tq:
    - tile:
        ptr
        count

    - vector<tile>

    push(ptr, count)
    begin(): vector<tile>::iterator
    end(): vector<tile>::iterator
tq2<Tile>:
    - vector<Tile>
    push<Ptr>(ptr, count)
    begin(): vector<Tile>::iterator
    end(): vector<Tile>::iterator