# Jeden silnik obliczeniowy dla Next.js i Flutter: jak uniknąć rozbieżności w liczbach
Kiedy ten sam kalkulator budowlany istnieje jednocześnie jako aplikacja mobilna na Flutter i strona internetowa na Next.js, nawet niewielkie różnice w formułach mogą podważyć zaufanie użytkowników. Rzeczywisty przypadek pokazał: jeśli w aplikacji i na stronie podawane są różne objętości betonu pod słupek ogrodzenia, to nie jest to po prostu błąd — to systemowy problem duplikowania logiki. Rozwiązanie — pojedyncze źródło prawdy oparte na deklaratywnych specyfikacjach JSON i testach parytetowych.
Problem dwóch niezależnych baz kodu
Początkowo logika obliczeń była zaimplementowana dwukrotnie: w Dart w aplikacji Flutter i w TypeScript na stronie Next.js. Oba projekty rozwijały się równolegle, ale niezależnie. Współczynniki, wartości domyślne (np. średnica dołu pod słupek), reguły zaokrąglania i pakowania materiałów żyły w kodzie każdego runtime'u osobno. W rezultacie — rozbieżności w wynikach przy tych samych danych wejściowych.
Taka architektura narusza zasadę DRY (Don’t Repeat Yourself) i generuje dług techniczny, który prędzej czy później ujawni się w postaci skarg użytkowników. Szczególnie krytyczne jest to dla narzędzi niszowych, gdzie precyzja obliczeń bezpośrednio wpływa na zaufanie do produktu.
Deklaratywny podejście: JSON jako źródło prawdy
Zamiast duplikować logikę biznesową, wszystkie zmienne parametry zostały wyodrębnione do strukturyzowanych plików JSON — po jednym na kalkulator. Każda specyfikacja zawiera:
- Wartości domyślne dla pól wejściowych (np. średnica pale — 150 mm)
- Reguły zaokrąglania (zawsze w górę)
- Współczynniki strat w zależności od trybu precyzji
- Informacje o pakowaniu materiałów (worki po 25 kg, wydajność 0,01 m³/work)
- Minimalne grubości warstw i rozstawy mocowań
Te pliki JSON stały się jedynym źródłem prawdy. Teraz każde change parametrów wprowadza się raz — w JSON — i automatycznie stosuje w obu runtime'ach.
Kodogeneneracja dla synchronizacji
Aby zagwarantować, że oba projekty używają tych samych danych, wdrożono proces kodogeneneracji:
- Przy zmianie specyfikacji JSON uruchamiany jest skrypt
npm run sync:specs - Skrypt odczytuje wszystkie specyfikacje i generuje plik Dart z mapą stałą (
const Map) - Wygenerowany plik jest commitowany do repozytorium aplikacji Flutter
- CI/CD obu systemów sprawdza zmiany i wdraża zaktualizowane wersje
Wygenerowany plik jest oznaczony jako @generated, a ręczna edycja zabroniona przez linter. To eliminuje błędy ludzkie i gwarantuje identyczność danych.
Testy parytetowe: ochrona przed rozbieżnościami w logice
Nawet przy identycznych danych możliwe są różnice w zachowaniu z powodu cech języków: Math.ceil() w TypeScript i num.ceil() w Dart mogą działać inaczej na wartościach brzegowych. Aby temu zapobiec, wdrożono system testów parytetowych:
- Utworzono zestaw fixture'ów — dziesiątki kombinacji danych wejściowych dla każdego z 61 kalkulatorów
- Silnik TypeScript jest uruchamiany po wszystkich fixture'ach, wyniki zapisywane do pliku referencyjnego
- Ten sam plik używany jest w repozytorium Flutter do porównania z wynikami silnika Dart
- Jakakolwiek rozbieżność — nawet w ostatnim znaku po przecinku — powoduje upadek CI
Przez pół roku takie testy wykryły trzy rzeczywiste przypadki rozbieżności, które bez automatycznej weryfikacji mogłyby pozostać niezauważone.
Trzy tryby precyzji zamiast „plus 10%"
Tradycyjne kalkulatory często stosują uniwersalny współczynnik zapasu (np. +10%). To uproszczenie wprowadza niedokładności: płytki rzeczywiście potrzebują zapasu na docinanie, ale tynk — na kompensację nierówności podłoża, a klej — na chłonność powierzchni.
Zamiast tego zaimplementowano trzy tryby obliczeń:
- Podstawowy — obliczenia normatywne bez zapasów, tylko według PN
- Realistyczny (domyślny) — uwzględnia typowe warunki remontu w mieszkaniu
- Profesjonalny — maksymalny zapas dla skomplikowanych obiektów i gwarancji braku niedoborów
Współczynniki dla każdego trybu przechowywane są również w specyfikacjach JSON, co pozwala na ich zmianę bez przebudowy aplikacji.
Ważna reguła: dla identycznych danych wejściowych wyniki muszą spełniać nierówność: Podstawowy ≤ Realistyczny ≤ Profesjonalny. Osobny test CI weryfikuje to dla wszystkich kalkulatorów.
Walidatory jako drugi poziom ochrony
Oprócz testów jednostkowych i parytetowych, w CI działają trzy specjalistyczne walidatory:
- Walidator logiczny: sprawdza, czy ilość do zakupu ≥ czystej ilości, wyniki nie zerują się przy poprawnych wejściach, etykiety używają polskich jednostek miary („m²”, a nie „m²")
- Walidator pakowania: upewnia się, że materiały pakowane są realistycznie (klej — w wiadrach po 5/10/15 l, płytki — w paczkach, a nie sztukami)
- Walidator jednostek: łapie mieszanie systemów metrycznych (np. dodawanie milimetrów do metrów bez konwersji)
Te skrypty działają jak „bramka magnetyczna” na wejściu do systemu — nie zastępują testów jednostkowych, ale zapobiegają głupim, lecz krytycznym błędom na poziomie całej ekosystemu.
Co najważniejsze
- Pojedyncze źródło prawdy w postaci specyfikacji JSON eliminuje duplikowanie parametrów między runtime'ami.
- Kodogeneneracja zapewnia synchronizację danych bez ręcznej interwencji.
- Testy parytetowe gwarantują identyczność wyników w Dart i TypeScript.
- Trzy tryby precyzji zastępują uproszczone „+10%" i dają użytkownikowi kontrolę nad poziomem zapasu.
- Specjalistyczne walidatory w CI chronią przed błędami systemowymi, które testy jednostkowe mogą przeoczyć.
— Editorial Team
Brak komentarzy.