Quais são os papéis dos singletons, classes abstratas e interfaces?


13

Estou estudando OOP em C ++ e, embora esteja ciente das definições desses 3 conceitos, não consigo realmente perceber quando ou como usá-lo.

Vamos usar esta classe para o exemplo:

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

Como funciona a restrição da classe de ter apenas um objeto?

Você pode criar uma classe que teria APENAS 2 instâncias? Ou talvez 3?

QUANDO está usando um singleton recomendado / necessário? É uma boa prática?

2. Classe Abstrata

Até onde eu sei, se houver apenas uma função virtual pura, a classe se tornará abstrata. Então, adicionando

virtual void print ()=0;

faria isso, certo?

POR QUE você precisaria de uma classe cujo objeto não é necessário?

3.Interface

Se uma interface é uma classe abstrata na qual todos os métodos são funções virtuais puras, então

Qual é a principal diferença entre os dois?

Desde já, obrigado!


2
Singleton é controverso, faça uma pesquisa neste site para obter várias opiniões.
Winston Ewert

2
Também vale a pena notar que, enquanto as classes abstratas fazem parte da linguagem, nem os singletons nem as interfaces fazem. São padrões que as pessoas implementam. Singleton em particular é algo que requer um pouco de hackers inteligentes para dar certo. (Embora, é claro, você possa criar um singleton apenas por convenção.)
Gort the Robot

1
Um de cada vez, por favor.
JeffO 8/12

Respostas:


17

1. Singleton

Você restringe o número de instâncias porque o construtor será privado, o que significa que apenas métodos estáticos podem criar instâncias dessa classe (na verdade, existem outros truques sujos para fazer isso, mas não vamos nos deixar levar).

Criar uma classe que terá apenas 2 ou 3 instâncias é perfeitamente viável. Você deve usar o singleton sempre que sentir a necessidade de ter apenas uma instância dessa classe em todo o sistema. Isso geralmente acontece com classes que têm um comportamento de 'gerente'.

Se você quiser saber mais sobre Singletons, pode começar na Wikipedia e, particularmente, em C ++ neste post .

Definitivamente, existem algumas coisas boas e ruins sobre esse padrão, mas essa discussão pertence a outro lugar.

2. Classes Abstratas

Sim está certo. Somente um único método virtual marcará a classe como abstrata.

Você usará esse tipo de classe quando tiver uma hierarquia de classe maior na qual as principais classes não devem realmente ser instanciadas.

Vamos supor que você esteja definindo uma classe Mammal e depois a herdando para Dog and Cat. Se você pensar bem, não faz muito sentido ter uma instância pura de um mamífero, pois você primeiro precisa saber que tipo de mamífero é realmente.

Potencialmente, existe um método chamado MakeSound () que só faz sentido nas classes herdadas, mas não há som comum que todos os mamíferos possam fazer (é apenas um exemplo para não tentar defender os sons dos mamíferos aqui).

Isso significa que o Mammal deve ser uma classe abstrata, pois terá algum comportamento comum implementado em todos os mamíferos, mas não deve ser instanciado. Esse é o conceito básico por trás das aulas abstratas, mas definitivamente há mais que você deve aprender.

3. Interfaces

Não há interfaces puras em C ++ no mesmo sentido que você possui em Java ou C #. A única maneira de criar uma é ter uma classe abstrata pura que imita a maior parte do comportamento desejado em uma interface.

Basicamente, o comportamento que você procura é definir um contrato no qual outros objetos possam interagir sem se preocupar com a implementação subjacente. Quando você torna uma classe puramente abstrata, significa que toda a implementação pertence a algum outro lugar; portanto, o objetivo dessa classe é apenas o contrato que ela define. Este é um conceito muito poderoso no OO e você definitivamente deve procurar mais.

Você pode ler sobre a especificação de interface do C # no MSDN para ter uma idéia melhor:

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

O C ++ fornecerá o mesmo tipo de comportamento ao ter uma classe abstrata pura.


