5. Aprimorando a aplicação Alarme com C++ (interfaces ou classes abstratas)

A divisão do sistema em componentes pode ser feita de diversas maneiras. Uma forma de se pensar em componentes é definir quais partes do sistema podem ser compradas e/ou atualizadas independentemente.

Isto implica que o nosso código principal deve funcionar praticamente do mesmo modo no caso de troca de componentes. Este comportamento pode ser obtido utilizando o conceito de interfaces, também conhecido como classe abstrata em C++.

5.1 Herança em C++

Para criar uma subclasse em C++, utilizamos a sintaxe

class subclasse: especificador-acesso superclasse

No geral, especificador-acesso é public, o que significa que mantemos o mesmo controle de acesso da classe pai.

No exemplo

class Fruit {
  public:
    Fruit() {};
    void setColor(char *_color) { color = _color; }
    char *getColor() { return color; }	
  protected: 
    char *color;
  private: 
    double acidity;
};

class Banana: public Fruit {
  public:
    Banana() {};
}

criamos uma classe Banana que é subclasse de Fruit. Dentro da definição dos métodos de Banana, podemos acessar apenas os identificadores públicos e protegidos da superclasse. Isso significa que não temos acesso ao membro acidity, pois este é privado.

Como é tradicional em orientação a objetos, a classe filha herda os métodos e membros da classe pai.

5.2 Métodos virtuais puros e classes abstratas

Em C++, podemos declarar métodos virtuais puros, que não possuem definição. Estes geralmente são declarados nas classes bases, de modo que devem ser redefinidos nas classes filhas.

Vamos utilizar este conceito na nossa classe Fruit:

class Fruit {
  public:
    Fruit();
    virtual void smell() = 0;
    void setColor(char *_color) { color = _color; }
    char *getColor() { return color; }	
  protected: 
    char *color;
  private: 
    double acidity;
};

Note que não existe definição do método smell e, assim, NÃO é possível instanciar objetos da classe Fruit. Isto é, a seguinte linha ocorre em erro:

Fruit alguma_fruta; // Erro!

No entanto, podemos criar uma subclasse e instanciá-la, desde que implementemos todos os método virtuais da superclasse:

class Banana: public Fruit {
  public:
    Banana() {};
    void smell() {
      printf("Smells like banana spirit\n");
    }
}

Banana alguma_banana; // OK

Classes abstratas são classes que possuem pelo menos um método virtual puro e, consequentemente, não podem ser instanciadas. Estas podem ser utilizadas para implementar interfaces, como veremos a seguir.

5.3 Novo diagrama de componentes

Com isso em mente, vamos propor um novo diagrama de componentes que implementa interfaces para cada componente do nosso sistema.

Diagrama de componentes

Para implementação das interfaces, utilizaremos classes abstratas com métodos virtuais puros públicos. Estes servirão de classe base para as subclasses que implementam os componentes.

Aqui mantivemos o nome original dos nossos componentes para a interfaces, de modo a diminuir o impacto no código original. Assim, tivemos que modificar o nome de todos os componentes. Outra forma de definir nomes interfaces é anexar Interface no fim, podendo, assim, manter o nome original dos componentes. Nesse caso, por exemplo, a interface do Timer se chamaria TimerInterface.

5.4 Criando a interface Timer e a classe TimerInterno

Para a interface Timer, precisamos converter a classe original para uma classe abstrata. No arquivo timer.h, deixamos apenas métodos, que devem ser convertidos para virtuais:

#ifndef TIMER_H_INCLUDED
#define TIMER_H_INCLUDED

class Timer {
  public:
  virtual void iniciar(int controle) = 0;
  virtual int timeout() = 0;
};

#endif // TIMER_H_INCLUDED

Como são métodos virtuais, não possuem definição e, desse modo, podemos deletar o arquivo timer.cpp.

Para criar o componente TimerInterno, vamos criar uma subclasse de Timer. Ou seja, no arquivo timer_interno.h, temos:

#ifndef TIMER_INTERNO_H_INCLUDED
#define TIMER_INTERNO_H_INCLUDED

#include "timer.h"

class TimerInterno: public Timer {
  public:
  TimerInterno();
  void iniciar(int controle);
  int timeout();

  private:
  int situacao;
  unsigned long horaInicio;
};

#endif // TIMER_INTERNO_H_INCLUDED

Para o arquivo timer_interno.cpp, considere que as definições dos métodos e inicialização dos membros é idêntica ao da classe Timer original.

Para finalizar o aprimoramento do código, idealmente devemos implementar todos os componentes com interface, de acordo com o novo diagrama. Isto fica como exercício.

No caso de dúvidas, o código final da aplicação pode ser consultado em

https://gitlab.uspdigital.usp.br/andre.kubagawa/alarme_arduino/-/tree/master/alarme_interfaces

Bibliografia para este capítulo