c++ - tag - ¿Qué son Mixins(como concepto)
title tag html (6)
Estoy tratando de entender el concepto de Mixin, pero parece que no puedo entender de qué se trata. La forma en que lo veo es que es una forma de expandir las capacidades de una clase mediante el uso de la herencia. He leído que las personas se refieren a ellos como "subclases abstractas". ¿Alguien puede explicar por qué?
Agradecería que explicaras tu respuesta en función del siguiente ejemplo (de una de mis presentaciones de diapositivas):
Antes de entrar en lo que es una mezcla, es útil describir los problemas que está tratando de resolver. Digamos que tiene un montón de ideas o conceptos que está tratando de modelar. Pueden estar relacionados de alguna manera, pero son ortogonales en su mayor parte, lo que significa que pueden mantenerse independientes unos de otros. Ahora puede modelar esto a través de la herencia y hacer que cada uno de esos conceptos derive de alguna clase de interfaz común. Luego, proporciona métodos concretos en la clase derivada que implementa esa interfaz.
El problema con este enfoque es que este diseño no ofrece ninguna forma clara e intuitiva de tomar cada una de esas clases concretas y combinarlas juntas.
La idea de mezclar es proporcionar un conjunto de clases primitivas, donde cada una de ellas modela un concepto ortogonal básico, y ser capaz de unirlas para componer clases más complejas con la funcionalidad que desee, algo así como legos. Las clases primitivas en sí mismas están destinadas a ser usadas como bloques de construcción. Esto es extensible ya que más adelante puede agregar otras clases primitivas a la colección sin afectar las existentes.
Volviendo a C ++, una técnica para hacer esto es usar plantillas y herencia. La idea básica aquí es conectar estos bloques de construcción juntos proporcionándolos a través del parámetro de la plantilla. Luego los encadenas, por ejemplo. a través de typedef
, para formar un nuevo tipo que contenga la funcionalidad que desee.
Tomando su ejemplo, supongamos que queremos agregar una funcionalidad de rehacer en la parte superior. Así es como se verá:
#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
}
Notarás que hice algunos cambios desde tu original:
- Las funciones virtuales realmente no son necesarias aquí porque sabemos exactamente cuál es nuestro tipo de clase compuesto en tiempo de compilación.
- He agregado un
value_type
predeterminado para el segundo param de plantilla para que su uso sea menos engorroso. De esta forma, no tiene que seguir escribiendo<foobar, int>
cada vez que pega una pieza. - En lugar de crear una nueva clase que herede de las piezas, se usa un
typedef
simple.
Tenga en cuenta que esto pretende ser un ejemplo simple para ilustrar la idea de mezclar. Por lo tanto, no tiene en cuenta los casos de esquina y los usos graciosos. Por ejemplo, realizar un undo
sin establecer un número probablemente no se comporte como es de esperar.
Como nota al margen, también puede encontrar útil este artículo .
Esto funciona igual que una interfaz y tal vez más como un resumen, pero las interfaces son más fáciles de obtener por primera vez.
Aborda muchos problemas, pero uno que encuentro en el desarrollo que surge mucho es el apis externo. imagina esto.
Usted tiene una base de datos de usuarios, esa base de datos tiene una forma determinada de obtener acceso a sus datos. ahora imagine que tiene Facebook, que también tiene una cierta forma de obtener acceso a sus datos (api).
en cualquier punto, es posible que su aplicación deba ejecutarse utilizando datos de Facebook o su base de datos. entonces, lo que debes hacer es crear una interfaz que diga "cualquier cosa que me implemente definitivamente tendrá los siguientes métodos" ahora puedes implementar esa interfaz en tu aplicación ...
debido a que una interfaz promete que los repositorios de implementación tendrán los métodos declarados en ellos, usted sabe que siempre y cuando use esa interfaz en su aplicación, si cambia los datos, siempre tendrá los métodos que está definiendo y, por lo tanto, tendrá datos para trabajar fuera de.
Hay muchas más capas para este patrón de trabajo, pero la esencia es que es bueno porque los datos u otros elementos persistentes se convierten en una gran parte de su aplicación, y si cambian sin que usted lo sepa, su aplicación puede romperse :)
Aquí hay un pseudo código.
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.
Me gusta la respuesta de greatwolf, pero ofrecería un punto de precaución.
greatwolf afirmó: "Las funciones virtuales realmente no son necesarias aquí porque sabemos exactamente cuál es nuestro tipo de clase compuesto en tiempo de compilación". Desafortunadamente, puede encontrarse con un comportamiento incoherente si usa su objeto polimórficamente.
Permítanme ajustar la función principal de su ejemplo:
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!
}
Al hacer que la función "establecer" sea virtual, se invocará la anulación correcta y no se producirá el comportamiento inconsistente anterior.
Mixins en C ++ se expresan usando el CRTP (CRTP). Esta publicación es un desglose excelente de lo que proporcionan sobre otras técnicas de reutilización ... polimorfismo en tiempo de compilación.
Para comprender el concepto, olvida las clases por un momento. Piensa (el más popular) JavaScript. Donde los objetos son matrices dinámicas de métodos y propiedades. Se puede llamar por su nombre como un símbolo o como un literal de cadena. ¿Cómo implementaría eso en C ++ estándar en un año 2018? No es facil Pero ese es el núcleo del concepto. En JavaScript, uno puede agregar y eliminar (también conocido como mix-in) siempre que lo desee. Muy importante: sin herencia de clase.
Ahora en C ++. El estándar C ++ tiene todo lo que necesita, no ayuda como una declaración aquí. Obviamente, no escribiré un lenguaje de scripting para implementar mix-in usando C ++.
Sí, este es un buen artículo , pero solo para inspiración. CRTP no es una panacea. Y también está here el llamado enfoque académico, también (en esencia) basado en CRTP.
Antes de votar negativamente esta respuesta, quizás considere mi código de poc en la caja de varitas :)
Una mezcla es una clase diseñada para proporcionar funcionalidad para otra clase, normalmente a través de una clase específica que proporciona las características básicas que la funcionalidad necesita. Por ejemplo, considere su ejemplo:
La mezcla en este caso proporciona la funcionalidad de deshacer la operación establecida de una clase de valor. Esta habilidad se basa en la funcionalidad get/set
proporcionada por una clase parametrizada (la clase Number
, en su ejemplo).
Otro ejemplo (Extraído de " Programación basada en Mixin en 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);
}
...
};
En este ejemplo, la mezcla proporciona la funcionalidad de contar vértices , dada una clase de gráfico que realiza operaciones de trasversal.
Comúnmente, en C ++ mixins se implementan a través del idioma CRTP . Este hilo podría ser una buena lectura sobre una implementación de mixin en C ++: ¿Qué es C ++ Mixin-Style?
Aquí hay un ejemplo de mixin que aprovecha el idioma CRTP (gracias a @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>
{
};
Esta mezcla proporciona la funcionalidad de copia heterogénea para un conjunto (jerarquía) de clases de formas.