Technologia · PRO

6533 linie w jednym pliku.
Dzień, w którym przestałem udawać, że to jest w porządku.

PC Workman trafił do Microsoft Store. Skrypt do pilnowania mojego komputera stał się programem, który uruchamiają inni. Po tym przyszło sprzątanie.

Autor: Marcin Firmuga·2026-07-21·9 min czytania·Technologia

Zawodowo zgrzewam plastik. Nie w czystym laboratorium z reklamy, tylko nad gorącym pistoletem, w pomieszczeniu, które już samo z siebie jest za ciepłe. Łączę części, które po pracy nie mają się rozpaść. Wieczór jest drugą połową dnia. Wtedy powstaje PC Workman.

Projekt zaczął się od małego skryptu obserwującego przegrzewający się komputer. Nie było pieniędzy na zgadywanie, dlaczego laptop obniża taktowanie. Później doszedł lokalny asystent hck_GPT, model temperatur uczący się konkretnej maszyny i w końcu publikacja w Microsoft Store.

Sklep nie zmienił kodu. Zmienił sposób, w jaki go czytałem

W dniu publikacji żaden plik w repozytorium nie poruszył się sam. Nadal były te same funkcje i ten sam autor. Zmieniła się odpowiedzialność. Ktoś w innym kraju mógł nacisnąć Instaluj i uruchomić program na własnym zasilaczu, własnym CPU i własnych temperaturach. Projekt przestał być prywatną zabawką.

Z takim momentem przychodzi rachunek. Gdy widzisz kod jako coś, od czego zależy obca osoba, trudniej po raz kolejny przewinąć plik omijany od roku.

Plik, którego nie chciałem otwierać

Nazywał się builder.py. Jedna klasa, ResponseBuilder, miała 6533 linie. Była sercem asystenta: otrzymywała rozpoznaną intencję, na przykład pytanie o temperaturę albo możliwość uruchomienia gry, i budowała dwujęzyczną odpowiedź z bieżących danych. Istniało 96 handlerów, po jednym dla każdej intencji. Wszystkie mieszkały w tej samej klasie i w tym samym pliku.

Nie stało się to wskutek jednej złej decyzji. Każda nowa odpowiedź dodawała kilkanaście lub kilkadziesiąt linii. Taka zmiana nigdy nie wygląda groźnie. Powtórzona wiele razy kończy się plikiem, przy którego otwieraniu edytor zatrzymuje się na chwilę.

Koszt monolitu nie jest widoczny na ekranie aplikacji. Każda zmiana dotyka ściany niepowiązanego kodu, więc część potrzebnych zmian odkładasz. Dwie osoby, albo jedna osoba i jej kod sprzed pół roku, częściej wchodzą sobie w drogę. Błąd łatwo ukrywa się w miejscu, którego nikt nie potrafi objąć jednym czytaniem.

Plik, którego boisz się otworzyć, jest plikiem, który już przestałeś ulepszać.

Fasada ukrywa, gdzie naprawdę mieszka kod

Cała aplikacja rozmawiała z builderem przez jedno wywołanie: response_builder.build(result, lang). Używał go panel czatu, monitor proaktywny i testy. Nie chciałem zmieniać tego kontraktu. Refaktoryzacja wymagająca poprawienia każdego klienta byłaby osobnym projektem.

Klasa została więc na swoim miejscu, ale jej ciało przeniosło się do mniejszych modułów. builder.py stał się fasadą, która nadal odpowiada na każde stare wywołanie:

class ResponseBuilder(HardwareResponses, UpgradeResponses, ThermalResponses,
                      GamingResponses, SystemResponses, PerformanceResponses,
                      InsightsResponses, AssistantResponses):
    """Builds bilingual responses for every parsed intent."""

Każdy rodzic jest mixinem zawierającym jedną kategorię odpowiedzi. HardwareResponses obsługuje CPU, GPU, RAM i dyski. ThermalResponses skupia temperatury, wentylatory oraz linie bazowe napięć. Dziedziczenie składa je w jedną klasę, więc z zewnątrz ResponseBuilder nadal ma wszystkie 96 metod.

Dispatch nie zmienił ani jednego znaku

Sposób zamiany intencji na odpowiedź pozostał ten sam:

intent  = self._INTENT_ALIASES.get(result.intent, result.intent)
handler = getattr(self, f"_resp_{intent}", None)
if handler is None:
    return None
out = handler(result, lang)

getattr wyszukuje metodę po nazwie w trakcie działania. Nie interesuje go plik, w którym metoda została zapisana. Kolejność rozwiązywania metod Pythona przechodzi przez mixiny i znajduje właściwy handler. Intencja temperature nadal trafia do _resp_temperature, choć metoda przeniosła się do r_thermal.py. Dla wywołujących zmiana jest niewidoczna.

Podział według odpowiedzialności, nie liczby linii

Nie ciąłem pliku na równe kawałki. Handlery trafiły do grup odpowiadających temu, jak szukam ich podczas pracy:

ModułHandleryZakres
r_hardware.py25CPU, GPU, RAM, dyski, płyta i pełna specyfikacja
r_insights.py15trendy, porównania, podsumowania sesji i zmiany
r_performance.py13obciążenie, throttling i przyczyny spowolnień
r_system.py13kondycja, procesy, autostart i bezpieczeństwo
r_thermal.py11temperatury, wentylatory i napięcia
r_assistant.py11powitania, pomoc, informacje i pamięć odpowiedzi
r_gaming.py6wymagania gier oraz czas gry i pracy
r_upgrade.py2lokalna zgodność podzespołów

