Що таке міксини (як концепція)


76

Я намагаюся зрозуміти концепцію Mixin, але, здається, я не можу зрозуміти, що це таке. Я бачу це так, що це спосіб розширити можливості класу за допомогою спадкування. Я читав, що люди називають їх "абстрактними підкласами". Хтось може пояснити, чому?

Буду вдячний, якщо б ви пояснили свою відповідь на наступному прикладі (З одного з моїх слайд-шоу з лекцій): Приклад змішування C ++


Фреймворком, який активно використовує міксини, є Apache Tapestry для веб-додатків Java. Прочитайте документацію та перегляньте кілька прикладів у Tapestry, і, можливо, ви зможете побачити паралелі / шаблони того, що ви бачите в прикладі C ++. Ось посилання: tapestry.apache.org/component-mixins.html
David Фліман

Я цілком подумав, що ви говорите про Рубі лише одним поглядом на назву ...
texasbruce

Відповіді:


130

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

Проблема такого підходу полягає в тому, що ця конструкція не пропонує жодного чіткого інтуїтивного способу взяти кожен із цих конкретних класів та поєднати їх разом.

Ідея поєднань полягає в тому, щоб забезпечити купу примітивних класів, де кожен з них моделює базову ортогональну концепцію, і мати можливість поєднати їх, щоб скласти більш складні класи з просто необхідною функціональністю - начебто лего. Самі примітивні класи призначені для використання в якості будівельних блоків. Це можна розширити, оскільки згодом ви можете додавати інші примітивні класи до колекції, не впливаючи на існуючі.

Повертаючись до C ++, методикою для цього є використання шаблонів та успадкування. Основна ідея тут полягає в тому, що ви з’єднуєте ці будівельні блоки разом, надаючи їх через параметр шаблону. Потім ви зв’язуєте їх ланцюгом, наприклад. через typedef, щоб сформувати новий тип, що містить потрібну функціональність.

На вашому прикладі скажімо, що ми хочемо додати функцію повтору зверху. Ось як це може виглядати:

#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не встановивши жодного числа, швидше за все, буде вести себе не так, як можна було б очікувати.

Як анотація, ця стаття також може бути вам корисною.


7
Цей приклад насправді дуже хороший, я насправді його прочитав і був здивований, виявивши, що він має багато сенсу лолол. Молодці товаришу. Дякую.
Jimmyt1988

12
Примітка для читачів, void Number::set(int)і int Number::get() constобидва повинні мати virtualна увазі поведінку змішування при використанні Number*вказівника.
Прашант Кумар

2
Зберігати базовий клас setта getвіртуальний має сенс, оскільки тоді ви можете визначити void doubler(Number &n){ n.set(n.get()*2); }та мати можливість використовувати саме для класів, що не підлягають повторному та повторному відновленню
Родді

7
Порада: коли Number::value_typeце вже визначено, його можна (і потрібно) також використовувати для Number::n, Number::getта Number::set.
Мірослав Опока,

Грунтуючись на моєму розумінні міркування в цьому пості, здається , та ж сама лінія міркувань може бути використана , щоб сказати , що рідні / примітивні типи будь-якої мови програмування (наприклад int, std::string, char, і т.д. в C ++) самі Домішки, НЕ так?
code_dredd

7

Mixin - це клас, призначений для забезпечення функціональності іншого класу, як правило, через вказаний клас, який забезпечує основні функції, які потрібні цій функціональності. Наприклад, розглянемо ваш приклад:
Mixin у цьому випадку надає функцію скасування встановленої операції класу значення. Ця здатність базується на get/setфункціональності, яку надає параметризований клас ( Numberклас у вашому прикладі).

Інший приклад (Витяг з " Програмування на основі Mixin в C ++ " ):

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);
  }
... 
};

У цьому прикладі мікшин надає функціонал підрахунку вершин із заданим класом графіка, який виконує операції обертання.

Як правило, в C ++ Домішки реалізуються через CRTP ідіоми. Цей потік може бути гарним прочитанням про реалізацію міксіну в C ++: Що таке C ++ Mixin-Style?

Ось приклад комбінації, яка використовує переваги ідіоми 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>
{
};

Цей мікшин забезпечує функціональність різнорідної копії набору (ієрархії) класів фігур.


7
Приклад - це взагалі не CRTP.
lapk