2
Uma classe base abstrata pura fornece tudo o que uma interface faz. As interfaces existem em Java (e C #) porque os designers de linguagem queriam impedir a herança múltipla (por causa das dores de cabeça que cria), mas reconheciam um uso muito comum da herança múltipla que não é problemático.
Gort the Robot

@StevenBurnap: Mas não em C ++, que é o contexto da pergunta.
DeadMG

3
Ele está perguntando sobre C ++ e interfaces. "Interface" não é um recurso de linguagem do C ++, mas as pessoas certamente criam interfaces no C ++ que funcionam exatamente como as interfaces Java, usando classes básicas abstratas. Eles fizeram isso antes mesmo de o Java existir.
Gort the Robot


1
O mesmo vale para Singletons. No C ++, ambos são padrões de design, não recursos de linguagem. Isso não significa que as pessoas não falem sobre interfaces em C ++ e para que servem. O conceito de "interface" surgiu de sistemas componentes como Corba e COM, que foram originalmente desenvolvidos para serem usados ​​em C. puro. No C ++, as interfaces são normalmente implementadas com classes base abstratas nas quais todos os métodos são virtuais. A funcionalidade disso é idêntica à de uma interface Java. Como tal, o conceito de uma interface Java é intencionalmente um subconjunto de classes abstratas C ++.
Gort the Robot

8

A maioria das pessoas já explicou o que são singletons / classes abstratas. Espero fornecer uma perspectiva um pouco diferente e dar alguns exemplos práticos.

Singletons - Quando você deseja que todo código de chamada use uma única instância de variáveis, por qualquer motivo, você tem as seguintes opções:

  • Variáveis ​​globais - obviamente sem encapsulamento, a maioria do código acoplado a globais ... ruim
  • Uma classe com todas as funções estáticas - um pouco melhor do que simples globais, mas essa decisão de design ainda o leva a um caminho em que o código se baseia em dados globais e pode ser muito difícil mudar mais tarde. Além disso, você não pode tirar proveito das coisas OO, como o polimorfismo, se tudo o que você tem são funções estáticas
  • Singleton - Mesmo que haja apenas uma instância da classe, a implementação real da classe não precisa saber nada sobre o fato de ser global. Então, hoje você pode ter uma classe que é um singleton; amanhã, você pode simplesmente tornar seu construtor público e permitir que os clientes instanciam várias cópias. A maioria dos códigos de clientes que referenciam o singleton não precisaria mudar e a implementação do singleton em si não precisaria mudar. A única alteração é como o código do cliente adquire a referência singleton em primeiro lugar.

De todas as opções ruins e ruins por aí, se você precisar de dados globais, o singleton é uma abordagem MUITO melhor do que as duas anteriores. Também permite que você mantenha suas opções em aberto se amanhã mudar de idéia e decidir usar inversão de controle em vez de ter dados globais.

Então, onde você usaria um singleton? Aqui estão alguns exemplos:

  • Log - se você quiser que todo o processo tenha um único log, você poderá criar um objeto de log e distribuí-lo por toda parte. Mas e se você tiver 100.000k linhas de código de aplicativo herdado? modificar todos eles? Ou você pode simplesmente introduzir o seguinte e começar a usá-lo onde quiser:

    CLog::GetInstance().write( "my log message goes here" );
  • Cache de conexão do servidor - isso foi algo que eu tive que apresentar em nosso aplicativo. Nossa base de código, e muito, era usada para conectar aos servidores sempre que quisesse. Na maioria das vezes, isso era bom, a menos que houvesse algum tipo de latência na rede. Precisávamos de uma solução e um novo design de um aplicativo de 10 anos que não estivesse realmente sobre a mesa. Eu escrevi um CServerConnectionManager único. Em seguida, pesquisei o código e substitui as chamadas CoCreateInstanceWithAuth por uma chamada de assinatura idêntica que invocava minha classe. Agora, depois que a primeira tentativa de conexão foi armazenada em cache e o resto do tempo, as tentativas de "conexão" foram instantâneas. Alguns dizem que singletons são maus. Eu digo que eles salvaram minha bunda.

  • Para depuração, geralmente consideramos a tabela global de objetos em execução muito útil. Temos algumas aulas que gostaríamos de acompanhar. Todos eles derivam da mesma classe base. Durante a instanciação, eles chamam a tabela de objetos de singleton e se registram. Quando são destruídos, eles cancelam o registro. Posso ir até qualquer máquina, conectar-me a um processo e criar uma lista de objetos em execução. Estive no produto por mais de meia década e nunca senti que precisávamos de 2 tabelas de objetos "globais".

  • Temos algumas classes de utilitário de analisador de string relativamente complexas que dependem de expressões regulares. As classes de expressão regular precisam ser inicializadas antes de poder executar correspondências. A inicialização é um pouco cara, porque é quando um FSM é gerado com base na sequência de análise. No entanto, depois disso, a classe de expressão regular pode ser acessada com segurança por 100 threads, porque o FSM criado uma vez nunca muda. Essas classes de analisador usam internamente singletons para garantir que essa inicialização ocorra apenas uma vez. Isso melhorou significativamente o desempenho e nunca causou problemas devido a "singletons maus".

Tendo dito tudo isso, você precisa ter em mente quando e onde usar singletons. 9 em 10 vezes, existe uma solução melhor e, por todos os meios, você deve usá-la. No entanto, há momentos em que o singleton é absolutamente a escolha certa para o design.

Próximo tópico ... interfaces e classes abstratas. Primeiro, como outros já mencionaram, a interface É uma classe abstrata, mas vai além disso, reforçando que ela não tem absolutamente nenhuma implementação. Em alguns idiomas, a palavra-chave da interface faz parte do idioma. Em C ++, simplesmente usamos classes abstratas. O Microsoft VC ++ deu um passo para definir isso em algum lugar internamente:

typedef struct interface;

... então você ainda pode usar a palavra-chave da interface (ela será destacada como uma palavra-chave 'real'), mas no que diz respeito ao compilador real, é apenas uma estrutura.

Então, onde você usaria isso? Vamos voltar ao meu exemplo de uma tabela de objetos em execução. Digamos que a classe base tenha ...

impressão nula virtual () = 0;

Essa é a sua aula abstrata. As classes que usam a tabela de objetos de tempo de execução derivam da mesma classe base. A classe base contém código comum para registro / cancelamento de registro. Mas nunca será instanciado por si só. Agora posso ter classes derivadas (por exemplo, solicitações, ouvintes, objetos de conexão do cliente ...), cada uma implementará print () para que, quando anexado ao processo e pergunte o que está sendo executado, cada objeto relate seu próprio estado.

Exemplos de classes / interfaces abstratas são incontáveis ​​e você definitivamente os usa (ou deve usá-los) com muito mais frequência do que com singletons. Em resumo, eles permitem que você escreva um código que funcione com os tipos base e não esteja vinculado à implementação real. Isso permite que você modifique a implementação posteriormente, sem precisar alterar muito código.

Aqui está outro exemplo. Digamos que eu tenho uma classe que implementa um logger, CLog. Esta classe grava no arquivo no disco local. Começo a usar essa classe em minhas 100.000 linhas de código herdadas. Por todo o lugar. A vida é boa até que alguém diga: ei, vamos escrever no banco de dados em vez de em um arquivo. Agora eu crio uma nova classe, vamos chamá-lo de CDbLog e gravar no banco de dados. Você consegue imaginar o incômodo de passar por 100.000 linhas e mudar tudo, de CLog para CDbLog? Como alternativa, eu poderia ter:

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

class CLog : public ILogger { ... };

class CDbLog : public ILogger { ... };

class CLogFactory {
    ILogger* GetLog();
};

Se todo o código estivesse usando a interface ILogger, tudo que eu precisaria mudar seria a implementação interna do CLogFactory :: GetLog (). O resto do código funcionaria automaticamente sem que eu tivesse que levantar um dedo.

Para obter mais informações sobre interfaces e bom design de OO, recomendo vivamente os princípios, padrões e práticas ágeis do tio Bob em C # . O livro está cheio de exemplos que usam abstrações e fornece explicações em linguagem clara de tudo.


4

QUANDO está usando um singleton recomendado / necessário? É uma boa prática?

Nunca. Pior que isso, eles são uma vadia absoluta para se livrar, então cometer esse erro pode assombrá-lo por muitos, muitos anos.

A diferença entre classes abstratas e interfaces é absolutamente nenhuma em C ++. Você geralmente tem interfaces para especificar algum comportamento da classe derivada, mas sem precisar especificar tudo. Isso torna seu código mais flexível, porque você pode trocar qualquer classe que atenda às especificações mais limitadas. As interfaces de tempo de execução são usadas quando você precisa de uma abstração em tempo de execução.


Interfaces são um subconjunto de classes abstratas. Uma interface é uma classe abstrata sem métodos definidos. (Uma classe abstrata sem código).
Gort the Robot

1
@StevenBurnap: Talvez em algum outro idioma.
DeadMG

4
"Interface" é apenas uma convenção em C ++. Quando o vi usado, é uma classe abstrata com apenas métodos virtuais puros e sem propriedades. Obviamente, você pode escrever qualquer classe antiga e colocar um "I" na frente do nome.
Gort the Robot

Era assim que eu esperava que as pessoas respondessem a este post. Uma pergunta de cada vez. De qualquer forma, muito obrigado por compartilhar seu conhecimento. Vale a pena investir tempo nessa comunidade.
appoll 12/12

3

Singleton é útil quando você não deseja várias cópias de um objeto em particular, deve haver apenas uma instância dessa classe - é usada para objetos que mantêm o estado global, precisam lidar com código não reentrante de alguma forma etc.

Um singleton que possui um número fixo de 2 ou mais instâncias é multiton , pense no pool de conexões com o banco de dados etc.

A interface especifica uma API bem definida que ajuda a modelar a interação entre objetos. Em alguns casos, você pode ter um grupo de classes que possui alguma funcionalidade comum - nesse caso, em vez de duplicá-lo em implementações, você pode adicionar definições de método à interface, transformando-a em uma classe abstrata .

Você pode até ter uma classe abstrata onde todos os métodos são implementados, mas você a marca como abstrata para indicar que não deve ser usada como está sem subclassificar.

Nota: Interface e classe abstrata não são muito diferentes no mundo C ++ com herança múltipla etc., mas têm significados diferentes em Java et al.


Muito bem dito! 1
jmort253

3

Se você parar para pensar sobre isso, é tudo sobre o polimorfismo. Você deseja escrever um pedaço de código uma vez que possa fazer mais do que se pensa, dependendo do que você passa.

Digamos que temos uma função como o seguinte código 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"

O bom dessa função é que ela será capaz de lidar com qualquer lista de objetos, desde que esses objetos implementem o método "printToScreen". Você pode passar uma lista de widgets felizes, uma lista de widgets tristes ou até mesmo uma lista que tem uma mistura deles e a função foo ainda será capaz de fazer suas coisas corretamente.

Nos referimos a essa restrição de tipo de necessidade de ter um conjunto de métodos implementados (neste caso, printToScreen) como uma interface e diz-se que os objetos que implementam todos os métodos implementam a interface.

Se estivéssemos falando sobre uma linguagem dinâmica e tipificada como Python, basicamente já teríamos terminado. No entanto, o sistema de tipo estático do C ++ exige que atribuamos uma classe aos objetos em nossa função e ele só poderá trabalhar com subclasses dessa classe 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();
    }
}

