Práticas recomendadas de OO para programas C [fechado]

19

"Se você realmente quer açúcar OO - vá usar C ++" - foi a resposta imediata que recebi de um dos meus amigos quando perguntei isso. Eu sei que duas coisas estão completamente erradas aqui. Primeiro OO NÃO é 'açúcar', e segundo, C ++ NÃO absorveu C.

Precisamos escrever um servidor em C (o front-end para o qual será em Python) e, portanto, estou explorando maneiras melhores de gerenciar grandes programas em C.

Modelar um sistema grande em termos de objetos e interações com objetos o torna mais gerenciável, sustentável e extensível. Mas quando você tenta traduzir esse modelo em C, que não contém objetos (e tudo o mais), você é desafiado a tomar algumas decisões importantes.

Você cria uma biblioteca personalizada para fornecer as abstrações de OO necessárias ao seu sistema? Coisas como objetos, encapsulamento, herança, polimorfismo, exceções, pub / sub (eventos / sinais), namespaces, introspecção etc. (por exemplo, GObject ou COS ).

Ou, você apenas usa as construções ( structe funções) básicas de C para aproximar todas as suas classes de objetos (e outras abstrações) de maneiras ad-hoc. (por exemplo, algumas das respostas para esta pergunta no SO )

A primeira abordagem fornece uma maneira estruturada de implementar todo o modelo em C. Mas também adiciona uma camada de complexidade que você precisa manter. (Lembre-se de que a complexidade era o que queríamos reduzir usando objetos em primeiro lugar).

Não sei sobre a segunda abordagem e qual é a eficácia em aproximar todas as abstrações que você pode precisar.

Portanto, minhas perguntas simples são: Quais são as melhores práticas para realizar um design orientado a objetos no C. Lembre-se de que não estou perguntando COMO fazê-lo. Esta e esta pergunta falam sobre isso, e há até um livro sobre isso. O que mais me interessa são alguns conselhos / exemplos realistas que abordam os problemas reais que aparecem quando isso acontece.

Nota: por favor, não avise por que C não deve ser usado em favor de C ++. Passamos bem além desse estágio.

codificador de árvore
fonte
3
Você pode escrever um servidor C ++ para que sua interface externa seja extern "C"e possa ser usada no python. Você pode fazer isso manualmente ou o SWIG pode ajudá-lo com isso. Portanto, o desejo de frontend em python não é motivo para não usar C ++. Isso não é tão dizer que não existem razões válidas para querer ficar com C.
Jan Hudec
1
Esta questão precisa de esclarecimentos. Atualmente, os parágrafos 4 e 5 perguntam basicamente qual abordagem deve ser adotada, mas você diz que "não está perguntando COMO fazê-lo" e deseja (uma lista?) Das melhores práticas. Se você não está procurando COMO fazer isso em C, está pedindo uma lista de "melhores práticas" relacionadas ao POO em geral? Se sim, diga isso, mas saiba que a pergunta provavelmente será encerrada por ser subjetiva .
Caleb
:) Estou solicitando exemplos reais (código ou não) em que isso foi feito - e os problemas que eles encontraram ao fazê-lo.
treecoder 7/11
4
Seus requisitos parecem confusos. Você insiste em usar a orientação a objetos por nenhuma razão que eu possa ver (em alguns idiomas, é uma maneira de tornar os programas mais sustentáveis, mas não em C), e insiste em usar C. A orientação a objetos é um meio, não um fim ou uma panacéia . Além disso, é muito beneficiado pelo suporte ao idioma. Se você realmente queria OO, deveria ter considerado isso durante a seleção do idioma. Uma pergunta sobre como criar um grande sistema de software com C faria muito mais sentido.
David Thornley
Você pode dar uma olhada em "Modelagem e Design Orientado a Objetos". (. Rumbaugh et al): existe secção em OO mapeamento projeta para línguas como C.
Giorgio

Respostas:

16

Da minha resposta a Como devo estruturar projetos complexos em C (não OO, mas sobre o gerenciamento da complexidade em C):

A chave é a modularidade. É mais fácil projetar, implementar, compilar e manter.

  • Identifique módulos em seu aplicativo, como classes em um aplicativo OO.
  • Interface e implementação separadas para cada módulo, coloque na interface apenas o que é necessário para outros módulos. Lembre-se de que não há espaço para nome em C, portanto, você deve tornar tudo em suas interfaces exclusivo (por exemplo, com um prefixo).
  • Oculte variáveis ​​globais na implementação e use funções de acessador para leitura / gravação.
  • Não pense em termos de herança, mas em termos de composição. Como regra geral, não tente imitar C ++ em C, isso seria muito difícil de ler e manter.

