Artigos
Criando Componentes Reusáveis em C++

Do original:

Creating Reusable C++ Components
By DENNIS MANCL

Lucent Technologies
Bell Laboratories
600 Mountain Ave., Room 3D-476
Murray Hill, NJ 07974
USA

Palesta apresentada em C++ World Conference in Dallas, TX on Nov. 13, 1996.


É um desafio criar componentes de software que possam ser usados por outros desenvolvedores.
C++  tem alguns dos mecanismos de encapsulamento que tornam possível criar componentes de fácil reutilização, mas a linguagem C++ impõe uma grande carga de detalhes ao trabalho dos desenvolvedores de classes.

Esse tutorial apresenta alguns dos tópicos chave na criação de componentes de boa qualidade.


Agenda

Obstáculos a reutilização
Três tipos de componentware
Estratégia de pequenas classes
Interfaces e herança
Estruturas
Testes
Procurando oportunidades de reutilização
Comprando bons componentes
Oito dicas para você fazer os seus próprios componentes
Resumo final
Notas:
O tema principal desse tutorial é como você pode criar componentes reusáveis em C++ para as suas próprias áreas de aplicação. A criação de bons componentes não requer programação orientada a objeto, embora os princípios de análise e de projeto orientado a objeto sejam freqüentemente utilizados para decompor o domínio do problema, e para projetar a interface do programador.

Muitas das idéias que são apresentadas nesse tutorial podem também ser usadas (com pequenas modificações) para produzir bons componentes reusáveis em C, Ada e Java

Obstáculos a reutilização

Muitos costumam pensar que as suas dificuldades em reutilizar componentes se devem a problemas ou deficiências técnicas:
  • a linguagem adequada
  • a ferramenta CASE adequada
  • se desenvolvermos o sistema mais orientado a objeto
  • se encontrarmos e aplicarmos os padrões adequados

Geralmente se pensa que se tivermos a tecnologia adequada a reutilização virá como conseqüência.

A reutilização de software é a principal razão que leva muitos grupos de desenvolvimento de software a migrar para tecnologia orientada a objeto, embora a reutilização seja difícil de se atingir em um primeiro projeto orientado a objeto.

Barreiras não-técnicas à reutilização

Quando falham várias tentativas de se implementar reutilização de software dentro de uma organização, muitas pessoas se voltam para a literatura sobre engenharia de software ou sobre reutilização de software, que apontam muitas das barreiras não-técnicas, gerenciais e culturais que bloqueiam a reutilização de software. Para superar esses problemas, nós tentamos
  • o tipo adequado de repositório de componentes reutilizáveis
  • o conjunto adequado de incentivos à reutilização de software
  • ...

com o intuito de quebrar as barreiras e criar uma cultura de reutilização.

Uma boa fonte de informação nessa área é o livro Confessions of a Used Program Salesman by Will Tracz (Addison-Wesley, 1995).

De volta para às questões técnicas

É realmente necessário considerar as estratégias e os problemas técnicos de reutilização de software, para que se tenha uma reutilização efetiva. Em bibliotecas e em aplicações C++:
  • classes C++ são normalmente os átomos da reutilização
  • princípios de projeto orientado a objeto podem ajudar a escrever software reutilizável, mas esses não são os únicos aspectos a considerar

Há vários livros recentes que contêm boas orientações para tornar seu código mais reutilizável:
Martin D. Carroll and Margaret A. Ellis, Designing and Coding Reusable C++, Addison-Wesley, 1995.
John Lakos, Large Scale C++ Software Design, Addison-Wesley, 1996.

Vários dos principais pontos deste tutorial foram extraídos desses dois livros

Quatro requerimentos de software reutilizável

Os fatores importantes em reutilização de software são plenamente conhecidos há muito tempo. Esses fatores são:
  • documentação
    deve haver informação adequada sobre o que cada componente de software reutilizável pode fazer.
  • flexibilidade
    um componente verdadeiramente reutilizável deve ser aplicável em mais de um contexto
  • visibilidade
    para reutilizar software, os desenvolvedores devem poder encontrar os componentes a reutilizar (em um catálogo de componentes reutilizáveis, por exemplo)
  • eficiência
    um componente de reutilizável não será utilizado se não tiver a eficiência requerida pelo programa de aplicação

Essa lista de requerimentos para reutilização foi extraída de Enhancing Reusability with Information Hiding, by David L. Parnas, Paul C. Clements, and David M. Weiss, ITT Proceedings of the Workshop on Reusability in Programming, 1983, pp. 240-247. Esse artigo foi reimpresso em um tutorial IEEE, de modo que pode ser encontradotambém em Peter Freeman, Tutorial: Software Reusability, IEEE Computer Society Press, 1988, pp. 91-95.