No nosso caso, a única razão pela qual a classe Printable existe é fornecer um local para o método printToScreen existir. Como não há implementação compartilhada entre as classes que implementam o método printToScreen, faz sentido transformar Printable em uma classe abstrata que é usada apenas como uma maneira de agrupar classes semelhantes em uma hierarquia comum.

No C ++, os conceitos de classe e interface absctract são um pouco embaçados. Se você deseja defini-las melhor, classes abstratas são o que você está pensando, enquanto as interfaces geralmente significam a idéia mais geral em linguagem cruzada do conjunto de métodos visíveis que um objeto expõe. (Embora algumas linguagens, como Java, usem o termo da interface para se referir a algo mais diretamente como uma classe base abstrata)

Basicamente, as classes concretas especificam como os objetos são implementados, enquanto as classes abstratas especificam como elas se relacionam com o restante do código. Para tornar suas funções mais polimórficas, tente receber um ponteiro para a superclasse abstrata sempre que fizer sentido.


Quanto aos singletons, eles são realmente inúteis, pois geralmente podem ser substituídos por apenas um grupo de métodos estáticos ou funções antigas simples. No entanto, às vezes você tem algum tipo de restrição que o força a usar um objeto, mesmo que você não queira usá-lo, portanto o padrão singleton é apropriado.