Da minha resposta a Quais são as convenções de nomenclatura típicas para funções públicas e privadas do OO C (acho que essa é uma prática recomendada):

A convenção que uso é:

  • Função pública (no arquivo de cabeçalho):

    struct Classname;
    Classname_functionname(struct Classname * me, other args...);
  • Função privada (estática no arquivo de implementação)

    static functionname(struct Classname * me, other args...)

Além disso, muitas ferramentas UML são capazes de gerar código C a partir de diagramas UML. Um de código aberto é o Topcased .

mouviciel
fonte
+1 para ligação Como devo estruturar projetos complexos em C
treecoder
1
Ótima resposta. Apontar para a modularidade. O OO deve fornecê-lo, mas 1) na prática, é muito comum acabar com espaguete com OO e 2) não é o único caminho. Para alguns exemplos na vida real, veja o kernel do linux (modularidade no estilo C) e os projetos que usam glib (OO no estilo C). Tive a oportunidade de trabalhar com ambos os estilos e a modularidade no estilo C da IMO vence.
Joh
E por que exatamente a composição é uma abordagem melhor do que a herança? A justificativa e as referências de suporte são bem-vindas. Ou você estava se referindo apenas aos programas C?
Aleksandr Blekh
1
@AleksandrBlekh - Sim, estou me referindo apenas a C.
Mouviciel 24/10
16

Eu acho que você precisa diferenciar OO e C ++ nesta discussão.

A implementação de objetos é possível em C e é muito fácil - basta criar estruturas com ponteiros de função. Essa é a sua "segunda abordagem", e eu iria com ela. Outra opção seria não usar ponteiros de função na estrutura, mas passar a estrutura dos dados para as funções em chamadas diretas, como um ponteiro de "contexto". Isso é melhor, IMHO, pois é mais legível, rastreável com mais facilidade e permite adicionar funções sem alterar a estrutura (fácil de herdar se nenhum dado for adicionado). Na verdade, é assim que o C ++ geralmente implementa o thisponteiro.

O polimorfismo se torna mais complicado porque não há suporte embutido à herança e abstração; portanto, você terá que incluir a estrutura pai na classe filho ou fazer muitas pastas de cópias, qualquer uma dessas opções é francamente horrível, mesmo que tecnicamente simples. Enorme quantidade de bugs esperando para acontecer.

As funções virtuais podem ser facilmente alcançadas através de ponteiros de função apontando para diferentes funções, conforme necessário, novamente - muito propenso a erros quando feito manualmente, muito trabalho tedioso na inicialização desses ponteiros corretamente.

Quanto a namespaces, exceções, modelos, etc - acho que se você estiver limitado a C -, deve desistir deles. Eu trabalhei em escrever OO em C, não faria isso se eu tivesse uma escolha (naquele local de trabalho, literalmente, lutei para introduzir o C ++, e os gerentes ficaram "surpresos" com a facilidade de integrá-lo com o restante dos módulos C no final.).

Escusado será dizer que se você pode usar C ++ - use C ++. Não há motivo real para não fazê-lo.

littleadv
fonte
Na verdade, você pode herdar de uma estrutura e adicionar dados: basta declarar o primeiro item da estrutura filho como uma variável cujo tipo é estrutura pai. Em seguida, faça o lançamento conforme necessário.
Mouviciel 7/11
1
@mouviciel - sim. Eu disse isso. " ... então você terá que incluir a estrutura pai na sua classe filho, ou ... "
littleadv
5
Não há razão para tentar implementar a herança. Como forma de obter a reutilização de código, é uma idéia falha, para começar. A composição do objeto é mais fácil e melhor.
precisa saber é o seguinte
@KaptajnKold - concorda.
Littleadv 7/11
8

Aqui estão os conceitos básicos de como criar orientação a objetos em C

1. Criando objetos e encapsulamento

Geralmente - cria-se um objeto como

object_instance = create_object_typex(parameter);

Os métodos podem ser definidos de uma das duas maneiras aqui.

object_type_method_function(object_instance,parameter1)
OR
object_instance->method_function(object_instance_private_data,parameter1)

Observe que, na maioria dos casos, object_instance (or object_instance_private_data)é retornado é do tipo void *.O aplicativo não pode ser referência a membros ou funções individuais disso.

Além disso, cada método usa esses object_instance para o método subsequente.

2. Polimorfismo

Podemos usar muitas funções e ponteiros de função para substituir determinadas funcionalidades em tempo de execução.

por exemplo - todos os object_methods são definidos como um ponteiro de função que pode ser estendido aos métodos público e privado.

Também podemos aplicar a sobrecarga de funções em um sentido limitado, usando um método var_args muito semelhante ao número variável de argumentos definidos em printf. Sim, isso não é tão flexível em C ++ - mas é o caminho mais próximo.

