refactory patrones patron metodo method los historia fabrica ejemplos diseño delegacion definicion decorador c++ stl

c++ - method - patrones de diseño metodo de fabrica



¿Está bien heredar la implementación de contenedores STL, en lugar de delegar? (7)

Aparte de los ditores virtuales, la decisión de heredar versus contener debe ser una decisión de diseño basada en la clase que está creando. Nunca debe heredar la funcionalidad de contenedor simplemente porque es más fácil que contener un contenedor y agregar algunas funciones de agregar y eliminar que parecen envoltorios simplistas a menos que pueda decir definitivamente que la clase que está creando es una especie de contenedor. Por ejemplo, una clase presencial a menudo contendrá objetos estudiantiles, pero un salón de clases no es una especie de lista de estudiantes para la mayoría de los propósitos, por lo que no debería heredar de la lista.

Tengo una clase que adapta std :: vector para modelar un contenedor de objetos específicos de dominio. Quiero exponer la mayor parte de la API std :: vector al usuario, para que él / ella pueda usar métodos conocidos (tamaño, claridad, en, etc.) y algoritmos estándar en el contenedor. Esto parece ser un patrón recurrente para mí en mis diseños:

class MyContainer : public std::vector<MyObject> { public: // Redeclare all container traits: value_type, iterator, etc... // Domain-specific constructors // (more useful to the user than std::vector ones...) // Add a few domain-specific helper methods... // Perhaps modify or hide a few methods (domain-related) };

Soy consciente de la práctica de preferir la composición a la herencia cuando reutilizo una clase para la implementación, ¡pero tiene que haber un límite! Si tuviera que delegar todo a std :: vector, ¡habría (por mi cuenta) 32 funciones de reenvío!

Entonces mis preguntas son ... ¿Realmente es tan malo heredar la implementación en tales casos? ¿Cuáles son los riesgos? ¿Hay alguna manera más segura de que pueda implementar esto sin tanta mecanografía? ¿Soy un hereje por usar herencia de implementación? :)

Editar:

¿Qué hay de dejar en claro que el usuario no debe usar MyContainer a través de un puntero std :: vector <>:

// non_api_header_file.h namespace detail { typedef std::vector<MyObject> MyObjectBase; } // api_header_file.h class MyContainer : public detail::MyObjectBase { // ... };

Las bibliotecas de impulso parecen hacer esto todo el tiempo.

Editar 2:

Una de las sugerencias fue usar funciones gratuitas. Lo mostraré aquí como pseudo-código:

typedef std::vector<MyObject> MyCollection; void specialCollectionInitializer(MyCollection& c, arguments...); result specialCollectionFunction(const MyCollection& c); etc...

Una forma más de OO de hacerlo:

