什么是Mixins(作为一个概念)

时间:2013-09-12 20:03:27

标签: c++ oop templates mixins

我正试图了解Mixin概念,但我似乎无法理解它是什么。 我看待它的方式是,它是一种通过使用继承来扩展类的功能的方法。 我读过人们将它们称为“抽象子类”。任何人都可以解释原因吗?

如果您根据以下示例(从我的一个演讲幻灯片中)解释您的答案,我将不胜感激: A C++ Mixin Example

6 个答案:

答案 0 :(得分:109)

在进入混合之前,描述它试图解决的问题是有用的。假设您有一堆想要建模的想法或概念。它们可能在某种程度上相关,但它们在大多数情况下是正交的 - 这意味着它们可以彼此独立地站立。现在,您可以通过继承对此进行建模,并将每个概念派生自一些常见的接口类。然后在实现该接口的派生类中提供具体方法。

这种方法的问题在于,这种设计没有提供任何清晰直观的方法来获取每个具体类并将它们组合在一起。

混合的想法是提供一堆原始类,其中每个类都模拟一个基本的正交概念,并且能够将它们组合在一起,只用你想要的功能组成更复杂的类 - 像legos。原始类本身旨在用作构建块。这是可扩展的,因为您可以在以后添加其他原始类而不影响现有类。

回到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可能不会像您预期的那样。

作为旁注,您可能还会发现this article有帮助。

答案 1 :(得分:7)

mixin是一个被设计用于为另一个类提供功能的类,通常通过指定的类提供功能所需的基本功能。例如,考虑一下你的例子:
在这种情况下,mixin提供了撤消值类的设置操作的功能。此可行性基于参数化类(在您的示例中为get/set类)提供的Number功能。

另一个例子(摘自"Mixin-based programming in 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);
  }
... 
};

在此示例中,mixin提供计算顶点的功能,给定执行trasversal操作的图形类。

通常,在C ++中,mixins是通过CRTP习语实现的。这个主题可以很好地理解C ++中的mixin实现:What is C++ Mixin-Style?

这是一个利用CRTP习语的mixin示例(感谢@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将异构副本的功能提供给形状类的集合(层次结构)。

答案 2 :(得分:5)

我喜欢大狼的回答,但会提出一点谨慎。

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”函数设为虚拟,将调用正确的覆盖,并且不会发生上述不一致的行为。

答案 3 :(得分:4)

C ++中的Mixins使用Curiously Recurring Template Pattern(CRTP)表示。 This post是对其他重用技术提供的优秀细分...编译时多态性。

答案 4 :(得分: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.

答案 5 :(得分:0)

要理解该概念,请暂时忘记课程。考虑(最受欢迎的)JavaScript。对象是方法和属性的动态数组。可通过其名称作为符号或字符串文字来调用。您将如何在2018年用标准C ++实现呢? 不容易。但这是概念的核心。在JavaScript中,您可以随时随地添加和删除(也称为混合)。非常重要:没有类继承。

现在使用C ++。标准C ++具有您所需的全部内容,在这里无助于说明。显然,我不会编写脚本语言来使用C ++实现混入。

是,this is a good article,但仅用于启发。 CRTP不是万能药。所谓的学术方法也是here,本质上也是基于CRTP的。

在否决此答案之前,请考虑一下我的p.o.c. code on wand box:)