Três tipos de componentware

Há várias e diferentes maneiras de se criar e usar componentware:
  • linguagens interpretadas controlando uma GUI (Visual Basic, Tcl/Tk)
  • linguagems compiladas usando um estrutura padrão de comunicação inter-componentes (Corba, OLE/COM, ActiveX)
  • linguagens compiladas que permitem a definição de classes ou de conjunto de classes, sem o overhead de uma estrutura padrão de comunicação inter-componentes

Você deve usar a tecnologia de componentes adequada especificamente para sua aplicação. Isso significa que para alguns casos Visual Basic é ótimo, para outros casos, CORBA ou OLE/COM serão melhores, e ainda para outros, uma abordagem mais customizada usando classes C++ vai oferecer melhor flexibilidade e eficiência.

Estratégias de reutilização em C++

Algumas dicas para reutilização bem sucedida em C++:
  • criar classes com interfaces pequenas e facilmente inteligíveis
  • definir suas classes com funções de entrada (read) e saída (write)
  • procurar algoritmos de finalidade especial que possam ser generalizados

A abordagem small is beaultifull é muito importante no projeto de classes C++. Criação de objetos que possam ser persistentes pode ajudar a ampliar a utilização de muitos componentes.

Estratégia de pequenas classes

Em muitos projetos de desenvolvimento em C++, há um pequeno número de classes centrais, com interfaces grandes e complexas. Um projeto desse tipo é uma garantia de que essas classes serão específicas para uma única aplicação.

É melhor a estratégia de criar uma grande número de classes individualmente mais simples, e potencialmente mais reutilizáveis.

Classes muito grandes são geralmente o resultado de se reprojetar um sistema já existente, cujo projeto não foi orientado a objeto. Nesses casos você pode  precisar ter algumas classes muito grandes para conter toda a funcionalidade do sistema existente. Isso é aceitável como ponto de partida, mas se você planeja continuar a fazer modificações no projeto ao longo do tempo, você tem que começar a decompor o antigo sistema em termos de grupos lógicos de dados.

Classes pequenas para um problema em particular

Manter a interface para uma classe tão pequena quanto possível é especialmente importante nos casos de classes que são definidas para o âmbito de um problema em particular.
  • é mais comum ter-se grandes interfaces públicas para classes stardard datatype, como character string ou time, e para classes container ou para classes de estrutura de dados, como matriz de tamanho variável, linta ligada ou árvore binária.
  • as classes stardard datatype ou containers raramente são extendidas via herança, e sim por classes para outros níveis de âmbito do problema:
    • você pode encontrar algum comportamento comum que possa ser colocado em uma classe base comum
    • novos requerimentos podem requerer a definição de classes similares com a adição de novos comportamentos
    • dados extras podem ser adicionados a novas classes derivadas
    • reformulação pode ser usada para reorganizar a hierarquia da classe, criando algums tipos de dados abstratos básicos com interfaces simples

Você não deve considerar stardard class libraries como o melhor modelo a emular, quando projetar as interfaces se suas próprias classes para outros níveis de âmbito do problema.

Reformulação é uma técnica para reorganizar uma hierarquia de classes mantendo interfaces similares para as classes derivadas na hierarquia. Em um caso muito simples, supondo que você queira extender uma classe concreta existente via herança. Você deveria, ao invés disso, escrever três classes:

// before refactoring:  a concrete class
// derived from another concrete class
class Concrete_A {
public:
  void f1();
  virtual void f2();
};
class Concrete_B :
   public Concrete_A {
public:
  void f2();
  void f3();
};
// after refactoring:  both concrete classes
// are derived from an abstract class
class Abstr_A {
public:
  void f1(); // use old Concrete_A::
             // f1() implementation
  virtual void f2() = 0;
};
class Concrete_A : public Abstr_A {
public:
  void f2();
}
class Concrete_B : public Abstr_A {
  void f2();
  void f3();
};
Mais a respeito de classes pequenas

Alguns iniciantes em C++ são relutantes em implementar várias classes pequenas. É mais fácil induzi-los se você escrever um conjunto de classes.

Uma estratégia para reduzir o tamanho da interface da classe: criar uma classe de baixo-nível data-only mais várias classes de interface, que contêm as interfaces especializadas para um dos subsistemas da aplicação.

  • isso é como um kit de envelopes de carta, como vários tipos de envelope
  • é uma boa idéia adcionar um contador de referência às classes de baixo-nível

