¿Cuáles son los roles de los singletons, las clases abstractas y las interfaces?


13

Estoy estudiando OOP en C ++ y, aunque conozco las definiciones de estos 3 conceptos, realmente no puedo darme cuenta de cuándo o cómo usarlo.

Usemos esta clase para el ejemplo:

class Person{
    private:
             string name;
             int age;
    public:
             Person(string p1, int p2){this->name=p1; this->age=p2;}
             ~Person(){}

             void set_name (string parameter){this->name=parameter;}                 
             void set_age (int parameter){this->age=parameter;}

             string get_name (){return this->name;}
             int get_age (){return this->age;}

             };

1. Singleton

¿CÓMO funciona la restricción de la clase de tener un solo objeto?

¿PUEDE diseñar una clase que tenga SOLO 2 instancias? O tal vez 3?

¿CUÁNDO está usando un singleton recomendado / necesario? ¿Es una buena práctica?

2. Clase abstracta

Hasta donde sé, si solo hay una función virtual pura, la clase se vuelve abstracta. Entonces, agregando

virtual void print ()=0;

lo haría, ¿verdad?

¿POR QUÉ necesitarías una clase cuyo objeto no sea necesario?

3 interfaz

Si una interfaz es una clase abstracta en la que todos los métodos son funciones virtuales puras, entonces

¿Cuál es la principal diferencia entre los 2?

¡Gracias por adelantado!


2
Singleton es controvertido, haga una búsqueda en este sitio para obtener varias opiniones.
Winston Ewert

2
También vale la pena señalar que, si bien las clases abstractas son parte del lenguaje, ni los singletons ni las interfaces lo son. Son patrones que las personas implementan. Singleton en particular es algo que requiere un poco de piratería inteligente para que funcione. (Aunque, por supuesto, puede crear un singleton solo por convención.)
Gort the Robot

1
Uno a la vez Por favor.
JeffO

Respuestas:


17

1. Singleton

Usted restringe el número de instancias porque el constructor será privado, lo que significa que solo los métodos estáticos pueden crear instancias de esa clase (hay otros trucos sucios para lograrlo, pero no nos dejemos llevar).

Crear una clase que tendrá solo 2 o 3 instancias es perfectamente factible. Debería usar singleton siempre que sienta la necesidad de tener solo una instancia de esa clase en todo el sistema. Eso suele suceder con las clases que tienen un comportamiento de "gerente".

Si desea obtener más información sobre Singletons, puede comenzar en Wikipedia y particularmente para C ++ en esta publicación .

Definitivamente hay algunas cosas buenas y malas sobre este patrón, pero esta discusión pertenece a otro lugar.

2. Clases abstractas

Si, eso es correcto. Solo un único método virtual marcará la clase como abstracta.

Utilizará ese tipo de clases cuando tenga una jerarquía de clases más grande en la que las clases principales no deberían ser instanciadas realmente.

Supongamos que está definiendo una clase de Mamíferos y luego la hereda a Perro y Gato. Si lo piensa, no tiene mucho sentido tener una instancia pura de un mamífero, ya que primero necesita saber qué tipo de mamífero es realmente.

Potencialmente, hay un método llamado MakeSound () que solo tendrá sentido en las clases heredadas, pero no hay un sonido común que todos los mamíferos puedan hacer (es solo un ejemplo de no tratar de hacer un caso para los sonidos de los mamíferos aquí).

Eso significa que Mamífero debería ser una clase abstracta, ya que tendrá un comportamiento común implementado para todos los mamíferos, pero no se supone que se cree una instancia. Ese es el concepto básico detrás de las clases abstractas, pero definitivamente hay más que debes aprender.

3. Interfaces

No hay interfaces puras en C ++ en el mismo sentido que tiene en Java o C #. La única forma de crear uno es tener una clase abstracta pura que imite la mayoría del comportamiento que desea de una interfaz.

Básicamente, el comportamiento que está buscando es definir un contrato donde otros objetos puedan interactuar sin preocuparse por la implementación subyacente. Cuando haces una clase puramente abstracta, significa que toda la implementación pertenece a otro lugar, por lo que el propósito de esa clase es solo el contrato que define. Este es un concepto muy poderoso en OO y definitivamente deberías investigarlo más.