BTW, algumas pessoas podem ter comentado que a palavra "interface" tem um significado particular na linguagem Java. Eu acho que é melhor ficar com a definição mais geral por enquanto.


1

Interfaces

É difícil entender o propósito de uma ferramenta que resolve um problema que você nunca teve. Não entendi as interfaces por um tempo depois que comecei a programar. Vamos entender o que eles fizeram, mas não sabia por que você gostaria de usar um.

Aqui está o problema - você sabe o que deseja fazer, mas possui várias maneiras de fazê-lo ou pode alterar a forma como o faz mais tarde. Seria bom se você pudesse desempenhar o papel de gerente sem noção - latir algumas ordens e obter os resultados desejados sem se preocupar com o que é feito.

Digamos que você tenha um pequeno site e salve todas as informações de seus usuários em um arquivo csv. Não é a solução mais sofisticada, mas funciona bem o suficiente para armazenar os detalhes de usuário de sua mãe. Mais tarde, seu site decola e você tem 10.000 usuários. Talvez seja hora de usar um banco de dados adequado.

Se você fosse esperto no começo, já teria visto isso e não faria as chamadas para salvar diretamente no csv. Em vez disso, você pensaria no que precisava fazer, independentemente de como foi implementado. Vamos dizer store()e retrieve(). Você faz uma Persisterinterface com os métodos abstratos para store()e retrieve()e criar uma CsvPersistersubclasse que realmente implementa esses métodos.