A reformulação se assemelha ao seguinte:

// before refactoring: one big class
class BigClass {
public:
  void f1();
  int f2();
  char *f3();
private:
  char bigbuf[512];
  int file_descriptor;
  int current_state;
};

// after refactoring: one low-level class
// with multiple interface classes
class BigClassRep {
private:
  char bigbuf[512];
  int file_descriptor;
  int current_state;
  friend class BigClass;
  friend class AltBigClass;
};

class BigClass {
public:
  // need constr and destr
  void f1();
  int f2();
private:
  BigClassRep *data;
};
class AltBigClass {
public:
  // need constr and destr
  char *f3();
private:
  BigClassRep *data;
};
Representação em caracteres

Um incentivo extra para a reutilização da classe em múltiplos subsistemas é ter funções para representação do estado da classe em caractéres, que possam ser usadas para armazenar o estado dos objetos em arquivos, ou transmitir o estado dos objetos em uma mensagem.
  • escreva funções membro Myclass::toString() e Myclass::fromString()
  • e operadores de I/O operator>>(istream &, Myclass &) e operator<<(ostream &, const Myclass &)

A criação de funções operator>>() e operator<<() requer conhecimento sobre a biblioteca C++ iostreams. Uma boa referência é o livro  C++ IOStreams Handbook by Steve Teale (Addison-Wesley, 1993).


Hierarquias de raiz única

Aguns desenvolvedores C++ optam por colocar todas as suas classes em uma hierarquia de raiz única:
                       -----------
                       | ZObject |
                       -----------
                            |
                      --------------
                      | LogObjects |
                      --------------
                      /            \
         --------------            --------------
         | CustAction |            | RecordList |
         --------------            --------------
          /         \                    |
   ---------    -------------    ------------------
   | Order |    | Complaint |    | CustRecordList |
   ---------    -------------    ------------------
Essa não é uma boa idéia. Embora essa solução torne possível a existência de containers heterogêneos, facilita o uso equivocado de containers. Esse é o caso de reutilização potencialmente perigosa.

Esse tipo de hierarquia é a favorita dos antigos programadores Smalltalk, que se acostumaram a ter uma grande flexibilidade no tratamento de tipos de dados.

Uma das bibliotecas de classes oringinais C++ (a NIH Class Library, escrita por Keith Gorlen at National Institutes of Health), tem todas as suas classes derivadas de uma base comum.

Mais a respeito de hierarquias de raiz única

Hierarquia de herança única torna muito mais fácil colocar as operações comuns no mais alto nível da hierarquia.
  • mas é muito mais trabalhosa a manutenção da hierarquia quando se adiciona um novo tipo de dado
  • essa solução cria uma infraestrutura difícil de transportar para um outro projeto
  • essa solução é um uso abusivo do paradigma de orientação a objeto

Isso é o que pensa normalmente o projetista de uma hisrarquia de raiz única: "Se isso é orientado a objeto, isso deve ser bom". É importante considerar o quanto a coleção de classes será portável, extensível e eficiente.

Mix-ins

Um modo melhor para se ter polimorfismo flexível em uma biblioteca é definir algumas classes base abstratas.
  • cada classe base não tem dados, mas um pequeno conjunto de funções de interface
  • classes concretas podem ser derivadas de uma ou mais classes base, e devem implementar todas as funções especificadas nas interfaces abstratas
  • as classes base definem os diferentes tipos possíveis de containers aos quais as classes concretas podem ser acrescentadas

A estratégia mix-in é similar às interfaces em linguagem Java. Java normalmente permite apenas herança simples, mas também permite herança de classes de interfaces especiais que não têm implementação.

Se você se restringe a esse tipo de herança múltipla nos projetos de bibliotecas C++, você vai evitar vários dos problemas de herança múltipla (por exemplo, se você não precisar de herança virtual)

A estratégia mix-in

Se você está escrevendo um função genérica (polimórfica) que deve operar com argumentos de diferentes tipos, você pode definir uma classe abstrata que contém exatamente a interface que função genérica necessita executar:
class Sortable { // abstract class
public:
  virtual int compare(const Sortable &) const = 0;
};

class ComplexNode : public Sortable {
public:
  int compare(const Sortable &b) const {
    // return 0 if equal, negative if this is less than b,
    // positive if this is greater than b.
  }
};