3. Definindo herança

Definir herança é um pouco complicado, mas é possível fazer o seguinte com estruturas.

typedef struct { 
     int age,
     int sex,
} person; 

typedef struct { 
     person p,
     enum specialty s;
} doctor;

typedef struct { 
     person p,
     enum subject s;
} engineer;

// use it like
engineer e1 = create_engineer(); 
get_person_age( (person *)e1); 

aqui o doctore engineeré derivado da pessoa e é possível tipcast para um nível superior, digamos person.

O melhor exemplo disso é usado no GObject e objetos derivados.

4. Criando classes virtuais Estou citando um exemplo da vida real de uma biblioteca chamada libjpeg usada por todos os navegadores para decodificação de jpeg. Ele cria uma classe virtual chamada error_manager que o aplicativo pode criar instância concreta e fornecer de volta -

struct djpeg_dest_struct {
  /* start_output is called after jpeg_start_decompress finishes.
   * The color map will be ready at this time, if one is needed.
   */
  JMETHOD(void, start_output, (j_decompress_ptr cinfo,
                               djpeg_dest_ptr dinfo));
  /* Emit the specified number of pixel rows from the buffer. */
  JMETHOD(void, put_pixel_rows, (j_decompress_ptr cinfo,
                                 djpeg_dest_ptr dinfo,
                                 JDIMENSION rows_supplied));
  /* Finish up at the end of the image. */
  JMETHOD(void, finish_output, (j_decompress_ptr cinfo,
                                djpeg_dest_ptr dinfo));

  /* Target file spec; filled in by djpeg.c after object is created. */
  FILE * output_file;

  /* Output pixel-row buffer.  Created by module init or start_output.
   * Width is cinfo->output_width * cinfo->output_components;
   * height is buffer_height.
   */
  JSAMPARRAY buffer;
  JDIMENSION buffer_height;
};

Observe aqui que o JMETHOD se expande em um ponteiro de função por meio de uma macro que precisa ser carregada com os métodos corretos, respectivamente.


Eu tentei dizer muitas coisas sem muitas explicações individuais. Mas espero que as pessoas possam experimentar suas próprias coisas. No entanto, minha intenção é apenas mostrar como as coisas são mapeadas.

Além disso, haverá muitos argumentos de que isso não será exatamente a propriedade verdadeira do equivalente em C ++. Eu sei que OO em C - não será tão rigoroso com sua definição. Mas trabalhar dessa maneira entenderia alguns dos princípios fundamentais.

O importante não é que o OO seja tão rigoroso quanto em C ++ e JAVA. É que se pode organizar estruturalmente o código com o pensamento OO em mente e operá-lo dessa maneira.

Eu recomendo fortemente que as pessoas vejam o design real do libjpeg e os seguintes recursos

uma. Programação orientada a objetos em C
b. este é um bom lugar onde as pessoas trocam idéias
c. e aqui está o livro completo

Dipan Mehta
fonte
3

A orientação a objetos se resume a três coisas:

1) Projeto de programa modular com classes autônomas.

2) Proteção de dados com encapsulamento privado.

3) Herança / polimorfismo e várias outras sintaxes úteis, como construtores / destruidores, modelos etc.

1 é de longe o mais importante, e também é completamente independente do idioma, é tudo sobre o design do programa. Em C, você faz isso criando "módulos de código" autônomos que consistem em um arquivo .h e um arquivo .c. Considere isso como o equivalente a uma classe OO. Você pode decidir o que deve ser colocado dentro deste módulo através do senso comum, UML ou qualquer outro método de design de OO que você está usando para programas C ++.

2 também é bastante importante, não apenas para proteger contra o acesso intencional a dados privados, mas também para proteger contra acesso não intencional, ou seja, "confusão de espaço de nome". O C ++ faz isso de maneiras mais elegantes que o C, mas ainda pode ser alcançado em C usando a palavra-chave static. Todas as variáveis ​​que você declararia como privadas em uma classe C ++, deveriam ser declaradas como estáticas em C e colocadas no escopo do arquivo. Eles são acessíveis apenas dentro de seu próprio módulo de código (classe). Você pode escrever "setters / getters" como faria em C ++.

3 é útil, mas não necessário. Você pode escrever programas OO sem herança ou sem construtores / destruidores. É bom ter essas coisas, elas certamente podem tornar os programas mais elegantes e talvez também mais seguros (ou o contrário, se usados ​​de maneira descuidada). Mas eles não são necessários. Como o C não suporta nenhum desses recursos úteis, você simplesmente terá que ficar sem eles. Os construtores podem ser substituídos por funções init / destruct.