1
Але мені сподобався ваш коментар стосовно CRTP. Оскільки міксини наближені до CRTP у C ++.
lapk

1
Будь-хто отримав приємний приклад із реального життя, який легко зрозуміти, я б гарно закінчив цю відповідь та набір коментарів.
Jimmyt1988

1
@ Просто, дякую, я додав це до відповіді. Ваш приклад надає можливість "бути клонованим" для класу, так? Гетерогенна копія - правильний термін? Я не впевнений.
Manu343726

1
@ Manu343726 Це дозволяє визначати класи з є- відносини з shapeкласом, але випливають з cloneable_shapeавтоматично реалізує cloneфункцію - член для вас, так що вам не доведеться писати його самостійно для кожного класу виводите.
Простий

6

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

greatwolf заявив: "Віртуальні функції насправді тут не потрібні, тому що ми точно знаємо, який тип нашого складеного класу є під час компіляції". На жаль, ви можете зіткнутися з деякою непослідовною поведінкою, якщо використовувати свій об’єкт поліморфно.

Дозвольте мені налаштувати основну функцію на його прикладі:

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" віртуальною, буде викликано належне перевизначення, і суперечлива поведінка вище не відбудеться.



0

Це працює так само, як інтерфейс і, можливо, більше як абстракт, але інтерфейси легше отримати в перший раз.

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

У вас є база даних користувачів, ця база даних має певний спосіб отримати доступ до своїх даних. тепер уявіть, що у вас є facebook, який також має певний спосіб отримати доступ до своїх даних (api).

у будь-який момент вашій програмі може знадобитися запустити, використовуючи дані з facebook або вашої бази даних. отже, що ви робите, це створюєте інтерфейс, який говорить, що "все, що мене реалізує, точно матиме такі методи", тепер ви можете впровадити цей інтерфейс у свою програму ...

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

У цій схемі роботи є набагато більше шарів, але суть полягає в тому, що це добре, тому що дані або інші такі постійні елементи стають важливою частиною вашої програми, і якщо вони змінюються без вашого відома, ваша програма може зламатися :)

Ось псевдокод.

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.

4
Ваш код виглядає як Java, а не як c ++. Крім того, чим міксини відрізняються від звичайних абстрактних класів?
BЈовић

ну ну, я роблю C #, PHP і C ++ ... Я насправді сказав, що вони дуже схожі на абстрактні, але мені важче пояснити їх використання ... враховуючи, що я ще не використовував їх багато-багато :). Щоразу, коли я намагаюся придумати хороший приклад успадкування, я змушую себе плакати. Код мав бути простіше продемонструвати за допомогою багатослівного синтаксису, але це не справжній код, псевдо, я сказав :) Я сподіваюся, хтось відповідає на нього набагато чіткіше, але мені особисто подобаються більше багатослівні відповіді, я вважаю їх більш корисними та хорошим прикладом із реального життя ^ _ ^
Jimmyt1988

Гаразд, це змусило мене задуматися. Прочитавши це, я шукав у мережі, і не міг знайти гідного пояснення. Я все ще не розумію, що таке міксини: /
BЈовић

Я розумію, що у вас є добрі наміри, але ваше пояснення робить картину більш розмитою, ніж це було раніше. Не кажучи вже про той факт, що ви пропустили частину "в C ++".
ашрасмун,

0

Щоб зрозуміти концепцію, на мить забудьте про заняття. Подумайте (найпопулярніший) JavaScript. Де об’єкти - це динамічні масиви методів та властивостей. Викликається за їхньою назвою як символ або як літеральний рядок. Як би ви застосували це у стандартному C ++ у 2018 році? Непросто . Але це суть концепції. У JavaScript можна додавати та видаляти (він же мікс), коли завгодно і коли завгодно. Дуже важливо: відсутність успадкування класу.

Тепер на C ++. Стандартний C ++ має все, що вам потрібно, не допомагає як твердження тут. Очевидно, що я не буду писати мову сценаріїв для того, щоб реалізувати змішування за допомогою C ++.

Так, це гарна стаття , але лише для натхнення. CRTP не є панацеєю. А також так званий академічний підхід тут , а також (по суті) на основі CRTP.

Перед голосуванням за цю відповідь, можливо, розгляньте мій код поку на коробці палички :)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.