Puede leer sobre la especificación de interfaz para C # en MSDN para tener una mejor idea:

http://msdn.microsoft.com/en-us/library/ms173156.aspx

C ++ proporcionará el mismo tipo de comportamiento al tener una clase abstracta pura.


2
Una clase base abstracta pura le ofrece todo lo que hace una interfaz. Las interfaces existen en Java (y C #) porque los diseñadores de lenguaje querían evitar la herencia múltiple (debido a los dolores de cabeza que crea) pero reconocieron un uso muy común de la herencia múltiple que no es problemático.
Gort the Robot

@StevenBurnap: Pero no en C ++, que es el contexto de la pregunta.
DeadMG

3
Pregunta sobre C ++ y las interfaces. "Interfaz" no es una característica del lenguaje de C ++, pero las personas ciertamente crean interfaces en C ++ que funcionan exactamente como las interfaces de Java, utilizando clases base abstractas. Lo hicieron antes de que Java existiera.
Gort the Robot


1
Lo mismo es cierto de Singletons. En C ++, ambos son patrones de diseño, no características del lenguaje. Esto no significa que las personas no hablen sobre interfaces en C ++ y para qué sirven. El concepto de "interfaz" surgió de sistemas de componentes como Corba y COM, que fueron desarrollados originalmente para ser utilizados en C. pura. En C ++, las interfaces se implementan típicamente con clases base abstractas en las que todos los métodos son virtuales. La funcionalidad de esto es idéntica a la de una interfaz Java. Como tal, el concepto de una interfaz Java es intencionalmente un subconjunto de clases abstractas C ++.
Gort the Robot

8

La mayoría de las personas ya explicaron qué son las clases simples / abstractas. Con suerte, proporcionaré una perspectiva un poco diferente y daré algunos ejemplos prácticos.

Singletons: cuando desea que todos los códigos de llamada utilicen una sola instancia de variables, por cualquier motivo, tiene las siguientes opciones:

  • Variables globales: obviamente, sin encapsulación, la mayoría del código acoplado a globales ... malo
  • Una clase con todas las funciones estáticas, un poco mejor que simples globales, pero esta decisión de diseño aún lo lleva a una ruta en la que el código se basa en datos globales y podría ser muy difícil de cambiar más adelante. Además, no puede aprovechar OO cosas como el polimorfismo si todo lo que tiene son funciones estáticas
  • Singleton: aunque solo hay una instancia de la clase, la implementación real de la clase no tiene que saber nada sobre el hecho de que es global. Entonces, hoy puede tener una clase que es un singleton, mañana simplemente puede hacer público su constructor y dejar que los clientes creen instancias de múltiples copias. La mayoría del código de cliente que hace referencia al singleton no tendría que cambiar y la implementación del singleton en sí no tendrá que cambiar. El único cambio es cómo el código del cliente adquiere una referencia única en primer lugar.

De todas las opciones malas y malas que existen, si necesita datos globales, Singleton es un enfoque MUCHO mejor que cualquiera de los dos anteriores. También le permite mantener sus opciones abiertas si mañana cambia de opinión y decide utilizar la inversión de control en lugar de tener datos globales.

Entonces, ¿dónde usarías un singleton? Aquí hay algunos ejemplos:

  • Registro: si desea que todo su proceso tenga un solo registro, puede crear un objeto de registro y pasarlo a todas partes. Pero, ¿qué sucede si tiene 100,000k líneas de código de aplicación heredado? modificar todos ellos? O simplemente puede presentar lo siguiente y comenzar a usarlo en cualquier lugar que desee:

    CLog::GetInstance().write( "my log message goes here" );
  • Caché de conexión del servidor: esto fue algo que tuve que introducir en nuestra aplicación. Nuestra base de código, y había una gran cantidad, solía conectarse a los servidores cuando lo deseaba. La mayoría de las veces esto estaba bien, a menos que hubiera algún tipo de latencia en la red. Necesitábamos una solución y el rediseño de una aplicación de 10 años no estaba realmente sobre la mesa. Escribí un singleton CServerConnectionManager. Luego busqué a través del código y reemplacé las llamadas CoCreateInstanceWithAuth con una llamada de firma idéntica que invocó a mi clase. Ahora, después del primer intento, la conexión se almacenó en caché y el resto del tiempo los intentos de "conexión" fueron instantáneos. Algunos dicen que los solteros son malvados. Yo digo que me salvaron el trasero.

  • Para la depuración, a menudo encontramos que la tabla global de objetos en ejecución es muy útil. Tenemos algunas clases que nos gustaría seguir. Todos derivan de la misma clase base. Durante la creación de instancias, llaman a la tabla de objetos singleton y se registran. Cuando son destruidos, se anulan el registro. Puedo acercarme a cualquier máquina, adjuntarme a un proceso y crear una lista de objetos en ejecución. He estado en el producto durante más de media década y nunca sentí que alguna vez necesitáramos 2 tablas de objetos "globales".

  • Tenemos algunas clases de utilidad de analizador de cadenas relativamente complejas que se basan en expresiones regulares. Las clases de expresiones regulares deben inicializarse antes de que pueda realizar coincidencias. La inicialización es algo costosa porque es cuando se genera un FSM basado en una cadena de análisis. Sin embargo, después de eso, 100 subprocesos pueden acceder de forma segura a la clase de expresión regular porque una vez construida, FSM nunca cambia. Estas clases de analizador interno usan singletons para asegurarse de que esta inicialización ocurra solo una vez. Esto mejoró significativamente el rendimiento y nunca causó ningún problema debido a los "singletons malvados".

Habiendo dicho todo esto, debe tener en cuenta cuándo y dónde usar singletons. 9 de cada 10 veces hay una mejor solución y, por supuesto, debes usarla. Sin embargo, hay momentos en que singleton es absolutamente la elección de diseño correcta.

Tema siguiente ... interfaces y clases abstractas. Primero, como otros han mencionado, la interfaz ES una clase abstracta, pero va más allá al hacer cumplir que NO tiene absolutamente ninguna implementación. En algunos idiomas, la palabra clave de la interfaz es parte del idioma. En C ++ simplemente usamos clases abstractas. Microsoft VC ++ dio un paso para definir esto internamente en algún lugar:

typedef struct interface;

... así que aún puede usar la palabra clave de la interfaz (incluso se resaltará como una palabra clave 'real'), pero en lo que respecta al compilador real, es solo una estructura.

Entonces, ¿dónde usarías esto? Volvamos a mi ejemplo de una tabla de objetos en ejecución. Digamos que la clase base tiene ...

impresión virtual vacía () = 0;

Ahí está tu clase abstracta. Las clases que usan la tabla de objetos de tiempo de ejecución derivarán de la misma clase base. La clase base contiene un código común para registrar / cancelar el registro. Pero nunca será instanciado por sí mismo. Ahora puedo tener clases derivadas (por ejemplo, solicitudes, oyentes, objetos de conexión del cliente ...), cada uno implementará print () para que cuando se adjunte al proceso y pregunte qué está ejecutándose, cada objeto informará su propio estado.

Los ejemplos de interfaces / clases abstractas son innumerables y definitivamente los usa (o debería usar) mucho, con mucha más frecuencia que usaría singletons. En resumen, le permiten escribir código que funciona con tipos base y no está vinculado a la implementación real. Esto le permite modificar la implementación más adelante sin tener que cambiar demasiado código.

Aquí hay otro ejemplo. Digamos que tengo una clase que implementa un registrador, CLog. Esta clase escribe en el archivo en el disco local. Comienzo a usar esta clase en mi legado de 100,000 líneas de código. Por todo el lugar. La vida es buena hasta que alguien diga, oye, escriba en la base de datos en lugar de un archivo. Ahora creo una nueva clase, llamémosle CDbLog y escriba en la base de datos. ¿Puede imaginarse la molestia de atravesar 100,000 líneas y cambiar todo, desde CLog a CDbLog? Alternativamente, podría tener:

interface ILogger {
    virtual void write( const char* format, ... ) = 0;
};

class CLog : public ILogger { ... };

class CDbLog : public ILogger { ... };

class CLogFactory {
    ILogger* GetLog();
};

Si todo el código usara la interfaz ILogger, todo lo que tendría que cambiar es la implementación interna de CLogFactory :: GetLog (). El resto del código funcionaría automáticamente sin que yo tuviera que mover un dedo.

Para obtener más información sobre interfaces y un buen diseño de OO, recomendaría encarecidamente los Principios, patrones y prácticas ágiles del tío Bob en C # . El libro está lleno de ejemplos que usan abstracciones y proporciona explicaciones en lenguaje claro de todo.


4

¿CUÁNDO está usando un singleton recomendado / necesario? ¿Es una buena práctica?

Nunca. Peor que eso, son una perra absoluta de la que deshacerse, por lo que cometer este error una vez puede perseguirlo durante muchos, muchos años.

La diferencia entre las clases abstractas y las interfaces no es absolutamente nada en C ++. Generalmente tiene interfaces para especificar algún comportamiento de la clase derivada, pero sin tener que especificarlo todo. Esto hace que su código sea más flexible, ya que puede intercambiar cualquier clase que cumpla con las especificaciones más limitadas. Las interfaces en tiempo de ejecución se utilizan cuando necesita una abstracción en tiempo de ejecución.


Las interfaces son un subconjunto de clases abstractas. Una interfaz es una clase abstracta sin métodos definidos. (Una clase abstracta sin código).
Gort the Robot

1
@StevenBurnap: Quizás en algún otro idioma.
DeadMG

44
"Interfaz" es solo una convención en C ++. Cuando lo he visto usado, es una clase abstracta con solo métodos virtuales puros y sin propiedades. Obviamente, puedes escribir cualquier clase antigua y dar una "I" delante del nombre.
Gort the Robot

Esta es la forma en que esperaba que la gente respondiera esta publicación. Una pregunta a la vez. De todos modos, muchas gracias por compartir sus conocimientos. Vale la pena invertir tiempo en esta comunidad.
appoll

3

Singleton es útil cuando no desea copias múltiples de un objeto en particular, solo debe haber una instancia de esa clase: se usa para objetos que mantienen el estado global, tienen que tratar con código no reentrante de alguna manera, etc.

Un singleton que tiene un número fijo de 2 o más instancias es un multitón , piense en la agrupación de conexiones de bases de datos, etc.

La interfaz especifica una API bien definida que ayuda a modelar la interacción entre objetos. En algunos casos, podría tener un grupo de clases que tienen alguna funcionalidad común ; de ser así, en lugar de duplicarlo en implementaciones, puede agregar definiciones de métodos a la interfaz convirtiéndola en una clase abstracta .

Incluso puede tener una clase abstracta donde se implementen todos los métodos, pero la marca como abstracta para indicar que no se debe usar como está sin subclases.

Nota: La interfaz y la clase abstracta no son muy diferentes en el mundo de C ++ con herencia múltiple, etc., pero tienen diferentes significados en Java et al.


¡Muy bien dicho! +1
jmort253

3

Si te detienes a pensarlo, se trata del polimorfismo. Desea poder escribir una pieza de código una vez que pueda hacer más de lo que se piensa dependiendo de lo que le pase.

Digamos que tenemos una función como el siguiente código de Python:

function foo(objs):
    for obj in objs:
        obj.printToScreen()

class HappyWidget:
    def printToScreen(self):
        print "I am a happy widget"

class SadWidget:
    def printToScreen(self):
        print "I am a sad widget"

Lo bueno de esta función es que podrá manejar cualquier lista de objetos, siempre que esos objetos implementen un método "printToScreen". Puede pasarle una lista de widgets felices, una lista de widgets tristes o incluso una lista que tenga una combinación de ellos y la función foo aún podrá hacer lo correcto.

Nos referimos a esta restricción de tipo de necesidad de tener un conjunto de métodos implementados (en este caso, printToScreen) como una interfaz y se dice que los objetos que implementan todos los métodos implementan la interfaz.

Si estuviéramos hablando de un lenguaje dinámico de tipo pato como Python, básicamente ya habríamos terminado. Sin embargo, el sistema de tipo estático de C ++ exige que asignemos una clase a los objetos en nuestra función y solo podrá trabajar con subclases de esa clase inicial.

void foo( Printable *objs[], int n){ //Please correctme if I messed up on the type signature
    for(int i=0; i<n; i++){
        objs[i]->printToScreen();
    }
}

En nuestro caso, la única razón por la que existe la clase Printable es para dar lugar al método printToScreen. Como no existe una implementación compartida entre las clases que implementan el método printToScreen, tiene sentido convertir Printable en una clase abstracta que solo se usa como una forma de agrupar clases similares en una jerarquía común.

En C ++, la clase absctract y los conceptos de interfaz son un poco borrosos. Si desea definirlos mejor, las clases abstractas son lo que está pensando, mientras que las interfaces generalmente significan la idea más general y en varios idiomas del conjunto de métodos visibles que expone un objeto. (Aunque algunos lenguajes, como Java, usan el término de interfaz para referirse a algo más directamente como una clase base abstracta)

Básicamente, las clases concretas especifican cómo se implementan los objetos, mientras que las clases abstractas especifican cómo interactúan con el resto del código. Para hacer que sus funciones sean más polimórficas, debe intentar recibir un puntero a la superclase abstracta siempre que tenga sentido hacerlo.


En cuanto a los Singleton, en realidad son bastante inútiles, ya que a menudo pueden ser reemplazados por solo un grupo de métodos estáticos o funciones antiguas simples. Sin embargo, a veces tienes algún tipo de restricción que te obliga a usar un objeto, aunque realmente no quieras usar uno, por lo que el patrón singleton es apropiado.


Por cierto, algunas personas podrían haber comentado que la palabra "interfaz" tiene un significado particular en el lenguaje Java. Sin embargo, creo que es mejor seguir con la definición más general por ahora.


1

Interfaces

Es difícil entender el propósito de una herramienta que resuelve un problema que nunca ha tenido. No entendí las interfaces por un tiempo después de que comencé a programar. Entendí lo que hicieron, pero no sabía por qué querrías usar uno.

Aquí está el problema: sabes lo que quieres hacer, pero tienes varias formas de hacerlo, o puedes cambiar la forma en que lo haces más tarde. Sería bueno si pudieras desempeñar el papel del administrador despistado: ladrar algunas órdenes y obtener los resultados que deseas sin preocuparte por cómo se hace.

Digamos que tiene un pequeño sitio web y guarda toda la información de sus usuarios en un archivo csv. No es la solución más sofisticada, pero funciona lo suficientemente bien como para almacenar los detalles de usuario de su madre. Más tarde, su sitio despega y tiene 10.000 usuarios. Tal vez es hora de usar una base de datos adecuada.

Si fue inteligente al principio, lo habría visto venir y no habría realizado las llamadas para guardar directamente en csv. En cambio, pensaría en lo que necesitaba que hiciera, sin importar cómo se implementó. Digamos store()y retrieve(). Haces una Persisterinterfaz con los métodos abstractos para store()y retrieve()y crear una CsvPersistersubclase que realmente implementa estos métodos.

Más tarde, puede crear una DbPersisterque implemente el almacenamiento y la recuperación de datos de manera completamente diferente de cómo lo hizo su clase csv.

Lo mejor es que todo lo que tienes que hacer ahora es cambiar

Persister* prst = new CsvPersister();

a

Persister* prst = new DbPersister();

y luego has terminado. Sus llamadas prst.store()y prst.retrieve()todo seguirá funcionando, simplemente se manejan de manera diferente "detrás de escena".

Ahora, aún tenía que crear las implementaciones de cvs y db, por lo que aún no ha experimentado el lujo de ser el jefe. Los beneficios reales son evidentes cuando usas interfaces que alguien más creó. Si otra persona fue tan amable de crear una CsvPersister()y DbPersister()ya, entonces sólo hay que escoger uno y llamar a los métodos necesarios. Si decide usar el otro más tarde, o en otro proyecto, ya sabe cómo funciona.

Estoy realmente oxidado en mi C ++, así que solo usaré algunos ejemplos de programación genéricos. Los contenedores son un gran ejemplo de cómo las interfaces le hacen la vida más fácil.

Usted puede tener Array, LinkedList, BinaryTree, etc. todas las subclases de Containerque tiene métodos como insert(), find(), delete().

Ahora, al agregar algo al medio de una lista vinculada, ni siquiera tiene que saber qué es una lista vinculada. Simplemente llame myLinkedList->insert(4)y mágicamente iterará a través de la lista y la mantendrá allí. Incluso si sabe cómo funciona una lista vinculada (que realmente debería), no tiene que buscar sus funciones específicas, porque probablemente ya sepa cuáles son al usar una diferente Containerantes.

Clases abstractas

Las clases abstractas son bastante similares a las interfaces (bueno, técnicamente las interfaces son clases abstractas, pero aquí me refiero a las clases base que tienen algunos de sus métodos desarrollados.

Digamos que estás creando un juego y necesitas detectar cuándo los enemigos están a una distancia sorprendente del jugador. Puede crear una clase base Enemyque tenga un método inRange(). Aunque hay muchas cosas sobre los enemigos que son diferentes, el método utilizado para verificar su alcance es consistente. Por lo tanto, su Enemyclase tendrá un método desarrollado para verificar el alcance, pero métodos virtuales puros para otras cosas que no comparten similitudes entre los tipos de enemigos.

Lo bueno de esto es que si arruinas el código de detección de rango o quieres modificarlo, solo tienes que cambiarlo en un solo lugar.

Por supuesto, hay muchas otras razones para las interfaces y las clases base abstractas, pero esas son algunas de las razones por las que podría usarlas.

Singletons

Los uso ocasionalmente y nunca me han quemado. Eso no quiere decir que no arruinarán mi vida en algún momento, según las experiencias de otras personas.

Aquí hay una buena discusión sobre el estado global de algunas personas más experimentadas y cautelosas: ¿Por qué el estado global es tan malvado?


1

En el reino animal hay varios animales que son mamíferos. Aquí el mamífero es una clase base y varios animales se derivan de él.

¿Alguna vez has visto un mamífero caminando? Sí, muchas veces estoy seguro, sin embargo, eran todos los tipos de mamíferos, ¿no?

Nunca has visto algo que era literalmente solo un mamífero. Eran todos los tipos de mamíferos.

Se requiere que la clase mamífero defina varias características y grupos, pero no existe como entidad física.

Por lo tanto, es una clase base abstracta.

¿Cómo se mueven los mamíferos? ¿Caminan, nadan, vuelan, etc.?

No hay forma de saber a nivel de los mamíferos, pero todos los mamíferos deben moverse de alguna manera (digamos que esta es una ley biológica para facilitar el ejemplo).

Por lo tanto, MoveAround () es una función virtual, ya que cada mamífero que se deriva de esta clase necesita poder implementarlo de manera diferente.

Sin embargo, como todos los mamíferos DEBEN definir MoveAround porque todos los mamíferos deben moverse y es imposible hacerlo a nivel de mamífero. Debe ser implementado por todas las clases secundarias, pero allí no tiene significado en la clase base.

Por lo tanto, MoveAround es una función virtual pura.

Si tiene una clase completa que permite la actividad pero no puede definir en el nivel superior cómo debe hacerse, entonces todas las funciones son virtuales y esta es una interfaz.
Por ejemplo, si tenemos un juego en el que codificarás un robot y me lo enviarás para pelear en un campo de batalla, necesito saber los nombres de las funciones y los prototipos a los que llamar. No me importa cómo lo implemente de su lado, siempre que la 'interfaz' esté clara. Por lo tanto, puedo proporcionarle una clase de interfaz de la que derivará para escribir su robot asesino.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.