什么是Mixins(概念)


75

我试图让我了解Mixin概念,但是我似乎无法理解它的含义。我看到的是,这是一种通过使用继承来扩展类功能的方法。我读过人们将它们称为“抽象子类”。谁能解释为什么?

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


大量使用mixins的框架是用于Java Web应用程序的Apache Tapestry。阅读文档并查看Tapestry中的一些示例,也许您将能够看到与C ++示例中所见内容相似的模式。这里是一个链接:tapestry.apache.org/component-mixins.html
David Fleeman 2013年

我完全以为您只是在标题中谈论Ruby ...
texasbruce 2014年

Answers:


129

在进行混搭之前,先描述一下它要解决的问题很有用。假设您有许多要建模的想法或概念。它们可能以某种方式相关,但是它们在大多数情况下是正交的,这意味着它们可以彼此独立地站立。现在,您可以通过继承对其进行建模,并使每个概念都来自某个通用接口类。然后,在实现该接口的派生类中提供具体方法。

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

混入的想法是提供一堆原始类,它们中的每一个都建模一个基本的正交概念,并能够将它们组合在一起,以仅具有所需功能的方式组成更复杂的类,就像乐高玩具一样。原始类本身旨在用作构建块。这是可扩展的,因为稍后您可以将其他原始类添加到集合中,而不会影响现有的原始类。

回到C ++,实现此目的的一种技术是使用模板和继承。这里的基本思想是,通过template参数提供这些构建块,从而将它们连接在一起。然后,将它们链接在一起,例如。通过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*指针时获得mixin行为。
Prashant Kumar

2
保持基类setget虚拟是有意义的,因为您可以定义void doubler(Number &n){ n.set(n.get()*2); }和使用可撤销和可撤销的类
Roddy

7
一个建议:当Number::value_type已经被定义它可以(也应该)也可以用于Number::nNumber::getNumber::set
Miroslaw Opoka '16

根据我在这个岗位推理的理解,似乎推理的同一条线可以用来说,原生/基本类型任何编程语言(如intstd::stringchar,等在C ++中)本身的混入,对不对?
–'code_dredd

7

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

另一个示例(摘自C ++中基于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提供给定执行遍历操作的图类的计数顶点的功能。

通常,在C ++中,mixin是通过CRTP习惯用法实现的。该线程可以很好地了解C ++中的mixin实现:什么是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>
{
};

该混合器提供了异类复制到形状类集(层次结构)的功能。


7
这个例子根本不是CRTP。
lapk 2013年

1
但是我喜欢您提到CRTP的评论。因为在C ++中,mixin是CRTP的近似值。
lapk

1
任何人都有一个很好的现实生活中的例子,它很容易理解,我认为它可以很好地完成这个答案和评论集。
Jimmyt1988

1
@简单,谢谢,我已将其添加到答案中。您的示例提供了对类“可克隆”的功能,对吗?异类复制是正确的术语吗?我不确定。
Manu343726

1
@ Manu343726它允许您定义与该类具有is-a关系的shape类,但是派生自该类将cloneable_shape自动clone为您实现成员函数,因此您不必为每个派生的类自己编写它。
2013年

6

我喜欢greatwolf的回答,但请注意一点。

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

它的作用与接口相同,也许与抽象的作用相同,但接口更容易获得。

它解决了许多问题,但是我在开发中发现很多问题是外部api。想象一下。

您有一个用户数据库,该数据库具有某种访问其数据的方式。现在想象一下,如果您拥有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 ++。另外,mixins与普通抽象类有何不同?
2013年

嗯,我使用C#,PHP和C ++ ...实际上,我说它们与抽象非常相似,但是我发现很难解释它们的用法...考虑到我还没有大量使用它们:)。每当我尝试提出一个很好的继承示例时,我都会哭泣。该代码本来可以通过冗长的语法来演示,但不是真正的代码,伪我说过:)我希望有人能更清楚地回答,但我个人确实更喜欢冗长的答案,我发现它们也更有用,也是一个真实的示例^ _ ^
Jimmyt1988

好的,这让我感到奇怪。读完这篇文章后,我搜寻了网络,找不到合适的解释。我还是不明白什么是混入:/
BЈовић

我了解您的意图很好,但是您的解释使图片比以前更加模糊。更不用说您错过了“ in C ++”部分。
ashrasmun

0

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

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

是的,这是一篇不错的文章,但仅供参考。CRTP不是万能药。所谓的学术方法是这里,也(本质)基于CRTP。

在拒绝投票之前,也许考虑一下我在魔盒上的POC代码:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.