Powrót do strony głównej

Jednolity silnik obliczeń dla Next.js i Flutter

Artykuł o tym, jak autor zsynchronizował logikę obliczeń między stroną www na Next.js a aplikacją mobilną na Flutter. Opisany jest podejście z wykorzystaniem specyfikacji JSON jako jednego źródła prawdy, kodogeneracji i testów parytetowych w celu zapewnienia identyczności wyników.

Jak jeden plik JSON uratował dwa runtime'y przed rozbieżnościami
Advertisement 728x90

# 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.

Google AdInline article slot

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:

Google AdInline article slot
  • 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.

Google AdInline article slot

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

Advertisement 728x90

Czytaj dalej