Spróbujmy napisać coś z MVC.
Programowanie to ciągła nauka, często poprzez eksperymenty. Najlepiej nowe rzeczy testować na jakimś faktycznym projekcie. Moim zdaniem uprzyjemnia to proces nauki 🙂 Stąd powstał pomysł prostego narzędzia do pracy z LEDami. Wypróbowałem parę nowych zabiegów. Może będzie to dla Was coś nowego i przyniesie jakąś wiedzę 🙂
Materiał o projekcie można zobaczyć oczywiście także na YouTube.
Założenia dotyczące funkcjonalności
Zestaw potrzeb, które były przyczynkiem do stworzenia tego urządzenia, był podyktowany budową mojego dziwnego zegara słonecznego, o którym niebawem będziecie mogli poczytać 😉
LED tester, bo tak nazywa się to narzędzie, służy do znajdowania kolorów dla adresowalnych LED RGB WS2812, manualnego sterowania PWM dla zwykłych LED oraz sterowania PWM względem funkcji matematycznych w czasie.
Znalezienie koloru w sieci to nie to samo co uwidocznienie go na LEDzie. Tak samo funkcja, w której zmienia się wypełnienie sygnału PWM sterującego LED, nie jest zgodna z tym, jak zmiany natężenia światła odbiera nasze oko.
Założenia dotyczące projektu
Oprócz założeń funkcjonalności, powstały takie dotyczące tego, czego chcę się nauczyć lub po prostu co chcę przetestować podczas tworzenia tego narzędzia.
Chciałem napisać projekt, który zawierałby unit testy i odpowiadającą temu dobrą architekturę. Dodatkowo miałem na celu pracę z repozytorium; w tym z CI/CD.
Część z tych elementów była mi dobrze znana z projektów komercyjnych. Jednak poza korzystaiem z gotowych rozwiązań nie miałam z nimi innej styczności.
Realizacja
Jeśli chodzi o CI/CD, od tego repozytorium zaczęłem korzystanie z GitHub Actions. Ono buduje mój projekt, przepuszcza przez statyczną analizę kodu, tworzy dokumentację w doxygenie i od razu daje do niej dostęp z poziomu repozytorium. Kiedy jeszcze trochę poćwiczę, przygotuję z pewnością o tym osobny materiał 🙂 Oczywiście, dobre praktyki, które tutaj nabyłem, zacząłem też przerzucać na inne projekty, także te starsze.
O takich zabiegach, jak testy jednostkowe, można poczytać w tej notce.
Całość kodu możecie pobrać na repozytorium tutaj. Dokumentację w doxygenie znajdziecie tutaj.
Zastosowałem wzorzec MVC. Stwierdziłem, że mam tutaj jakieś GUI i znam dobrze ten wzorzec z aplikacji pisanych w C# z WPF lub w C++ z Qt. Były to jednak dotychczas aplikacje desktopowe z dużym wsparciem frameworków. Tutaj platforma jest znacznie skromniejsza i ograniczyłem się do C++ bez wyższych standardów, nie miałem także wszystkiego z std. Nie chciało mi się kombinować z kompilatorem do tej platformy, który na domyśle nie wspiera smart pointerów czy kontenerów.
W MVC występują zależne od siebie obiekty modelu, controllera i view.
View odpowiada za reprezentowanie danych z modelu na GUI, w tym wypadku np. wypisanie nasyceń R, G i B na ekranie, odpowiednie wypełnienie kwadratu itd. Controller pracuje na danych w modelu, czyli np. odpowiednio zwiększa lub zmniejsza nasycenie i daje znać o tym do view, aby to odświeżyło widok. Taka jest mniej więcej zasada działania tej architektury.
Zastosowanie wzorca MVC ułatwiło proces testowania i dość jasno podzieliło aplikację. W view pojawiają się magic numbers, które normalnie w controllerze nie wyglądałyby dobrze, ale wiadomo, że są one związane z rozmieszczeniem elementów na ekranie. Przerobienie aplikacji na inny ekran nie byłoby skomplikowane. Hardwarem zajmuje się klasa Hal, gdzie można podmienić funkcje związane z ekranem, a cała reszta mogłaby działać dalej tak samo 🙂
Część klas dziedziczy po klasach wirtualnych, np. Hal po IHal, aby docelowo można było w miejsce unit testów umieścić tam mocki. Dodatkowo, aby unit testy były łatwiejsze do przygotowania, zależności między np. view a controllerem są tworzone poprzez przekazanie wskaźnika na view w konstruktorze, zamiast tworzenia view w coontrollerze. Taki zabieg nazywa się depedency injection. Idzie za tym jednak pewna odpowiedzialność, ponieważ podczas działania programu nie utrzymuję obiektów wszystkich controllerów i wszystkich view. Po stworzeniu view tylko controller wie, gdzie ono jest. W jego destruktorze muszę pamiętać, aby usunąć view, inaczej skończyłoby się to wyciekiem pamięci.
Controllery mają uniwersalne API i dziedziczą po jednym wspólnym interfejsie, co dzięki polimorfizmowi umożliwia łatwe użycie tylko jednego obiektu w kodzie, jak widać poniżej.
IController* m_controller; ///< dynamically changing controller to opararting on hal, model and connect with view
/**
* @brief according to new mode change view and controller
* @param mode: mode to set active
*/
void change_mode(Mode mode)
{
if (m_controller != nullptr)
{
delete m_controller;
}
...
switch (mode)
{
case Mode::ws_color_tester:
{
Color_tester::Color_tester_view* color_tester_view = new Color_tester::Color_tester_view(m_hal, m_color_tester_model);
m_controller = new Color_tester::Color_tester_controller(m_hal, m_color_tester_model, color_tester_view);
break;
}
...
}
m_controller->active();
}
Reakcję controllera na użycie klawiatury zrealizowałem za pomocą mechanizmu callbacków. Całkiem fajnie udaje się to przepinać pomiędzy usuwanymi i tworzonymi na nowo controllerami.
class IController;
using callback_cursor_move = void (IController::*)(Cursor_move);
/**
* @brief set callback to joystick buttons
* @param callback: what will be called after button be pressed
* @param controller: controller own callback
*/
void Hal::set_keyboard_callback(callback_cursor_move callback, IController* controller)
{
m_callback = callback;
m_controller = controller;
}
/**
* @brief mode activation, relaod UI and releted hal part
*/
void Color_tester_controller::active()
{
...
m_hal.set_keyboard_callback(&IController::keyboar_reaction, this);
}
Jeśli macie jeszcze jakieś pomysły co do kodu lub pytania, piszcie śmiało 🙂