Drobne programowanie

Kaczuś zaprasza do opowieści o algorytmach, językach programowania i strukturach danych

Na stronie stosowane są pliki cookies. Więcej na podstronie.
odsłon: 370

Refaktoring: tablicowanie funkcji

Dawno nic nie pisałem na tę stronę, a że natrafiłem na pewien specyficzny kod, który aż prosi się o poprawę, toteż może kilka słów na temat pewnych mechanizmów, które warto by użyć podczas pisania kodu. Tu uproszczę trochę całość problemu i przedstawię dość schematycznie, po to aby lepiej było widać ideę. Jest sobie funkcja inicjująca (tutaj void main_init()), w której, aby nie była zbyt długa, wywołujemy inne funkcje inicjujące różne rzeczy (tutaj void init_x(), gdzie x jest tu od 1 do 9). Problem jest taki, że mimo wszystko finalnie funkcja i tak ma około 300 linii kodu, dodatkowo funkcje inicjujące zmieniają się w zależności od projektu, reszta jak zauważyłem robiona jest metodą copy/paste.... Nie dość, że łatwo o błędy, funkcja jest niepotrzebnie rozrośnięta, to jeszcze trzeba wszystko tworzyć osobno dla każdego projektu. Tak więc mamy brzydki kod wyglądający:
#include <stdio.h>

#include <stdio.h>

#define DBG_FLAG_INIT 1

void init_1()
{
    printf("Init 1 works\n");
}
void init_2()
{
    printf("Init 2 works\n");
}

void init_3()
{
    printf("Init 3 works\n");
}

void init_4()
{
    printf("Init 4 works\n");
}
void init_5()
{
    printf("Init 5 works\n");
}
void init_6()
{
    printf("Init 6 works\n");
}
void init_7()
{
    printf("Init 7 works\n");
}
void init_8()
{
    printf("Init 8 works\n");
}
void init_9()
{
    printf("Init 9 works\n");
}

void main_init()
{
    printf("before init\n");
    init_1();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_1 done\n");
    init_2();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_2 done\n");
    init_3();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_3 done\n");
    init_4();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_4 done\n");
    init_5();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_5 done\n");
    init_6();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_6 done\n");
    init_7();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_7 done\n");
    init_8();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_8 done\n");
    init_9();
    if (DBG_FLAG_INIT)
    	printf("DBG: init_9 done\n");
    printf("after init\n");

}

int main()
{
    main_init();
    return 0;
} 

Oczywiście to co jest w funkcjach init_x jest w rzeczywistości inne niż tu pokazane, ale chodzi o przedstawienie problemu. Jak widać aż chciałoby się wywołać te funkcje inicjujące w pętli. Co będziemy się zastanawiać, zróbmy to:
typedef void (*Tinit_func)();
void main_init()
{
    const Tinit_func c_init_func_arr[] = {init_1, init_2, init_3, init_4, init_5, init_6, init_7, init_8, init_9};
    int lcnt = sizeof(c_init_func_arr)/sizeof(Tinit_func);
    int i;
    printf("before init\n");

    for (i = 0; i < lcnt; ++i)
      c_init_func_arr[i]();
    printf("after init\n");
}
Jest prawie tak samo, ale brakuje nam jeszcze informacji wypisywanej przy załączonej opcji z debuglogiem, więc zmieńmy to:
typedef void (*Tinit_func)();
void main_init()
{
    const Tinit_func c_init_func_arr[] = {init_1, init_2, init_3, init_4, init_5, init_6, init_7, init_8, init_9};
    const char * c_initfunc_names_arr[] = {"init_1", "init_2", "init_3", "init_4", "init_5", "init_6", "init_7", "init_8", "init_9"};
    int lcnt = sizeof(c_init_func_arr)/sizeof(Tinit_func);
    int i;
    printf("before init\n");

    for (i = 0; i < lcnt; ++i)
    {
      c_init_func_arr[i]();
      if (DBG_FLAG_INIT)
    	printf("DBG: %s done\n", c_initfunc_names_arr[i]);
    }
    printf("after init\n");
}
Działa tak samo, ale pewien niesmak pozostaje, wprowadzona dodatkowa tablica.... Hmmm może by jakoś to scalić?
typedef void (*Tinit_func)();
#define KOD_INI_FUNC(a) {a,#a}
struct Tinit_func_str
{
    Tinit_func func;
    char *name;
};
void main_init()
{
    const struct Tinit_func_str c_init_func_arr[] = {KOD_INI_FUNC(init_1), KOD_INI_FUNC(init_2), KOD_INI_FUNC(init_3), KOD_INI_FUNC(init_4), KOD_INI_FUNC(init_5), KOD_INI_FUNC(init_6), KOD_INI_FUNC(init_7), KOD_INI_FUNC(init_8), KOD_INI_FUNC(init_9)};
    int lcnt = sizeof(c_init_func_arr)/sizeof(struct Tinit_func_str);
    int i;
    printf("before init\n");

    for (i = 0; i < lcnt; ++i)
    {
      c_init_func_arr[i].func();
      if (DBG_FLAG_INIT)
    	printf("DBG: %s done\n", c_init_func_arr[i].name);
    }
    printf("after init\n");

}
No to mamy już ładną strukturę - jak to w piosence możemy usłyszeć "jest dobrze, ale nie najgorzej jest". Brakuje nam jeszcze najważniejszego: pewnej uniwersalności, tak by mieć przetestowaną funkcję, do której będziemy tylko podawać co ma zainicjować, gdyż przypominam, że w różnych projektach funkcja ma wywoływać różne zestawy funkcji. Tak więc stwórzmy sobie w osobnym pliku:
#include "initcodelib.h"
#define DBG_FLAG_INIT 1

void main_init(const struct Tinit_func_str *ainit_func_arr, size_t acnt)
{
    int i;
    printf("before init\n");

    for (i = 0; i < acnt; ++i)
    {
      ainit_func_arr[i].func();
      if (DBG_FLAG_INIT)
    	printf("DBG: %s done\n", ainit_func_arr[i].name);
    }
    printf("after init\n");
}
natomiast zmodyfikujmy naszego maina w ten sposób:
int main()
{
    const struct Tinit_func_str c_init_func_arr[] = {KOD_INI_FUNC(init_1), KOD_INI_FUNC(init_2), KOD_INI_FUNC(init_3), KOD_INI_FUNC(init_4), KOD_INI_FUNC(init_5), KOD_INI_FUNC(init_6), KOD_INI_FUNC(init_7), KOD_INI_FUNC(init_8), KOD_INI_FUNC(init_9)};
    int lcnt = sizeof(c_init_func_arr)/sizeof(struct Tinit_func_str);
    main_init(c_init_func_arr, lcnt);
    return 0;
}
I teraz w zależności od projektu zmieniać nam się będzie plik główny programu, natomiast funkcję inicjującą mamy już krótką i przetestowaną! W razie występujących problemów, będzie to ostatnie miejsce, do którego będziemy musieli zajrzeć, gdyż z dużym prawdopodobieństwem problem będzie w innej części kodu. Oczywiście, można by pomyśleć jeszcze czy flaga, czy mają być wyświetlane informacje dla debuggera, czy nie, ma być stałą, czy może parametryzować to w zależności od sposobu wywołania programu - to jest coś co tez warto rozważyć, ale zostawiam to Wam, drodzy czytelnicy, jako "pracę domową". Dla chętnych do pobrania przykładowe kody do testów

2016-10-25 22:58:01


refactoring język C tablica funkcji