Что такое Mixins (как концепция)
Я пытаюсь разобраться в концепции Mixin, но я не могу понять, что это такое.
Я вижу, что это способ расширить возможности класса с помощью наследования.
Я читал, что люди называют их "абстрактными подклассами". Может кто-нибудь объяснить, почему?
Буду признателен, если вы объясните свой ответ на следующем примере (с одного из моих слайд-шоу):
![A C++ Mixin Example]()
Ответы
Ответ 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.