A herança pode ser feita através de vários truques de estrutura, mas eu desaconselharia, pois provavelmente tornará seu programa mais complexo sem nenhum ganho (a herança em geral deve ser cuidadosamente aplicada, não apenas em C, mas em qualquer idioma).

Finalmente, todos os truques de OO podem ser feitos no livro de C. Axel-Tobias Schreiner "Programação orientada a objetos com ANSI C", do início dos anos 90, prova isso. No entanto, eu não recomendaria esse livro a ninguém: ele adiciona uma complexidade desagradável e estranha aos seus programas C que simplesmente não vale a pena. (O livro está disponível gratuitamente aqui para aqueles que ainda estão interessados, apesar do meu aviso.)

Portanto, meu conselho é implementar 1) e 2) acima e pular o resto. É uma maneira de escrever programas em C que tem se mostrado bem-sucedido há mais de 20 anos.


fonte
2

Tomando emprestada alguma experiência dos vários tempos de execução do Objective-C, escrever um recurso de OO dinâmico e polimórfico em C não é muito difícil (tornando-o rápido e fácil de usar, por outro lado, parece ainda estar em andamento após 25 anos). No entanto, se você implementar um recurso de objeto no estilo Objective-C sem estender a sintaxe da linguagem, o código com o qual você termina é bastante confuso:

  • toda classe é definida por uma estrutura que declara sua superclasse, as interfaces com as quais está em conformidade, as mensagens que implementa (como um mapa de "seletor", o nome da mensagem, a "implementação", a função que fornece o comportamento) e a instância da classe layout variável.
  • toda instância é definida por uma estrutura que contém um ponteiro para sua classe e, em seguida, suas variáveis ​​de instância.
  • o envio de mensagens é implementado (especifique ou aceite alguns casos especiais) usando uma função que se parece objc_msgSend(object, selector, …). Ao saber de qual classe o objeto é uma instância, ele pode encontrar a implementação correspondente ao seletor e, assim, executar a função correta.

Isso tudo faz parte de uma biblioteca OO de uso geral projetada para permitir que vários desenvolvedores usem e estendam as classes uns dos outros, portanto, pode ser um exagero para o seu próprio projeto. Muitas vezes, projetei projetos C como projetos orientados a classe "estáticos", usando estruturas e funções: - cada classe é a definição de uma estrutura C que especifica o layout ivar - cada instância é apenas uma instância da estrutura correspondente - os objetos não podem ser "messaged", mas funções semelhantes a métodos MyClass_doSomething(struct MyClass *object, …)são definidas. Isso torna as coisas mais claras no código do que a abordagem ObjC, mas tem menos flexibilidade.

O ponto de equilíbrio das mentiras depende do seu próprio projeto: parece que outros programadores não estarão usando suas interfaces C, portanto a escolha se resume às preferências internas. Obviamente, se você decidir algo como a biblioteca de tempo de execução objc, existem bibliotecas de tempo de execução objc entre plataformas que serão atendidas.


fonte
1

O GObject não esconde realmente nenhuma complexidade e apresenta uma complexidade própria. Eu diria que a coisa ad-hoc é mais fácil do que o GObject, a menos que você precise do material avançado do GObject, como sinais ou o mecanismo de interface.

A coisa é um pouco diferente com o COS, pois ele vem com um pré-processador que estende a sintaxe C com algumas construções OO. Existe um pré-processador semelhante ao GObject, o G Object Builder .

Você também pode experimentar a linguagem de programação Vala , que é uma linguagem de alto nível que é compilada em C e de uma maneira que permite o uso de bibliotecas Vala a partir de código C simples. Ele pode usar o GObject, sua própria estrutura de objetos ou a maneira ad-hoc (com recursos limitados).

Jan Hudec
fonte
1

Primeiro, a menos que seja um dever de casa ou não exista um compilador C ++ para o dispositivo de destino. Não acho que você precise usar C, você pode fornecer uma interface C com ligação C a partir do C ++ com bastante facilidade.

Em segundo lugar, veria o quanto você dependerá de polimorfismo e exceções e os outros recursos que as estruturas podem fornecer, se não forem estruturas muito simples com funções relacionadas, será muito mais fácil do que uma estrutura com todos os recursos, se uma parte significativa de seu o design precisa deles, em seguida, explique e use uma estrutura para que você não precise implementar os recursos por conta própria.

Se você ainda não tem um design para tomar a decisão, faça um pico e veja o que o código diz.

por fim, não precisa ser uma opção ou outra (embora, se você for um framework desde o início, você também possa segui-lo), deve ser possível começar com estruturas simples para as partes simples e adicionar apenas bibliotecas como necessário.

EDIT: Então é uma decisão da gerência certa, vamos ignorar o primeiro ponto então.

jk.
fonte