typedef std::vector<MyObject> MyCollection; class MyCollectionWrapper { public: // Constructor MyCollectionWrapper(arguments...) {construct coll_} // Access collection directly MyCollection& collection() {return coll_;} const MyCollection& collection() const {return coll_;} // Special domain-related methods result mySpecialMethod(arguments...); private: MyCollection coll_; // Other domain-specific member variables used // in conjunction with the collection. }


Como todos ya han mencionado, los contenedores STL no tienen destructores virtuales, por lo que heredarlos no es seguro en el mejor de los casos. Siempre he considerado la programación genérica con plantillas como un estilo diferente de OO, uno sin herencia. Los algoritmos definen la interfaz que requieren. Es lo más parecido a Duck Typing que puede obtener en un lenguaje estático.

De todos modos, tengo algo que agregar a la discusión. La forma en que he creado mis propias especializaciones de plantilla anteriormente es definir clases como las siguientes para usar como clases base.

template <typename Container> class readonly_container_facade { public: typedef typename Container::size_type size_type; typedef typename Container::const_iterator const_iterator; virtual ~readonly_container_facade() {} inline bool empty() const { return container.empty(); } inline const_iterator begin() const { return container.begin(); } inline const_iterator end() const { return container.end(); } inline size_type size() const { return container.size(); } protected: // hide to force inherited usage only readonly_container_facade() {} protected: // hide assignment by default readonly_container_facade(readonly_container_facade const& other): : container(other.container) {} readonly_container_facade& operator=(readonly_container_facade& other) { container = other.container; return *this; } protected: Container container; }; template <typename Container> class writable_container_facade: public readable_container_facade<Container> { public: typedef typename Container::iterator iterator; writable_container_facade(writable_container_facade& other) readonly_container_facade(other) {} virtual ~writable_container_facade() {} inline iterator begin() { return container.begin(); } inline iterator end() { return container.end(); } writable_container_facade& operator=(writable_container_facade& other) { readable_container_facade<Container>::operator=(other); return *this; } };

Estas clases exponen la misma interfaz que un contenedor STL. Me gustó el efecto de separar las operaciones de modificación y no modificación en clases base distintas. Esto tiene un efecto realmente bueno en la corrección de const. El único inconveniente es que debe ampliar la interfaz si desea utilizarlos con contenedores asociativos. Aunque no me he encontrado con la necesidad.


En este caso, heredar es una mala idea: los contenedores STL no tienen destructores virtuales, por lo que podría encontrarse con fugas de memoria (además, es una indicación de que los contenedores STL no están destinados a ser heredados en primer lugar).

Si solo necesita agregar alguna funcionalidad, puede declararla en métodos globales, o una clase ligera con un puntero / referencia miembro de contenedor. Esto, por supuesto, no le permite ocultar los métodos: si eso es realmente lo que busca, entonces no hay otra opción que redeclar toda la implementación.


Es más fácil de hacer:

typedef std::vector<MyObject> MyContainer;


Los métodos de reenvío estarán alejados de todos modos. No obtendrás un mejor rendimiento de esta manera. De hecho, es probable que obtenga un peor rendimiento.


Puede combinar la herencia privada y la palabra clave ''usar'' para solucionar la mayoría de los problemas mencionados anteriormente: la herencia privada está ''implementada-en-términos-de'' y, como es privada, no puede contener un puntero a la clase base

#include <string> #include <iostream> class MyString : private std::string { public: MyString(std::string s) : std::string(s) {} using std::string::size; std::string fooMe(){ return std::string("Foo: ") + *this; } }; int main() { MyString s("Hi"); std::cout << "MyString.size(): " << s.size() << std::endl; std::cout << "MyString.fooMe(): " << s.fooMe() << std::endl; }


El riesgo es la desasignación a través de un puntero a la clase base ( eliminar , eliminar [] , y potencialmente otros métodos de desasignación). Como estas clases ( deque , mapa , cadena , etc.) no tienen dtors virtuales, es imposible limpiarlas correctamente con solo un puntero a esas clases:

struct BadExample : vector<int> {}; int main() { vector<int>* p = new BadExample(); delete p; // this is Undefined Behavior return 0; }

Dicho eso, si estás dispuesto a asegurarte de que nunca lo hagas accidentalmente, hay un pequeño inconveniente importante al heredarlos, pero en algunos casos eso es algo grande. Otros inconvenientes incluyen el choque con los detalles y las extensiones de la implementación (algunos de los cuales pueden no usar identificadores reservados) y el manejo de interfaces infladas ( cadena en particular). Sin embargo, la herencia está pensada en algunos casos, ya que los adaptadores de contenedor como la pila tienen un miembro protegido c (el contenedor subyacente que adaptan), y es casi solo accesible desde una instancia de clase derivada.

En lugar de herencia o composición, considere escribir funciones gratuitas que tomen un par de iteradores o una referencia de contenedor y operen sobre eso. Prácticamente todo <algorithm> es un ejemplo de esto; y make_heap , pop_heap y push_heap , en particular, son un ejemplo del uso de funciones gratuitas en lugar de un contenedor específico del dominio.

Por lo tanto, use las clases de contenedor para sus tipos de datos, y aún llame a las funciones gratuitas para su lógica específica de dominio. Pero aún puede lograr cierta modularidad usando un typedef, que le permite tanto simplificar su declaración como proporcionar un solo punto si parte de ellos necesita cambiarse:

typedef std::deque<int, MyAllocator> Example; // ... Example c (42); example_algorithm(c); example_algorithm2(c.begin() + 5, c.end() - 5); Example::iterator i; // nested types are especially easier

Observe que value_type y allocator pueden cambiar sin afectar el código posterior utilizando typedef, e incluso el contenedor puede cambiar de un deque a un vector .