ÍNDICE DE CONTEÚDO
O princípio da responsabilidade única, do inglês single responsability, defende que cada classe deve ter uma única responsabilidade e que essa responsabilidade deve estar inteiramente encapsulada dentro da classe. É o princípio S da acrônimo mnemônico SOLID que auxilia a programação orientada a objetos (OOP). Embora elaborado para OOP, esse princípio não só pode, mas deve ser aplicado em firmware escrito em C, e a forma de aplicá-lo é através da modularização, onde cada módulo terá a sua única responsabilidade.
Neste artigo é apresentado um template de módulo de firwmare. Se corretamente aplicado, esse template simplifica grandes complexidades em pequenos probleminhas fáceis de solucionar, mas antes de entrar em detalhes sobre a o template em si, é importante enfatizar que a aplicação do princípio da responsabilidade única produz sistemas melhores, pois:
- reduz a complexidade de código;
- aumenta a legibilidade;
- evita código espaguete;
- aumenta a testabilidade;
- facilita a manutenção e expansão.
De fato a lógica é simples, se um módulo foca apenas em resolver um pequeno problema, o código desse módulo está fadado a ser mais robusto e melhor expansível, e, por consequência, o sistema composto por módulos, que obedeçam o princípio da responsabilidade única, também será mais robusto e expansível.
Explicando o template utilizado para o Princípio da Responsabilidade Única
Como em uma classe, o módulo deve ter a parte de alocação de memória, inicialização e desligamento. O gerenciamento de memória fica sob a responsabilidade do usuário (o programador que vai usar seu código). No exemplo abaixo observe que o módulo module_name é usado de duas maneiras, uma onde o objeto é alocado no heap, fazendo uso de malloc e free, e outra na stack, alocando module_name_t diretamente sem usar ponteiro. Entre a inicialização module_name_init e o desligamento module_name_deinit, o exemplo solicita que o objeto efetue uma ação chamando module_name_do. Na verdade, module_name é um módulo vazio que tem o objetivo de ilustrar e fornecer um modelo. Então nenhuma das funções faz algo realmente útil.
Exemplo de Uso: main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include "module_name.h" #include <malloc.h> int main(int argc, char ** argv) { /* Stack Example */ module_name_t stack_obj; module_name_init(&stack_obj); module_name_do(&stack_obj); module_name_deinit(&stack_obj); /* Heap Example */ module_name_t *heap_obj = (module_name_t *)calloc(1, sizeof(module_name_t)); module_name_init(heap_obj); module_name_do(heap_obj); module_name_deinit(heap_obj); free(heap_obj); return 0; } |
Interface e Implementação
Abaixo encontra-se a interface do module_name e sua implementação, observe que o tipo privado struct s_module_name_private está definido publicamente para que possa ser alocada na stack uma vez que em C não existe uma maneira segura de esconder os membros de estruturas privadas sem impor limitações quanto ao seu uso, entretanto o acesso aos seus membros é proibido, obedecendo assim as regras de abstração.
Header: module_name.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#ifndef MODULE_NAME_H_ #define MODULE_NAME_H_ #include <stdint.h> /* Private struct declared public so client can alloc it in the STACK, access to struct s_module_name_private members is forbidden. */ struct s_module_mame_private { /* module internal variables */ }; typedef struct s_module_mame_private module_name_t; void module_name_init(module_name_t * obj); void module_name_deinit(module_name_t * obj); int module_name_do(module_name_t * obj); #endif /* MODULE_NAME_H_ */ |
Source: module_name.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include "module_name.h" void module_name_init(module_name_t * obj) { if (!obj) return; /* * whole module initialization */ } void module_name_deinit(module_name_t * obj) { if (!obj) return; /* * whole module deinitialization */ } int module_name_do(module_name_t * obj) { if (!obj) return -1; /* * do something with module instance obj */ return 0; } |
Aplicando apenas a estrutura de módulo proposta é possível construir sistemas desde os mais simples até os mais complexos.
Implementar uma pequena solução para cada problema simples, fazer isso camada por camada, abstração por abstração, tornar cada módulo facilmente testável, e sempre obedecer o princípio da responsabilidade única até se obter um grande sistema com grande qualidade de código: eis aí uma ótima prática de projeto.
Referências
- Test-Driven Development for Embedded C, James W. Grenning – Capitulo 11
- Design Pattern for Embedded Systems in C, Bruce Powel Douglass – Code Listings
- Práticas para a Qualidade – Os 4 tipos de módulos de firmware, blog Sistema Embarcado Livre.
Bacana. Isto é OOP em C! Bastante útil quando se quer ter várias instâncias de um módulo ou controlar o ciclo de vida das instâncias. Porém em software embarcado é muito comum que os módulos sejam “singleton” (apenas uma instância global necessária). Nestes casos eu não recomendo seguir este padrão pois seria uma complexidade adicional para pouco ou nenhum benefício. Sobre manter a estrutura privada, pode-se utilizar uma estrutura opaca para alocação em pilha e heap (alocação estática é mais complicado). Você só precisa definir uma função (ou uma constante global se preferir) que retorne o tamanho da estrutura e… Leia mais »
Muito bom.