Posteriormente, você pode criar um DbPersisterque implemente o armazenamento e a recuperação reais de dados de maneira completamente diferente de como sua classe csv fez isso.

O melhor é que tudo o que você precisa fazer agora é mudar

Persister* prst = new CsvPersister();

para

Persister* prst = new DbPersister();

e então você está pronto. Suas chamadas prst.store()e prst.retrieve()todas continuarão funcionando, elas são tratadas de maneira diferente "nos bastidores".

Agora, você ainda precisava criar as implementações cvs e db, para que ainda não experimentasse o luxo de ser o chefe. Os benefícios reais são aparentes quando você usa interfaces criadas por outras pessoas. Se alguém já teve a gentileza de criar um CsvPersister()e DbPersister()já, basta escolher um e chamar os métodos necessários. Se você decidir usar o outro posteriormente, ou em outro projeto, já sabe como ele funciona.

Estou muito enferrujado no meu C ++, então usarei apenas alguns exemplos genéricos de programação. Os contêineres são um ótimo exemplo de como as interfaces facilitam sua vida.

Você pode ter Array, LinkedList, BinaryTree, etc. todas as subclasses de Containerque tem métodos como insert(), find(), delete().

Agora, ao adicionar algo no meio de uma lista vinculada, você nem precisa saber o que é uma lista vinculada. Você acabou de ligar myLinkedList->insert(4)e iterativamente magicamente percorre a lista e a coloca lá. Mesmo se você souber como uma lista vinculada funciona (o que você realmente deveria), não precisará procurar suas funções específicas, porque provavelmente já sabe o que elas estão usando usando uma diferente Containeranteriormente.

Classes abstratas

Classes abstratas são bastante semelhantes às interfaces (bem, tecnicamente, as interfaces são classes abstratas, mas aqui quero dizer classes básicas que têm alguns de seus métodos detalhados.

Digamos que você esteja criando um jogo e precisa detectar quando os inimigos estão a uma curta distância do jogador. Você pode criar uma classe base Enemyque possui um método inRange(). Embora existam muitas coisas diferentes sobre os inimigos, o método usado para verificar seu alcance é consistente. Portanto, sua Enemyclasse terá um método detalhado para verificar o alcance, mas métodos virtuais puros para outras coisas que não compartilham semelhanças entre os tipos inimigos.

O bom disso é que, se você bagunçar o código de detecção de alcance ou quiser ajustá-lo, precisará alterá-lo em um só lugar.

Obviamente, existem muitos outros motivos para interfaces e classes básicas abstratas, mas esses são alguns dos motivos pelos quais você pode usá-los.

Singletons

Eu os uso ocasionalmente, e nunca fui queimado por eles. Isso não quer dizer que eles não arruinem minha vida em algum momento, com base nas experiências de outras pessoas.

Aqui está uma boa discussão sobre o estado global de algumas pessoas mais experientes e cautelosas: Por que o Estado Global é tão mau?


1

No reino animal existem vários animais que são mamíferos. Aqui o mamífero é uma classe base e dela derivam vários animais.

Você já viu um mamífero passando? Sim, muitas vezes tenho certeza - no entanto, eram todos os tipos de mamíferos, não eram?

Você nunca viu algo que era literalmente apenas um mamífero. Eles eram todos os tipos de mamíferos.

O mamífero de classe é necessário para definir várias características e grupos, mas ele não existe como uma entidade física.

Portanto, é uma classe base abstrata.

Como os mamíferos se movem? Eles andam, nadam, voam, etc?

Não há como saber no nível dos mamíferos, mas todos os mamíferos devem se mover de alguma maneira (digamos que essa é uma lei biológica para facilitar o exemplo).

Portanto, MoveAround () é uma função virtual, pois todo mamífero derivado dessa classe precisa ser capaz de implementá-la de maneira diferente.

No entanto, ser como todo mamífero DEVE definir o MoveAround porque todos os mamíferos devem se mover e é impossível fazê-lo no nível do mamífero. Ele deve ser implementado por todas as classes filho, mas não tem significado na classe base.

Portanto, MoveAround é uma função virtual pura.

Se você tem uma classe inteira que permite a atividade, mas não é capaz de definir no nível superior como isso deve ser feito, todas as funções são totalmente virtuais e essa é uma interface.
Por exemplo - se tivermos um jogo no qual você codificará um robô e o enviará para lutar em um campo de batalha, preciso saber os nomes das funções e protótipos a serem chamados. Eu não me importo como você implementá-lo do seu lado, desde que a 'interface' seja clara. Portanto, posso fornecer uma classe de interface da qual você derivará para escrever seu robô matador.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.