Wspólne funkcje pomocnicze trafiły do common.py, a rozmowy prowadzone krok po kroku do flows.py. Dawny plik z 6533 liniami ma teraz mniej niż 600 i robi jedną rzecz: składa mixiny oraz kieruje żądania.

Najmłodszy r_upgrade.py dostał własny moduł od pierwszej linii. Ma dziś dwa handlery i miejsce na rozwój. Wcześniej wkleiłbym nową funkcję do pliku, który akurat był otwarty. Tym razem funkcja od początku dostała adres.

Dwa testy, aby monolit nie odrósł

Sam podział trwał jedno popołudnie. Utrzymanie granic jest trudniejsze, bo presja, która stworzyła pierwszy monolit, nie zniknęła. Kolejny handler zawsze łatwiej wkleić niż zastanowić się nad miejscem. Dlatego dwie reguły zatrzymują build, zamiast liczyć na pamięć autora.

Pierwsza ogranicza każdy moduł odpowiedzi do 1600 linii:

def test_monolith_guard(self):
    for p in glob.glob(_RESPONSES_GLOB):
        n = _read(p).count("\n") + 1
        self.assertLess(n, 1600,
            f"{os.path.basename(p)} has {n} lines - split it "
            f"instead of growing another monolith")

Po przekroczeniu granicy test nie tylko zgłasza błąd. Mówi przyszłemu autorowi, co ma zrobić: podzielić moduł, zamiast budować następny monolit.

Drugi test pilnuje ostrej krawędzi mixinów. Gdy dwa z nich zdefiniują metodę o tej samej nazwie, jeden może cicho wygrać przez kolejność dziedziczenia. Test zbiera definicje _resp_* ze wszystkich modułów i odrzuca duplikaty:

all_defs = []
for p in glob.glob(_RESPONSES_GLOB):
    all_defs += re.findall(r'def (_resp_[a-z0-9_]+)\(', _read(p))
dupes = {d for d in all_defs if all_defs.count(d) > 1}
self.assertEqual(dupes, set())
Podział zajął jedno popołudnie. Dwa testy są powodem, dla którego za rok nadal będzie podziałem. Refaktoryzacja bez mechanizmu blokującego regresję tylko odsuwa problem.

To nie była jedyna dojrzałościowa zmiana

Strona Mój PC otwierała się przez około dwie sekundy. Po zmianie wraca w 1-17 ms. Przyczyną były dwa wywołania wmic wykonywane na wątku interfejsu, każde z limitem trzech sekund. Tożsamość sprzętu jest teraz pobierana raz przy uruchomieniu, a strona pozostaje zbudowana zamiast powstawać od nowa przy każdym wejściu.

Elementy podrzędne paska bocznego powstają dopiero przy pierwszym otwarciu sekcji. Zimny start okna spadł do około 190 ms. Numer wersji, wcześniej powielony w ośmiu plikach i już niespójny, znajduje się teraz w jednym źródle. Test odrzuca każdą nową, wpisaną na sztywno kopię. Zestaw testów wzrósł z 21 w czerwcu do 194.

Nie są to funkcje widoczne na zrzucie ekranu. Są pracą, która obniża koszt każdej następnej zmiany. Refaktoryzacja ma też cenę: przez kilka tygodni użytkownik nie zobaczy nowego przycisku, a przeniesienie sześciu tysięcy linii może wprowadzić błąd właśnie dlatego, że ma nie zmieniać zachowania. Każdy etap przeszedł więc przez testy publicznego wywołania.

Nie ufam własnej pamięci dotyczącej pliku z 6533 liniami. Ufam małym, sprawdzalnym krokom. Alternatywą był plik rosnący do momentu, w którym nikt nie chce go dotknąć, wewnątrz programu instalowanego przez obcych ludzi.

Przy zgrzewaniu dobra robota rzadko jest efektowną częścią. To połączenie, które później się nie rozpada. W oprogramowaniu jest podobnie. Najważniejsza zmiana tej wersji nie zostawia śladu na ekranie: 6533 linie stały się ośmioma modułami, które można otworzyć bez pauzy i zmieniać bez strachu.

Microsoft Store nie zrobił z PC Workmana prawdziwego projektu. Pokazał mi tylko lustro, a ja w końcu posprzątałem to, co w nim zobaczyłem.

Paragon Artykuł PRO · ciche awarie Najgroźniejszy błąd nie wywala programu. On kończy się sukcesem. Sześć prawdziwych awarii, które działały bez zarzutu i nie robiły nic, tydzień z asystentem piszącym szybciej, niż zdążyłem czytać, i pięć kontroli, które to łapią. Przeczytaj artykuł →
To działający kod. Podział buildera, oba testy granic i przyspieszenie strony Mój PC są częścią PC Workmana 1.8.5. Projekt jest darmowy i otwartoźródłowy. Pobierz program albo przeczytaj kod. Powiązane: czego komputer nauczył się o sobie i dlaczego stałe progi nie wystarczają.
MF

Marcin Firmuga

Solo developer · HCK_Labs · publicznie buduje PC Workmana

Piszę o tym, co faktycznie trafiło do kodu, z liczbami i fragmentami, które można sprawdzić. Więcej: moja historia.