Подтвердить что ты не робот

Что такое Mixins (как концепция)

Я пытаюсь разобраться в концепции Mixin, но я не могу понять, что это такое. Я вижу, что это способ расширить возможности класса с помощью наследования. Я читал, что люди называют их "абстрактными подклассами". Может кто-нибудь объяснить, почему?

Буду признателен, если вы объясните свой ответ на следующем примере (с одного из моих слайд-шоу): A C++ Mixin Example

4b9b3361

Ответ 1

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

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

Идея с mix-ins состоит в том, чтобы предоставить кучу примитивных классов, где каждая из них моделирует базовую ортогональную концепцию и может объединять их вместе для создания более сложных классов с помощью только той функции, которую вы хотите - своего рода как легос. Сами примитивные классы предназначены для использования в качестве строительных блоков. Это расширяемо, так как позже вы можете добавить другие примитивные классы в коллекцию, не затрагивая существующие.

Возвращаясь к С++, для этого нужно использовать шаблоны и наследование. Основная идея здесь заключается в том, что вы соединяете эти строительные блоки вместе, предоставляя их через параметр шаблона. Затем вы соединяете их вместе, например. через typedef, чтобы сформировать новый тип, содержащий нужную функциональность.

Взяв ваш пример, предположим, что мы хотим добавить функциональность redo сверху. Вот как это могло бы выглядеть:

#include <iostream>
using namespace std;

struct Number
{
  typedef int value_type;
  int n;
  void set(int v) { n = v; }
  int get() const { return n; }
};

template <typename BASE, typename T = typename BASE::value_type>
struct Undoable : public BASE
{
  typedef T value_type;
  T before;
  void set(T v) { before = BASE::get(); BASE::set(v); }
  void undo() { BASE::set(before); }
};

template <typename BASE, typename T = typename BASE::value_type>
struct Redoable : public BASE
{
  typedef T value_type;
  T after;
  void set(T v) { after = v; BASE::set(v); }
  void redo() { BASE::set(after); }
};

typedef Redoable< Undoable<Number> > ReUndoableNumber;

int main()
{
  ReUndoableNumber mynum;
  mynum.set(42); mynum.set(84);
  cout << mynum.get() << '\n';  // 84
  mynum.undo();
  cout << mynum.get() << '\n';  // 42
  mynum.redo();
  cout << mynum.get() << '\n';  // back to 84
}

Вы заметите, что я сделал несколько изменений от вашего оригинала:

  • Виртуальные функции действительно не нужны здесь, потому что мы точно знаем, что наш составной тип класса находится во время компиляции.
  • Я добавил по умолчанию value_type для второго параметра шаблона, чтобы сделать его использование менее громоздким. Таким образом, вам не нужно набирать <foobar, int> каждый раз, когда вы вставляете кусок вместе.
  • Вместо создания нового класса, который наследуется от частей, используется простой typedef.

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

Как побочный элемент, вы также можете найти эту статью.

Ответ 2

Mixin - это класс, назначенный для обеспечения функциональности для другого класса, обычно через определенный класс, который предоставляет основные функции, которые необходимы функциональности. Например, рассмотрите ваш пример:
Смесин в этом случае обеспечивает функциональность отмены заданной операции класса значений. Эта хабитация основана на функциональности get/set, предоставляемой параметризованным классом (класс Number в вашем примере).

Другой пример (извлечено из "Программирование на основе Mixin на С++" ):

template <class Graph>
class Counting: public Graph {
  int nodes_visited, edges_visited;
public:
  Counting() : nodes_visited(0), edges_visited(0), Graph() { }
  node succ_node (node v) {
    nodes_visited++;
    return Graph::succ_node(v);
  }
  edge succ_edge (edge e) {
    edges_visited++;
    return Graph::succ_edge(e);
  }
... 
};

В этом примере mixin предоставляет функциональность подсчета вершин, учитывая класс графа, который выполняет трансверсальные операции.

Обычно в С++ mixins реализуются с помощью CRTP идиомы. Этот поток может быть хорошо прочитан о реализации mixin в С++: Что такое С++ Mixin-Style?

Вот пример mixin, который использует идиому CRTP (благодаря @Simple):

#include <cassert>
#ifndef NDEBUG
#include <typeinfo>
#endif

class shape
{
public:
    shape* clone() const
    {
        shape* const p = do_clone();
        assert(p && "do_clone must not return a null pointer");
        assert(
            typeid(*p) == typeid(*this)
            && "do_clone must return a pointer to an object of the same type"
        );
        return p;
    }

private:
    virtual shape* do_clone() const = 0;
};

template<class D>
class cloneable_shape : public shape
{
private:
    virtual shape* do_clone() const
    {
        return new D(static_cast<D&>(*this));
    }
};

class triangle : public cloneable_shape<triangle>
{
};

class square : public cloneable_shape<square>
{
};

Этот mixin предоставляет функциональность гетерогенной копии для набора (иерархии) классов формы.

Ответ 3

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

Великий волчок заявил: "Виртуальные функции действительно здесь не нужны, потому что мы точно знаем, что наш составной тип класса находится во время компиляции". К сожалению, вы можете столкнуться с некоторым непоследовательным поведением, если вы используете свой объект полиморфно.

Позвольте мне настроить основную функцию из его примера:

int main()
{
  ReUndoableNumber mynum;
  Undoable<Number>* myUndoableNumPtr = &mynum;

  mynum.set(42);                // Uses ReUndoableNumber::set
  myUndoableNumPtr->set(84);    // Uses Undoable<Number>::set (ReUndoableNumber::after not set!)
  cout << mynum.get() << '\n';  // 84
  mynum.undo();
  cout << mynum.get() << '\n';  // 42
  mynum.redo();
  cout << mynum.get() << '\n';  // OOPS! Still 42!
}  

Сделав функцию "set" виртуальной, будет вызвано правильное переопределение, и несогласованное поведение выше не произойдет.

Ответ 4

Микшины в С++ выражаются с помощью Curiously Recurring Template Pattern (CRTP). Этот пост - отличная разбивка того, что они предоставляют по сравнению с другими методами повторного использования... полиморфизм времени компиляции.

Ответ 5

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

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

У вас есть база данных пользователей, эта база данных имеет определенный способ доступа к ее данным. теперь представьте, что у вас есть facebook, который также имеет определенный способ доступа к своим данным (api).

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

потому что интерфейс promises, что в репозиториях реализации будут указаны объявленные в них методы, вы знаете, что везде или когда вы используете этот интерфейс в своем приложении, если вы переключите данные, они всегда будут иметь методы, которые вы являются определяющими и, следовательно, имеют данные для работы.

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

Вот некоторый псевдокод.

interface IUserRepository
{
    User GetUser();
}

class DatabaseUserRepository : IUserRepository
{
    public User GetUser()
    {
        // Implement code for database
    }
}

class FacebookUserRepository : IUserRepository
{
    public User GetUser()
    {
        // Implement code for facebook
    }
}

class MyApplication
{
    private User user;

    MyApplication( IUserRepository repo )
    {
        user = repo;
    }
}

// your application can now trust that user declared in private scope to your application, will have access to a GetUser method, because if it isn't the interface will flag an error.