void sort(Sortable *first, Sortable *last) {
  .... use the virtual function compare() in here ....
}
Embora seja fácil criar novos tipos mix-in, é difícil aplicá-los a classes pré-existentes - você precisa criar uma classe artificial extra, a qual herda de uma classe concreta e um mix-in.

Por exemplo, se você deseja classificar strings, você deve criar um novo tipo de dado:

class SortableString : public string, public Sortable {
public:
  int compare(const Sortable &b) const { .... }
};
A alternativa template

Uma maneira simples de escrever funções genéricas é usar templates.
template <class T>
sort(const T *first, const T *last) {
  .... you can still make calls to p->compare(*q) ....
O problema com funções genéricas usando templates: a interface que uma função genérica requer é menos explícita (Você vai ainda obter erros de compilação se os argumentos forem de tipos diferentes, ou se a classe dos argumentos não definir uma função compare)

A vantagem das funções genéricas usando templates: as funções genéricas podem ter um valor de argumento normal, além de argumentos pointers e referências.

Essa abordagem com templates tem se tornado mais popular desde que a Standard Template Library tornou-se disponível.

Quão grande deve ser um arquivo fonte?

Isso depende.

Quando codificam um conjunto de classes, alguns autores (por exemplo, John Lakos) acreditam na inclusão de tudo o que for possível em dois arquivos:

  • um único arquivo-header (".h") que declara todas as classes do conjunto
  • um único arquivo fonte (".c" ou ".cpp") que implementa todas as funções daquelas classes

Outros autores (Carroll e Ellis) preconizam arquivos fonte menores:

  • ponha apenas funções de uma única classe em um mesmo arquivo fonte, e divida as funções de uma classe em diversos arquivos, com grupos de funções que normalmente são usadas juntas em um mesmo arquivo.

Para bibliotecas de classe em desenvolvimento, o segundo esquema funciona um pouco melhor, na medida em que o código se torne mais maduro, é mais fácil ter-se tudo em um único arquivo.

Geralmente é uma boa idéia, em projetos grandes, com em torno de um milhão de linhas de código, minimizar o número de arquivos fonte e arquivos-header. Deve-se considerar também o tempo consumido para link-editar uma aplicação muito grande.

Classes muito simples

As classes, consideradas individualmente, são mais facilmente reutilizáveis se forem muito simples, se tiverem pequenas interfaces públicas. Esse critério pode ser levado ao extremo: uma família de classes onde cada uma tem apenas uma função membro pública.

Essas classes atômicas podem maximizar a reutilização, mas também têm seus problemas:

  • pode ser difícil encontrar o componente que se quer para uma tarefa em particular (é mais fácil localizar componentes quando estes estão organizados por categoria: acessadores de dados, transformadores de dados, controladores de fluxo de execução, etc)
  • você normalmente vai precisar reunir um grande número de componentes para montar um sistema de aplicação
  • você precisa fazer mais testes ou simulações de chamadas entre os diferentes componentes de software

Há algumas ferramentas e métodos comerciais baseados na criação de classes muito simples. Em particular, há uma ferramenta denominada  ANSWER:Architect by Tony Martins at Claremont Technology. Na metodologia de Tony Martins há um pequeno número de categorias de classes simples, e há um processo sofisticado para conexão dessas classes em conjunto, formando uma rede de classes.

Estruturas

A última palavra em tecnologia de reutilização de componentes em C++ é estruturas orientadas a objeto.

Um exemplo:

  • MFC (Microsoft Foundation Classes) mais as ferramentas que acompanham o compilador Microsoft Visual C++ para criar aplicações baseadas em Windows

Qual a principal diferença entre bibliotecas de componentes e estruturas?

  • para usar uma biblioteca de componentes, você escreve código que cria objetos da classe da biblioteca e chama funções membro da classe
  • para usar uma estrutura, você cria novas classes que são derivadas de uma estrutura de classes - a estrutura contém códigos em nível de aplicação que você vai chamar conforme o comportamento que você adicione às suas novas classes derivadas

Estruturas são bem mais difíceis de se escrever, e algumas vezes difíceis de se usar, mas promovem a reutilização de código.

A reutilização em estruturas é mais reutilização de projeto do que reutilização de código.

Onde estruturas são úteis?

Quando você deve preferir escrever uma estrutura, ao invés de uma biblioteca de componentes?
  • se você precisa criar vários programas de aplicação com arquiteturas similares, mas que precisam trabalhar em diferentes contextos (você pode embutir a informação de contexto em uma classe abstrata que será implementada diferentemente para diferentes programas de aplicação)

Uma estrutura possui seja um principal, seja um conjunto de funções genéricas, que dependem de uma ou mais interfaces abstratas que são implementadas em diferentes classes concretas, escritas pelo programador usuário da estrutura.

Descobrindo as estruturas

A criação de estruturas é normalmente um processo de descoberta. É mais fácil transformar a implementação concreta de um grupo de classes em uma estrutura extensível se:
  • as classes não usam qualquer dado global
  • as interfaces públicas das classes oferecem operações de alto nível, ao invés de meras funções de get e set, as quais realmente expõem os detalhes internos da classe, muito além do recomendável
  • você pode encontrar, no projeto inicial, várias classes que tenham interfaces públicas similares

Estruturas tornaram-se conhecidas em Smalltalk já há algum tempo, mas estruturas em C++ foram discutidas pela primeira vêz em um conjunto de artigos em 1988:

Ralph E. Johnson and Brian Foote, Designing Reusable Classes, Journal of Object-Oriented Programming, June/July 1988, pp. 22-35.

Andre Weinand, Erich Gamma, and Rudolf Marty, ET++ - An object-oriented application framework in C++, Proceedings of OOPSLA '88, pp. 46-57.

Testando classes C++

No projeto de conjuntos de classes e sistemas é importante evitar as dependências recursivas ou recíprocas. Se você conseguir quebrar o conjunto de classes em níveis, será mais fácil definir um plano de testes bottom-up
  • cada componente de baixo nível pode ser testado isoladamente
  • componentes de alto nível são construídos sobre peças de baixo nível confiáveis, portanto são mais fáceis de se testar

A técnica de dividir um sistema grande em níveis - denominada nivelização (levitalization) - é um dos conceitos de projeto físico mais importantes no livro de John Lakos, Large-Scale C++ Software Design.

Procurando oportunidades de reutilização

A maioria do esforço para reutilização é uma combinação de varredura e novos investimentos

Você nunca sabe, a priori, o que será bem sucedido:

  • Nós tinhamos uma classe Graph (criada como parte de classes container de uso geral)
    • cada Graph contém um conjunto de objetos dos tipos Vertex e Edge
    • cada Edge refere-se a dois objetos Vertex
    • as classes Vertex e Edge podem ser derivadas para adição de dados extras e de novos comportamento
  • Embora esse conjunto de classes tenha sido reutilizado apenas internamente, por um pequeno número de pessoas, aqueles que o utilizaram o consideraram indispensável
  • reação dos usuários da classe: programamos apenas a solução do problema

Como mencionado por Andrew Loeing em uma de suas colunas JOOP: projeto de biblioteca é projeto de linguagem. A criação de um conjunto de classes é como definir uma extensão da linguagem para resolver um conjunto específico de problemas.

Comprando bons componentes

Posto que o melhor tipo de reutilização é a reutilização de caixas-pretas, você não tem porque requerer o código fonte dos componentes que você comprar.
  • não tendo os códigos fonte, você precisará ser mais exigente quanto à documentação fornecida
  • o ambiente operacional dos componentes deve ser claramente identificado

Você precisa inspecionar a reusabilidade dos componentes:

  • bons componentes não usam dados globais (você pode usar as ferramentas de seu sistema de compilação para examinar a tabela de símbolos da biblioteca)
  • arquivos-header devem ser claros e legíveis, e os comentários nos arquivos-header devem ser concisos
  • arquivos-header devem ter um mínimo de dependências de outros componentes
  • faça as verificações normais de projeto de classes em C++, por exemplo, as classes com dados membro que são pointers devem ter um construtor de cópia declarado na classe
  • experimente: você deve construir algumas classes simples que utlizem os componentes como dados membro, e talvez como classes base

Não há (ainda) um selo de qualidade para componentes C++, mas o comprador inteligente pode fazer uma série de verificações para assegurar-se de estar comprando componentes de boa qualidade.

Um dos problemas mais comuns é a interação entre componentes ou diferentes versões de componentes. Por exemplo, se um componente para banco de dados persistente usa classes container de Rogue Wave, você pode precisar usar a mesma versão das bibliotecas Rogue Wave ao longo de toda a sua aplicação.

Oito dicas pra você implementar seus próprios componentes

  • use inspeção de código e ferramentas para encontrar problemas de alocação de memória
  • para classes container, use alocação de memória dedicada
  • observe os problemas com a ordem de inicialização de objetos estáticos
  • defina um único arquivo-header que contenha a interface completa de seu componente
  • não envie mensagens de erro para standard output
  • nunca encerre a execução (exit) a partir de um componente
  • defina cada função membro para uma ação razoável em qualquer estado
  • construa componentes que se ajustem às estruturas padrões

Essa é uma lista de dicas muito pessoais. Cada especialista em componentes terá sua própria lista, que será específica para a sua área de atuação.

Alocação de memória

É importante ser um bom cidadão quando se trata de memória dinâmica
  • muitas das aplicações baseadas em C++ usam alocação de memória dinâmica freqüentemente
  • em aplicações de execução longa, memory-leaks podem provocar desastres

Há dois modos de se evitar problemas com alocação de memória:

  • fazer uma análise e revisão do código para certificar-se que toda memória alocada dinamicamente é liberada (a questão mais comum na revisão é: qual a duração esperada para esse objeto?)
  • usar ferramentas que adicionam instrumentação em tempo de execução (como Purify, Sentinel, etc)

Purify é uma excelente ferramenta para localizar potenciais problemas de alocação de memória, e é de fácil utilização. Ainda não há substituto para análise e prevenção.

Alocação de memória dedicada

Uma maneira de se obter melhor performance quando da implementação de classes containers é fazer versões especiais para as funções new e delete para nós do container
  • na maioria das classes containers, os nós terão o mesmo tamanho, portanto um alocador de memória de tamanho fixo pode ser mais rápido do que o operador global new, que depende da função malloc()

Operadores especializados new e delete são especialmente úteis em sistemas de tempo real, onde memória disponível é um prêmio. Você pode pré-alocar um pool de objetos de tamanho fixo no stack.

Ordem de inicialização estática

Se seu componente tem algumas classes internas de objetos estáticos, que precisam ser inicializados antes de qualquer uso do componente, você precisa controlar esse processo para assegurar que a inicialização ocorra como deve ser (especialmente se alguém decide usar seu componente para inicializar um objeto estático dentro da aplicação cliente).

Há pelos menos três artifícios de linguagem para assegurar a seqüência correta de inicialização.

O código nos exemplos seguintes visa garantir que certos objetos serão inicializados antes que sejam usados por outro código.

Um artifício de inicialização

Se você define uma classe que, para funcionar corretamente, requer a existência de um objeto estático inicializado, você precisa de uma classe auxiliar para fazer a inicialização:
/* file Myclass.h */
#ifndef __MYCLASS_H
#define __MYCLASS_H
class Myclass {
private:
  static char *machname; // set to name of machine for lifetime
                        // of the program
public:
  static void set_machname();
  static void unset_machname();
  ....
};
class Myclass_init { // initializer class (cf. Jerry Schwarz)
private:
  static unsigned count;
public:
  Myclass_init(); // the constructor will set the value of machname
  ~Myclass_init();
};
static Myclass_init Myclass_initobj; // 1 object per source file
#endif
O artifício contido no exemplo acima é amplamente usado (em código como a biblioteca iostreams de Jerry Schwarz).

Nesse exemplo, a variável Myclass::machname deve ser inicializada o mais cedo possível, uma vez que os construtores de Myclass a utilizam. Já que o objeto estático

Myclass_init::Myclass_initobj
vai aparecer no arquivo fonte antes de qualquer objeto estático Myclass, o construtor Myclass_init será invocado primeiro. Tudo o que temos que fazer é assegurar que o construtor chame a função Myclass::set_machname()
Um artifício de inicialização (continuação)

Aqui estão as definições dos construtores e destrutores de  Myclass_init
/* file Myclass.c */
#include "Myclass.h"
#include <param.h>
#include <sys/utsname.h>
int Myclass_init::count = 0;
Myclass_init::Myclass_init() {
  if (count++ == 0) Myclass::set_machname();
}
Myclass_init::~Myclass_init() {
  if (--count == 0) Myclass:unset_machname();
}

void Myclass::set_machname() {
  machname = new char[MAXHOSTNAMELEN + 1];
  struct utsname u;
  uname(&u);
  strcpy(machname, u.nodename);
}
void Myclass::unset_machname() {
  delete [] machname;
}
O objeto classe estática auxiliar que está definido no arquivo-header vai garantir que o construtor Myclass_init será chamado uma vêz para cada arquivo fonte que inclua Myclas.h. O construtor fazer algo substancial apenas na primeira vêz em que for chamado, na segunda vêz em que for chamado, e nas vezes subseqüentes, o construtor vai apenas somar 1 à variável Myclass_init::count

A definição do destrutor de  Myclass_init vai também garantir que o código para limpeza seja executado exatamente uma vêz - quando o último arquivo, do conjunto de arquivos que incluem Myclass.h, tenha seu objeto estático destruído.

Um artifício de inicialização alternativo

Um outro modo de assegurar a correta utilização de um objeto estático é criar uma função que seja sempre utilizada para acessar o objeto:
/* file Myclass.h */
#ifndef __MYCLASS_H
#define __MYCLASS_H
#include <param.h>
#include <sys/utsname.h>
class Myclass {
private:
  static char *machname_; // actual object
public:
  static char *machname(); // accessor function
  static void set_machname(); // initialization function
};
inline char *Myclass::machname() {
  if (machname_ == 0) Myclass::set_machname();
  return machname_;
}
inline void Myclass::set_machname() {
  machname_ = new char[MAXHOSTNAMELEN + 1];
  struct utsname u;
  uname(&u);
  strcpy(machname_, u.nodename);
}
#endif
Nesse exemplo, a variável machname_ é usada para armazenar o valor real do pointer, e a função machname() é usada para acessar esse valor

Basicamente, esse mesmo método pode ser usado para inicializar variáveis que não sejam pointers, exceto que a função de acesso deve ser definida para retornar uma referência para o objeto.

/* file Myclass.h */
#ifndef __MYCLASS_H
#define __MYCLASS_H
#include <param.h>
#include <sys/utsname.h>
#include <String.h>
class Myclass {
private:
  static String *machname_; // pointer to actual object
public:
  static String &machname(); // accessor function
  static void set_machname(); // initialization function
};
inline String &Myclass::machname() {
  if (machname_ == 0) Myclass::set_machname();
  return *machname_;
}
inline void Myclass::set_machname() {
  struct utsname u; uname(&u);
  machname_ = new String(u.nodename);
}
#endif
Um outro artifício de inicialização

O artifício anterior tem um desvantagem importante: nào destrói automaticamente o objeto ao término da execução. Uma maneira de superar esse problema é fazer um objeto function-static:
char *Myclass::machname() {
  static char lcl_machname[MAXHOSTNAMELEN + 1];
  static int flag = 0;
  if (flag == 0) {
    flag = 1;
    struct utsname u;
    uname(&u);
    strcpy(lcl_machname, u.nodename);
  }
  return lcl_machname;
}
Nesse caso, Myclass::set_machname() não pode ser uma função inline. Se o objeto function-static é um objeto de classe com destrutor, tal destrutor será executado quando do encerramento do programa.

Basicamente, esse mesmo método pode ser usado para inicializar variáveis que não sejam pointers, exceto que a função de acesso deve ser definida para retornar uma referência para o objeto.

String &Myclass::machname() {
  static String *lcl_machname = 0;
  if (lcl_machname == 0) {
    machname_ = lcl_machname;
    struct utsname u;
    uname(&u);
    lcl_machname = new String(u.nodename);
  }
  return *lcl_machname;
}
Defina um único arquivo-header para sua interface

Uma boa estratégia é definir um único arquivo-header que contenha a definição da classe (ou as definições) para seu componente, e também ter os comandos #include para obter tudo o que o seu componente precisa para ser compilado corretamente

Uma verificação simples, conforme sugerida por John Lakos:

  • Criar um arquivo fonte (".c" ou ".cpp") que contenha uma única linha
    (
    #include "yourcomp.h") e compilá-lo. Se a compilação apresentar erro, você deve ter se esquecido de incluir algum arquivo-header necessário, ou você deve rever a ordem das declarações em seu arquivo ".h"
Não envie mensagens de erro

Essa não é uma regra absoluta. É aceitável desenvolver componentes que contenham relatórios de seus próprio de erros, mas isso reduz a reusabilidade do componente.

Alternativas:

  • retorne um código de erro específico para cada tipo de problema
  • ative (throw) exceções

Mecanismos reservados para mensagens de erro do tipo UNIX (onde as mensagens são enviadas para stderr) não funcionam da mesma forma em outros ambientes, como MS Windows ou Macintosh.

Nunca exit

Essa é uma outra recomendação, muitas vezes desrespeitada.
  • mais uma vêz, você reduz a reusabilidade do componente se inclui alguma condição de encerramento da execução do programa
  • ativar uma exceção pode ser uma alternativa aceitável

Esse é um item de checklist para muitos programadores que procuram por componentes. Se um componente pode encerrar a execução do programa, é melhor não utilizá-lo.

Interface completa

Muitas classes de componentes resultam de uma análise orientada a objeto superficial, que define uma lista incompleta de comportamentos para os métodos da classe.
  • algumas vezes a análise considera apenas os casos normais, não prevendo as exceções
  • algumas vezes o espaço interno de estados da classe é particionado em vários estados ou modos, e nem todas as funções podem ser aplicadas a todos os modos.

Você não pode controlar totalmente a maneira como outros vão usar sua classe

  • os clientes da classe podem aplicar métodos a um objeto em um estado impróprio
  • os clientes pode estender sua classe sem compreender o seu modelo operacional interno

Por isso é uma boa idéia inserir código que garanta que uma chamada imprópria não cause um estrago.

Componentes e estruturas padrões

Há três estruturas especialmente importantes:
  • Standard Template Library (STL) - uma estrutura de classes containers e algoritmos
  • iostreams - classes padrões de input e output em C++
  • Microsoft Foundation Classes (MFC) - estrutura para exibir e modificar documentos usando vários formatos em Microsoft Windows

Você tem várias vantagens em estender estruturas existentes

  • você pode reusar muitos dos componentes básicos da infraestrutura já existente
  • muitos usuários já compreendem o projeto de estruturas, portanto têm maior confiança em usar componentes baseados em estruturas
  • você tem menor probabilidade de ter conflitos com outros componentes
Standard Template Library

Já que a STL foi aceita pelo comitê ANSI C++ como parte do padrão preliminar ANSI, é uma boa idéia construir futuros containers de modo compatível com essa estrutura.

A parte principal do trabalho é definir o iterator apropriado para sua coleção de classes:

  • unidirecional (baseado na classe forward_iterator)
  • bidirecional (baseado na classe bidirectional_iterator)
  • acesso randômico (baseado na classe random_access_iterator)

A classe iterator é sempre uma classe aninhada dentro da sua classe container, e é derivado de um dos tipos básicos de iterator.

Iterators são implementados normalmente da seguinte maneira:

#include <iterator.h>
template <class T>
class MyContainer {
public:
  // implementation details of my container class
  // .....

  class iterator :
    public bidirectional_iterator<T,difference_type> {
  private:
    // ... internal data structure for the iterator ...
  public:
    iterator() {}
    bool operator==(const iterator &) const;
    reference operator*() const; // returns a MyContainer&
    iterator& operator++(); // advance the iterator
    iterator operator++(int); // post-increment version
    iterator& operator--(); // move iterator back one object
    iterator operator--(int); // post-decrement version
  };
};
Para maiores informações, veja  STL Tutorial and Reference Guide by David R. Musser and Atul Saini (Addison-Wesley, 1996).
Classes iostreams

É complicado criar uma nova classe derivada a partir das classes istream (input stream) e ostream (output stream).

O trabalho mais difícil é criar uma nova classe especial que seja derivada de streambuf

  • streambuf define as operações internas de buffering em memória, tanto para a pesquisa caracter-a-caracter e transferência de blocos a partir da origem dos caracteres, quanto para a destinação de caracteres.

As funções streambuf::underflow() e streambuf::underflow() são as mais importantes.

O único trabalho necessário nas classes stream reais (as classes derivadas a partir de istream e ostream) é construir corretamente as classes streambuf derivadas e inicializar tudo corretamente.

Microsoft Foundation Classes

A estrutura MFC é similar a outras estrutruras GUI - a classe document (derivada de CDocument) contém a representação interna da da informação que será completa ou parcialmente apresentada na tela. As classes view (derivadas de CView) mapeam a representação interna para o objeto gráfico real (como caracteres, caixas, setas, etc) que são efetivamente apresentadas na tela.

Outras classes recebem os eventos do usuário (cliques no mouse, pressionamento de botões, etc) e atualizam tanto view quanto document.

Classes MFC não funcionam muito bem com classes STL, porque os compiladores Microsoft C++ mais antigos tinham um suporte inadequado para templates. Isso deve ser melhorado aos poucos.

Uma boa fonte de idéias sobre componentes baseados em MFC (fora da documentação da Microsoft) é  Extending the MFC Library by David A. Schmitt (Addison-Wesley, 1996).

Resumo final

Escrever componentes em C++ é compensador, mas é trabalhoso. Há boas fontes de informação sobre
  • Estilo de codificação em C++
  • Estruturas
  • Reformulação de projetos
  • Construção de arquiteturas de implementação voltadas para facilidades de testes

Com alguns desses recursos, seus esforços em reutilização de código serão melhor sucedidos

© 1996 DENNIS MANCL, Lucent Technologies - Bell Laboratories
Tradução de Dagoberto Haele Arnaut

| Home | Bookmarks | Universidades | Para Saber mais | Universidades | WEB Directory | Mapa do site |