Frankenstein w programowaniu to kod złożony z niekompatybilnych części różnych technologii, stylów i architektur. Według badania ThoughtWorks Technology Radar (2024), 28% dużych projektów zawiera objawy syndromu Frankensteina — architektonicznej eklektyki, powstającej przy braku jednolitej wizji technicznej. Analogicznie do powieści Mary Shelley, taki kod działa, ale jego utrzymanie staje się koszmarem.
Najważniejsze
Frankenstein (Frankenstein code, Frankenstein pattern) — to antywzorzec, w którym system oprogramowania składany jest z części nieprzeznaczonych do współpracy. Jak potwór Frankensteina, taki kod może działać, ale jest brzydki, nieprzewidywalny i niebezpieczny przy najmniejszych zmianach.
Termin pochodzi z literatury: w powieści Mary Shelley „Frankenstein, czyli współczesny Prometeusz” (1818) naukowiec stworzył żywą istotę z fragmentów ciał różnych zmarłych osób. W programowaniu analogia jest trafna — programiści biorą fragmenty różnych frameworków, bibliotek, języków i sklejają je „na żywca”, uzyskując działający, ale potworny efekt.
Różnica między Frankensteinem a kodem spaghetti polega na skali i naturze problemu. Kod spaghetti — to zagmatwana struktura w ramach jednego stosu technologicznego. Frankenstein — to eklektyka na poziomie architektury: różne technologie, niekompatybilne paradygmaty, konfliktowe podejścia w ramach jednego systemu.
Architektura mikrousług dopuszcza użycie różnych technologii dla różnych usług, ale pod warunkiem wyraźnych granic i standaryzowanych protokołów komunikacji. Frankenstein — to chaotyczne mieszanie bez granic: REST i GraphQL w jednym kontrolerze, dwa ORM-y w jednym module, SQL i NoSQL dla jednej encji.
Brak lidera technicznego lub architekta — to główna przyczyna. Gdy w projekcie nie ma osoby odpowiedzialnej za spójność architektury, każdy programista wybiera narzędzia „pod siebie”. Jeden lubi Spring, drugi — Guice, trzeci — własnoręcznie napisany DI. Efekt — architektoniczny winegret.
Łączenie projektów — druga częsta przyczyna. Dwa zespoły rozwijały swoje moduły niezależnie, używając różnych stosów technologicznych. Gdy moduły trzeba połączyć w jedną aplikację, są one po prostu „sklejane” adapterami i przekładkami. Powstaje Frankenstein.
Przejęcia korporacyjne — trzeci scenariusz. Firma A kupiła firmę B i chce zintegrować jej produkt ze swoim. Zamiast przepisywania — sklejanie przez API, wspólne bazy danych i protezy. Po roku system zmienia się w potwora, którego nikt nie rozumie.
| Przyczyna | Opis | Typowy rezultat |
|---|---|---|
| Brak architekta | Każdy programista wybiera własny stos | 3 różne klienty HTTP w jednym module |
| Łączenie projektów | Dwa produkty sklejane w jeden | Dwa ORM-y, dwa sposoby logowania |
| M&A | Przejęcie firmy z jej produktem | Hybryda różnych architektur i stylów |
| Eksperymenty | Wdrażanie nowych technologii bez strategii | Java 8 + Java 21 w jednym pliku |
| Decyzje polityczne | Narzucanie technologii z góry bez uwzględnienia kontekstu | Enterprise-framework dla prostego skryptu |
Doświadczeni programiści, którzy chcą wypróbować nowe technologie w produkcji, często stają się źródłem Frankensteina. Zamiast ograniczyć eksperymenty do izolowanego modułu, wdrażają eksperymentalny kod w krytycznej części systemu.
Klasyczny przykład — użycie kilku ORM-ów w jednej aplikacji. Część modułów używa Hibernate, część — MyBatis, a część — bezpośrednich zapytań JDBC. Transakcje stają się niezarządzalne, pamięć podręczna — niespójna, a nowy programista nie wie, które podejście wybrać dla nowej funkcji.
Drugi przykład — mieszanie stylów architektonicznych. W kontrolerze REST API pojawiają się wywołania usług SOAP, bezpośrednie zapytania SQL, dostęp do systemu plików i generacja HTML. Takiej aplikacji nie da się testować, rozszerzać ani dokumentować.
Trzeci przykład — stos technologiczny, gdzie Python jest używany do backendu, Node.js — do mikrousługi, C# — do klienta desktopowego, a Java — do aplikacji Android, przy czym cała logika biznesowa jest rozrzucona między nimi bez wyraźnego podziału odpowiedzialności.
// frankenstein — mieszane style i technologie
// połączenie callbacks, Promises i async/await
// callbacks
db.query("SELECT * FROM users", function(err, rows) {
if (err) handleError(err);
// Promise wewnątrz callback
fetch("/api/data").then(function(data) {
// async/await wewnątrz then
(async () => {
const result = await processData(data);
sendResponse(result);
})();
});
});
// czysty kod — ujednolicony styl async/await
async function getUserData(userId) {
const user = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
const data = await fetch("/api/data/" + userId);
return await processData(user, data);
}
Jedna baza danych jest używana jednocześnie jako relacyjna SQL (z normalizacją) i jako dokumentowa NoSQL (z kolumnami JSON). Część zapytań idzie przez ORM, część — przez procedury składowane, część — przez bezpośredni SQL z kodu. Schemat bazy nie jest udokumentowany, migracje są konfliktowe.
Trudność onboardingu — pierwsza konsekwencja. Nowy programista musi znać 5 języków, 3 frameworki, 2 style architektoniczne, aby zrozumieć, jak działa system. Onboarding wydłuża się z tygodni do miesięcy. Według LinkedIn (2023), projekty z eklektyką technologiczną tracą nowych pracowników 2 razy częściej.
Nieprzewidywalność zachowania — druga konsekwencja. Zmiana w mikrousłudze w Pythonie może nieoczekiwanie złamać moduł w Javie, ponieważ używają one wspólnej bazy danych bez wyraźnych kontraktów. Debugowanie takich problemów wymaga jednoczesnej znajomości wszystkich technologii w stosie.
Bezpieczeństwo — trzecia konsekwencja. Każda technologia w stosie wymaga własnej konfiguracji bezpieczeństwa, własnych łatek, własnego monitoringu. Utrzymanie bezpieczeństwa na akceptowalnym poziomie dla 5–6 różnorodnych technologii jest praktycznie niemożliwe. Jedna z nich nieuchronnie okaże się podatna na ataki.
SonarQube może zmierzyć dług techniczny, ale nie może zmierzyć „długu architektonicznego” — niekompatybilności komponentów. Ten dług objawia się nie w ostrzeżeniach lintera, ale w niemożliwości dodania nowej funkcji bez modyfikacji trzech różnych modułów napisanych w różnych technologiach.
Pierwszy i najważniejszy krok — wyznaczenie architekta lub tech leada odpowiedzialnego za spójność stosu technologicznego. Ta osoba ma prawo weta wobec wdrażania nowych technologii bez przeglądu architektonicznego. Nie demokracja, a odpowiedzialna jednoosobowa decyzja w kwestii kluczowych technologii.
Drugi — wdrożenie procesu Architecture Decision Record (ADR). Każda znacząca decyzja architektoniczna (wybór bazy danych, frameworka, protokołu) jest dokumentowana w formie krótkiego tekstu: kontekst, rozważane alternatywy, podjęta decyzja, konsekwencje. ADR są przechowywane w repozytorium i dostępne dla całego zespołu.
Trzeci — ustanowienie zasady „jedno zadanie — jedno narzędzie”. Dla zapytań HTTP — jeden klient. Dla ORM — jedna biblioteka. Dla logowania — jeden framework. Wyjątki są dozwolone tylko przez ADR z uzasadnieniem. Jeśli w projekcie jest już Axios — nie dodawaj fetch, jeśli jest SLF4J — nie pisz przez System.out.
Eksperymenty są dozwolone, ale w izolowanym środowisku. Wydziel moduł lub usługę, którą można przepisać na nową technologię bez wpływu na resztę systemu. Jeśli eksperyment się uda — ustandaryzuj go przez ADR. Jeśli nie — usuń bez konsekwencji.
Inwentaryzacja — pierwszy krok. Sporządź pełną mapę stosu technologicznego: jakie frameworki, biblioteki, języki, protokoły są używane, w jakich modułach i do jakich zadań. Zobaczysz skalę problemu: duplikowanie narzędzi, konfliktujące technologie, nieużywane zależności.
Standaryzacja — drugi krok. Wybierz jeden instrument dla każdego zadania. Na przykład: tylko Hibernate dla ORM, tylko SLF4J + Logback dla logowania, tylko REST dla API. Udokumentuj standard w ADR. Rozpocznij wymianę od modułów, w których eklektyka przynosi najwięcej problemów.
Strategia Parallel Run — trzeci krok. Stare i nowe narzędzia działają równolegle, aż nowe udowodni swoją niezawodność. Na przykład, stary i nowy klient HTTP działają jednocześnie, ale nowy — tylko dla części zapytań. Po okresie stabilizacji stary jest usuwany.
// frankenstein — trzy podejścia HTTP w jednym projekcie
// Moduł A: OkHttp
OkHttpClient client = new OkHttpClient().newCall(request);
// Moduł B: RestTemplate (Spring)
restTemplate.getForObject(url, String.class);
// Moduł C: java.net.HttpURLConnection
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
// ujednolicone podejście: RestTemplate dla synchronicznego, WebClient dla reaktywnego
@Autowired
private RestTemplate restTemplate;
public String callApi(String url) {
return restTemplate.getForObject(url, String.class);
}
Lider techniczny — główne narzędzie walki z Frankensteinem. Nie menedżer, nie architekt w wieży z kości słoniowej, ale praktykujący programista, który pisze kod, przegląda PR-y i podejmuje decyzje architektoniczne. Bez takiej osoby projekt nieuchronnie stacza się w technologiczną eklektykę.
RFC (Request for Comments) — proces zapożyczony ze społeczności Open Source. Przed wdrożeniem każdej znaczącej technologii autor pisze RFC: problem, proponowane rozwiązanie, alternatywy, plan wdrożenia. Zespół dyskutuje, głosuje, przyjmuje lub odrzuca. RFC tworzy przejrzystość i zapobiega „cichym” decyzjom architektonicznym.
Radar technologiczny (Technology Radar od ThoughtWorks) — narzędzie kategoryzacji technologii: Adopt, Trial, Assess, Hold. Zespół regularnie przegląda radar i aktualizuje statusy. Pomaga to odróżniać „modne” od „użytecznego” i unikać wdrażania niesprawdzonych technologii w krytycznym kodzie.
Najważniejszą cechą architektury jest spójność (consistency). Nawet nie najlepsze narzędzie, używane w całym projekcie, jest lepsze niż najlepsze narzędzie używane tylko w jednym module. Spójność zmniejsza obciążenie poznawcze, upraszcza onboarding i czyni kod przewidywalnym.
Często zadawane pytania
Polyglot persistence — świadome użycie różnych baz danych do różnych zadań (PostgreSQL dla transakcji, Redis dla pamięci podręcznej, Elasticsearch dla wyszukiwania). Frankenstein — chaotyczne mieszanie bez strategii. Różnica polega na istnieniu decyzji architektonicznej: polyglot — to plan, Frankenstein — jego brak.
Tak, i to częsty problem. Gdy każda mikrousługa używa własnego języka, własnej bazy danych, własnego protokołu i własnego podejścia do wdrażania bez scentralizowanych standardów — powstaje rozproszony Frankenstein. Dla mikrousług ważne są wspólne standardy: jednolity protokół (REST/gRPC), wspólny format logów, scentralizowane observability.
Nie zakazuj — kieruj. Zaproponuj autorowi RFC: opisz, dlaczego istniejące rozwiązanie nie pasuje, jakie alternatywy rozważyłeś, jak będziesz migrować. Często w trakcie pisania RFC programista sam rozumie, że nowa technologia nie jest potrzebna. Jeśli RFC jest przekonujące — wdrażaj, ale z planem i ograniczeniami.
Najpierw inwentaryzacja, potem standaryzacja. Nie próbuj przepisać wszystkiego naraz. Wyodrębnij jedną warstwę (na przykład klienty HTTP lub logowanie), wybierz jednolite narzędzie, napisz ADR i migruj stopniowo. Metoda Strangler Fig — zastępuj stare komponenty nowymi pojedynczo, bez zatrzymywania aplikacji.
Im mniej, tym lepiej. Idealnie — jeden język, jeden framework, jedna baza danych, jeden sposób logowania. Realistycznie — 2–3 języki (przy wyraźnym podziale), 1–2 bazy danych, 1–2 frameworki. Każda dodatkowa technologia zwiększa obciążenie poznawcze zespołu i koszt utrzymania.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również