[{"data":1,"prerenderedAt":1011},["ShallowReactive",2],{"post-ddd-eda-w-systemie-iot":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"author":11,"tags":12,"readingTime":19,"body":20,"_type":1005,"_id":1006,"_source":1007,"_file":1008,"_stem":1009,"_extension":1010},"\u002Fblog\u002Fddd-eda-w-systemie-iot","blog",false,"","DDD i EDA w systemie IoT, w którym błąd zabija roślinę","Jak zaprojektowaliśmy huba do ochrony roślin przed mrozem: bounded contexts, event bus z priorytetami i silnik reguł jako czysta funkcja. Architektura, w której safety jest wymuszone strukturą, nie dobrą wolą.","2026-07-07","Kamil Sułek",[13,14,15,16,17,18],"DDD","EDA","SOLID","architektura","Go","IoT",7,{"type":21,"children":22,"toc":999},"root",[23,39,71,78,97,152,193,325,360,366,378,434,481,487,498,694,699,746,809,897,916,963,969,981,993],{"type":24,"tag":25,"props":26,"children":27},"element","p",{},[28,31,37],{"type":29,"value":30},"text","W większości systemów źle postawiona granica między modułami kosztuje refaktor.\nW systemie, który w nocy decyduje, czy włączyć grzałkę pod palmą przy −12 °C, kosztuje\n",{"type":24,"tag":32,"props":33,"children":34},"strong",{},[35],{"type":29,"value":36},"martwą roślinę u klienta",{"type":29,"value":38},". To zmienia sposób, w jaki się projektuje — architektura\nprzestaje być estetyką, a staje się mechanizmem bezpieczeństwa.",{"type":24,"tag":25,"props":40,"children":41},{},[42,44,49,51,56,58,63,65],{"type":29,"value":43},"Poniżej trzy decyzje, które w naszym hubie (Go, lokalny-first, na Raspberry Pi) niosą\nnajwięcej: ",{"type":24,"tag":32,"props":45,"children":46},{},[47],{"type":29,"value":48},"bounded contexts",{"type":29,"value":50}," z DDD, ",{"type":24,"tag":32,"props":52,"children":53},{},[54],{"type":29,"value":55},"event bus z priorytetami",{"type":29,"value":57}," z EDA, i silnik reguł\njako ",{"type":24,"tag":32,"props":59,"children":60},{},[61],{"type":29,"value":62},"czysta funkcja",{"type":29,"value":64},". Wszystkie sprowadzają się do jednego: ",{"type":24,"tag":66,"props":67,"children":68},"em",{},[69],{"type":29,"value":70},"każdą drogę do złego\nzachowania system ma odciętą strukturą, nie dyscypliną — złej decyzji trzeba by chcieć.",{"type":24,"tag":72,"props":73,"children":75},"h2",{"id":74},"bounded-contexts-granica-której-nie-da-się-przypadkiem-przekroczyć",[76],{"type":29,"value":77},"Bounded contexts: granica, której nie da się przypadkiem przekroczyć",{"type":24,"tag":25,"props":79,"children":80},{},[81,83,88,90,95],{"type":29,"value":82},"Domenę pocięliśmy na pięć ",{"type":24,"tag":32,"props":84,"children":85},{},[86],{"type":29,"value":87},"bounded contextów",{"type":29,"value":89}," — pięć modeli w jednym binarium\n(modularny monolit na Pi), gdzie granica kontekstu to granica pakietu, nie procesu.\nKażdy ma własny ",{"type":24,"tag":32,"props":91,"children":92},{},[93],{"type":29,"value":94},"ubiquitous language",{"type":29,"value":96}," — własne pojęcia, których sąsiedni kontekst nie\nwspółdzieli (na granicy się je tłumaczy, nie przenosi):",{"type":24,"tag":98,"props":99,"children":100},"ul",{},[101,112,122,132,142],{"type":24,"tag":102,"props":103,"children":104},"li",{},[105,110],{"type":24,"tag":32,"props":106,"children":107},{},[108],{"type":29,"value":109},"Device",{"type":29,"value":111}," — fizyczne urządzenia i protokoły. Zna „kanał\", „odczyt\", nie zna „palmy\".",{"type":24,"tag":102,"props":113,"children":114},{},[115,120],{"type":24,"tag":32,"props":116,"children":117},{},[118],{"type":29,"value":119},"Station",{"type":29,"value":121}," — stanowisko jako kontener logiczny. Most między fizycznym a domenowym.",{"type":24,"tag":102,"props":123,"children":124},{},[125,130],{"type":24,"tag":32,"props":126,"children":127},{},[128],{"type":29,"value":129},"Monitoring",{"type":29,"value":131}," — zbieranie i walidacja pomiarów (świeżość, sanity bounds).",{"type":24,"tag":102,"props":133,"children":134},{},[135,140],{"type":24,"tag":32,"props":136,"children":137},{},[138],{"type":29,"value":139},"Protection",{"type":29,"value":141}," — reguły ochrony: kiedy i czym grzać.",{"type":24,"tag":102,"props":143,"children":144},{},[145,150],{"type":24,"tag":32,"props":146,"children":147},{},[148],{"type":29,"value":149},"Identity",{"type":29,"value":151}," — konta i uprawnienia.",{"type":24,"tag":25,"props":153,"children":154},{},[155,157,162,164,170,172,177,179,184,186,191],{"type":29,"value":156},"Reguła, której ",{"type":24,"tag":32,"props":158,"children":159},{},[160],{"type":29,"value":161},"nie wolno naruszyć",{"type":29,"value":163},": ",{"type":24,"tag":165,"props":166,"children":168},"code",{"className":167},[],[169],{"type":29,"value":139},{"type":29,"value":171}," nie importuje niczego z ",{"type":24,"tag":165,"props":173,"children":175},{"className":174},[],[176],{"type":29,"value":109},{"type":29,"value":178},".\nSilnik decydujący o grzaniu nie wie, że po drugiej stronie jest przekaźnik Sonoff czy\nTasmota — dostaje odczyt w kontekście ",{"type":24,"tag":66,"props":180,"children":181},{},[182],{"type":29,"value":183},"roli",{"type":29,"value":185}," stanowiska i zwraca ",{"type":24,"tag":66,"props":187,"children":188},{},[189],{"type":29,"value":190},"intencję",{"type":29,"value":192},":",{"type":24,"tag":194,"props":195,"children":199},"pre",{"className":196,"code":197,"language":198,"meta":7,"style":7},"language-go shiki shiki-themes github-dark","\u002F\u002F Granica Protection → Station: intencja, nie sterownik.\ntype ControlCommand struct {\n    StationID   string\n    Action      ControlAction \u002F\u002F turnOn \u002F turnOff \u002F boost\n    Reason      string\n    RequestedAt time.Time     \u002F\u002F UTC\n}\n","go",[200],{"type":24,"tag":165,"props":201,"children":202},{"__ignoreMap":7},[203,215,242,256,275,288,317],{"type":24,"tag":204,"props":205,"children":208},"span",{"class":206,"line":207},"line",1,[209],{"type":24,"tag":204,"props":210,"children":212},{"style":211},"--shiki-default:#6A737D",[213],{"type":29,"value":214},"\u002F\u002F Granica Protection → Station: intencja, nie sterownik.\n",{"type":24,"tag":204,"props":216,"children":218},{"class":206,"line":217},2,[219,225,231,236],{"type":24,"tag":204,"props":220,"children":222},{"style":221},"--shiki-default:#F97583",[223],{"type":29,"value":224},"type",{"type":24,"tag":204,"props":226,"children":228},{"style":227},"--shiki-default:#B392F0",[229],{"type":29,"value":230}," ControlCommand",{"type":24,"tag":204,"props":232,"children":233},{"style":221},[234],{"type":29,"value":235}," struct",{"type":24,"tag":204,"props":237,"children":239},{"style":238},"--shiki-default:#E1E4E8",[240],{"type":29,"value":241}," {\n",{"type":24,"tag":204,"props":243,"children":245},{"class":206,"line":244},3,[246,251],{"type":24,"tag":204,"props":247,"children":248},{"style":238},[249],{"type":29,"value":250},"    StationID   ",{"type":24,"tag":204,"props":252,"children":253},{"style":221},[254],{"type":29,"value":255},"string\n",{"type":24,"tag":204,"props":257,"children":259},{"class":206,"line":258},4,[260,265,270],{"type":24,"tag":204,"props":261,"children":262},{"style":238},[263],{"type":29,"value":264},"    Action      ",{"type":24,"tag":204,"props":266,"children":267},{"style":227},[268],{"type":29,"value":269},"ControlAction",{"type":24,"tag":204,"props":271,"children":272},{"style":211},[273],{"type":29,"value":274}," \u002F\u002F turnOn \u002F turnOff \u002F boost\n",{"type":24,"tag":204,"props":276,"children":278},{"class":206,"line":277},5,[279,284],{"type":24,"tag":204,"props":280,"children":281},{"style":238},[282],{"type":29,"value":283},"    Reason      ",{"type":24,"tag":204,"props":285,"children":286},{"style":221},[287],{"type":29,"value":255},{"type":24,"tag":204,"props":289,"children":291},{"class":206,"line":290},6,[292,297,302,307,312],{"type":24,"tag":204,"props":293,"children":294},{"style":238},[295],{"type":29,"value":296},"    RequestedAt ",{"type":24,"tag":204,"props":298,"children":299},{"style":227},[300],{"type":29,"value":301},"time",{"type":24,"tag":204,"props":303,"children":304},{"style":238},[305],{"type":29,"value":306},".",{"type":24,"tag":204,"props":308,"children":309},{"style":227},[310],{"type":29,"value":311},"Time",{"type":24,"tag":204,"props":313,"children":314},{"style":211},[315],{"type":29,"value":316},"     \u002F\u002F UTC\n",{"type":24,"tag":204,"props":318,"children":319},{"class":206,"line":19},[320],{"type":24,"tag":204,"props":321,"children":322},{"style":238},[323],{"type":29,"value":324},"}\n",{"type":24,"tag":25,"props":326,"children":327},{},[328,330,336,338,344,346,351,353,358],{"type":29,"value":329},"Dzięki temu wymiana urządzenia to zmiana w jednym adapterze, a nie w logice ochrony.\nZależności biegną tylko do wewnątrz: ",{"type":24,"tag":165,"props":331,"children":333},{"className":332},[],[334],{"type":29,"value":335},"infrastructure → application → domain",{"type":29,"value":337},". ",{"type":24,"tag":165,"props":339,"children":341},{"className":340},[],[342],{"type":29,"value":343},"domain\u002F",{"type":29,"value":345},"\nnie zna nikogo. To nie jest konwencja „na słowo\": cyklu importów Go w ogóle nie skompiluje,\na jednokierunkowość warstw i regułę ",{"type":24,"tag":165,"props":347,"children":349},{"className":348},[],[350],{"type":29,"value":139},{"type":29,"value":352},"↛",{"type":24,"tag":165,"props":354,"children":356},{"className":355},[],[357],{"type":29,"value":109},{"type":29,"value":359}," domyka lint importów w CI\n(depguard\u002Farch-test) — złamanie to czerwony build, nie uwaga w review. Bo złamanie tej\ngranicy to pierwszy krok do sytuacji, w której refaktor sterownika psuje decyzję o grzaniu.",{"type":24,"tag":72,"props":361,"children":363},{"id":362},"event-bus-z-priorytetami-nie-każde-zdarzenie-jest-równe",[364],{"type":29,"value":365},"Event bus z priorytetami: nie każde zdarzenie jest równe",{"type":24,"tag":25,"props":367,"children":368},{},[369,371,376],{"type":29,"value":370},"Event-driven architecture zwykle kojarzy się z „wszystko async, wszystko luźno spięte\".\nW systemie, w którym spóźniona decyzja zabija roślinę, to za mało — bo ",{"type":24,"tag":32,"props":372,"children":373},{},[374],{"type":29,"value":375},"decyzja o grzaniu\nnie może czekać w kolejce za zapisem do bazy",{"type":29,"value":377},". Nasz bus ma trzy pasma:",{"type":24,"tag":194,"props":379,"children":382},{"className":380,"code":381,"language":29,"meta":7,"style":7},"language-text shiki shiki-themes github-dark","CRITICAL — synchronicznie, w goroutine publishera (µs)\n  pomiar → ocena reguł → komenda sterująca. Błąd propaguje się do publishera.\nNORMAL   — async, bufor 256, publish nieblokujący (ms)\n  zapis pomiaru, sesje grzania, alerty. Pełny bufor → drop zapisu (at-most-once).\nLOW      — async, bufor 64, drop policy\n  push do dashboardu (SSE). Klient nie nadąża? Dropujemy najstarsze.\n",[383],{"type":24,"tag":165,"props":384,"children":385},{"__ignoreMap":7},[386,394,402,410,418,426],{"type":24,"tag":204,"props":387,"children":388},{"class":206,"line":207},[389],{"type":24,"tag":204,"props":390,"children":391},{},[392],{"type":29,"value":393},"CRITICAL — synchronicznie, w goroutine publishera (µs)\n",{"type":24,"tag":204,"props":395,"children":396},{"class":206,"line":217},[397],{"type":24,"tag":204,"props":398,"children":399},{},[400],{"type":29,"value":401},"  pomiar → ocena reguł → komenda sterująca. Błąd propaguje się do publishera.\n",{"type":24,"tag":204,"props":403,"children":404},{"class":206,"line":244},[405],{"type":24,"tag":204,"props":406,"children":407},{},[408],{"type":29,"value":409},"NORMAL   — async, bufor 256, publish nieblokujący (ms)\n",{"type":24,"tag":204,"props":411,"children":412},{"class":206,"line":258},[413],{"type":24,"tag":204,"props":414,"children":415},{},[416],{"type":29,"value":417},"  zapis pomiaru, sesje grzania, alerty. Pełny bufor → drop zapisu (at-most-once).\n",{"type":24,"tag":204,"props":419,"children":420},{"class":206,"line":277},[421],{"type":24,"tag":204,"props":422,"children":423},{},[424],{"type":29,"value":425},"LOW      — async, bufor 64, drop policy\n",{"type":24,"tag":204,"props":427,"children":428},{"class":206,"line":290},[429],{"type":24,"tag":204,"props":430,"children":431},{},[432],{"type":29,"value":433},"  push do dashboardu (SSE). Klient nie nadąża? Dropujemy najstarsze.\n",{"type":24,"tag":25,"props":435,"children":436},{},[437,439,444,446,451,453,458,460,465,467,472,474,479],{"type":29,"value":438},"Pasmo CRITICAL jest uczciwie ",{"type":24,"tag":32,"props":440,"children":441},{},[442],{"type":29,"value":443},"nie-EDA",{"type":29,"value":445},": synchroniczny łańcuch z propagacją błędu, bliżej\nwywołania funkcji niż magistrali zdarzeń — i tak ma być, bo event-driven jest dopiero to,\nco może poczekać. Ścieżka przeżyciowa jest ",{"type":24,"tag":32,"props":447,"children":448},{},[449],{"type":29,"value":450},"synchroniczna i pierwsza",{"type":29,"value":452},"; NORMAL i LOW są async i celowo\n",{"type":24,"tag":32,"props":454,"children":455},{},[456],{"type":29,"value":457},"nieblokujące",{"type":29,"value":459}," — gdy bufor zapisu się zapełni (padnięta baza), dropujemy pomiar, nigdy nie\nwstrzymujemy publishera. To świadomy trade-off: wolimy stracić punkt na wykresie\n(at-most-once) niż tick sterowania. Software'owy interlock przegrzania ustawia blokadę w tym\nsamym „ticku\", zanim silnik cokolwiek zdecyduje (gwarantuje to kolejność subskrybentów CRITICAL\n— interlock rejestrowany przed silnikiem) — ale to ostatnia linia obrony w ",{"type":24,"tag":66,"props":461,"children":462},{},[463],{"type":29,"value":464},"kodzie",{"type":29,"value":466},",\nnie ostatnia w ogóle: żadna architektura nie przeżyje zwisu procesu, więc realnym\nbezpiecznikiem musi być sprzętowy failsafe (watchdog, termik) ",{"type":24,"tag":66,"props":468,"children":469},{},[470],{"type":29,"value":471},"pod",{"type":29,"value":473}," softwarem. To jest EDA\nużyte nie dla mody na async, lecz do ",{"type":24,"tag":32,"props":475,"children":476},{},[477],{"type":29,"value":478},"rozdzielenia awarii",{"type":29,"value":480},": padnięcie bazy nie może\nzatrzymać sterowania.",{"type":24,"tag":72,"props":482,"children":484},{"id":483},"silnik-reguł-jako-czysta-funkcja-solid-który-się-opłaca",[485],{"type":29,"value":486},"Silnik reguł jako czysta funkcja: SOLID, który się opłaca",{"type":24,"tag":25,"props":488,"children":489},{},[490,492,497],{"type":29,"value":491},"Najważniejszy element — silnik decydujący o grzaniu — jest ",{"type":24,"tag":32,"props":493,"children":494},{},[495],{"type":29,"value":496},"czystą funkcją",{"type":29,"value":192},{"type":24,"tag":194,"props":499,"children":501},{"className":196,"code":500,"language":198,"meta":7,"style":7},"\u002F\u002F Wejście → wyjście. Zero I\u002FO, zero time.Now() w środku.\nfunc Evaluate(\n    readings  []StationReading,\n    rules     []ControlRule,\n    states    map[ControlRole]ControlState,\n    forecast  map[string]float64,\n    at        time.Time,           \u002F\u002F czas WSTRZYKIWANY, nigdy pobierany w środku\n) []ControlCommand\n",[502],{"type":24,"tag":165,"props":503,"children":504},{"__ignoreMap":7},[505,513,531,555,577,614,649,680],{"type":24,"tag":204,"props":506,"children":507},{"class":206,"line":207},[508],{"type":24,"tag":204,"props":509,"children":510},{"style":211},[511],{"type":29,"value":512},"\u002F\u002F Wejście → wyjście. Zero I\u002FO, zero time.Now() w środku.\n",{"type":24,"tag":204,"props":514,"children":515},{"class":206,"line":217},[516,521,526],{"type":24,"tag":204,"props":517,"children":518},{"style":221},[519],{"type":29,"value":520},"func",{"type":24,"tag":204,"props":522,"children":523},{"style":227},[524],{"type":29,"value":525}," Evaluate",{"type":24,"tag":204,"props":527,"children":528},{"style":238},[529],{"type":29,"value":530},"(\n",{"type":24,"tag":204,"props":532,"children":533},{"class":206,"line":244},[534,540,545,550],{"type":24,"tag":204,"props":535,"children":537},{"style":536},"--shiki-default:#FFAB70",[538],{"type":29,"value":539},"    readings",{"type":24,"tag":204,"props":541,"children":542},{"style":238},[543],{"type":29,"value":544},"  []",{"type":24,"tag":204,"props":546,"children":547},{"style":227},[548],{"type":29,"value":549},"StationReading",{"type":24,"tag":204,"props":551,"children":552},{"style":238},[553],{"type":29,"value":554},",\n",{"type":24,"tag":204,"props":556,"children":557},{"class":206,"line":258},[558,563,568,573],{"type":24,"tag":204,"props":559,"children":560},{"style":536},[561],{"type":29,"value":562},"    rules",{"type":24,"tag":204,"props":564,"children":565},{"style":238},[566],{"type":29,"value":567},"     []",{"type":24,"tag":204,"props":569,"children":570},{"style":227},[571],{"type":29,"value":572},"ControlRule",{"type":24,"tag":204,"props":574,"children":575},{"style":238},[576],{"type":29,"value":554},{"type":24,"tag":204,"props":578,"children":579},{"class":206,"line":277},[580,585,590,595,600,605,610],{"type":24,"tag":204,"props":581,"children":582},{"style":536},[583],{"type":29,"value":584},"    states",{"type":24,"tag":204,"props":586,"children":587},{"style":221},[588],{"type":29,"value":589},"    map",{"type":24,"tag":204,"props":591,"children":592},{"style":238},[593],{"type":29,"value":594},"[",{"type":24,"tag":204,"props":596,"children":597},{"style":227},[598],{"type":29,"value":599},"ControlRole",{"type":24,"tag":204,"props":601,"children":602},{"style":238},[603],{"type":29,"value":604},"]",{"type":24,"tag":204,"props":606,"children":607},{"style":227},[608],{"type":29,"value":609},"ControlState",{"type":24,"tag":204,"props":611,"children":612},{"style":238},[613],{"type":29,"value":554},{"type":24,"tag":204,"props":615,"children":616},{"class":206,"line":290},[617,622,627,631,636,640,645],{"type":24,"tag":204,"props":618,"children":619},{"style":536},[620],{"type":29,"value":621},"    forecast",{"type":24,"tag":204,"props":623,"children":624},{"style":221},[625],{"type":29,"value":626},"  map",{"type":24,"tag":204,"props":628,"children":629},{"style":238},[630],{"type":29,"value":594},{"type":24,"tag":204,"props":632,"children":633},{"style":221},[634],{"type":29,"value":635},"string",{"type":24,"tag":204,"props":637,"children":638},{"style":238},[639],{"type":29,"value":604},{"type":24,"tag":204,"props":641,"children":642},{"style":221},[643],{"type":29,"value":644},"float64",{"type":24,"tag":204,"props":646,"children":647},{"style":238},[648],{"type":29,"value":554},{"type":24,"tag":204,"props":650,"children":651},{"class":206,"line":19},[652,657,662,666,670,675],{"type":24,"tag":204,"props":653,"children":654},{"style":536},[655],{"type":29,"value":656},"    at",{"type":24,"tag":204,"props":658,"children":659},{"style":227},[660],{"type":29,"value":661},"        time",{"type":24,"tag":204,"props":663,"children":664},{"style":238},[665],{"type":29,"value":306},{"type":24,"tag":204,"props":667,"children":668},{"style":227},[669],{"type":29,"value":311},{"type":24,"tag":204,"props":671,"children":672},{"style":238},[673],{"type":29,"value":674},",           ",{"type":24,"tag":204,"props":676,"children":677},{"style":211},[678],{"type":29,"value":679},"\u002F\u002F czas WSTRZYKIWANY, nigdy pobierany w środku\n",{"type":24,"tag":204,"props":681,"children":683},{"class":206,"line":682},8,[684,689],{"type":24,"tag":204,"props":685,"children":686},{"style":238},[687],{"type":29,"value":688},") []",{"type":24,"tag":204,"props":690,"children":691},{"style":227},[692],{"type":29,"value":693},"ControlCommand\n",{"type":24,"tag":25,"props":695,"children":696},{},[697],{"type":29,"value":698},"To pojedyncza decyzja projektowa, która realizuje kilka zasad SOLID naraz:",{"type":24,"tag":98,"props":700,"children":701},{},[702,719,736],{"type":24,"tag":102,"props":703,"children":704},{},[705,710,712,717],{"type":24,"tag":32,"props":706,"children":707},{},[708],{"type":29,"value":709},"Single Responsibility",{"type":29,"value":711}," — silnik ",{"type":24,"tag":66,"props":713,"children":714},{},[715],{"type":29,"value":716},"decyduje",{"type":29,"value":718},", nie pobiera danych i nie wysyła komend.",{"type":24,"tag":102,"props":720,"children":721},{},[722,727,729,734],{"type":24,"tag":32,"props":723,"children":724},{},[725],{"type":29,"value":726},"Dependency Inversion — a właściwie jej brak",{"type":29,"value":728}," — silnik nie sięga po zegar, bazę ani\nsieć: czas, odczyty i stan dostaje jako argumenty. To nawet nie inwersja zależności od\ninfrastruktury — on jej ",{"type":24,"tag":66,"props":730,"children":731},{},[732],{"type":29,"value":733},"nie ma",{"type":29,"value":735},", więc nie istnieje konkret, który trzeba by mockować.\nProdukcja podaje realny czas; test — czas ustawiony ręcznie.",{"type":24,"tag":102,"props":737,"children":738},{},[739,744],{"type":24,"tag":32,"props":740,"children":741},{},[742],{"type":29,"value":743},"Open\u002FClosed",{"type":29,"value":745}," — nowy scenariusz ochrony to nowa reguła (dana), nie zmiana silnika.",{"type":24,"tag":25,"props":747,"children":748},{},[749,751,756,758,763,765,771,773,778,780,785,787,792,794,799,801,807],{"type":29,"value":750},"Trzy powyższe mieszkają w silniku. Czwarta — ",{"type":24,"tag":32,"props":752,"children":753},{},[754],{"type":29,"value":755},"Liskov",{"type":29,"value":757}," — pilnuje drugiego filaru:\n",{"type":24,"tag":32,"props":759,"children":760},{},[761],{"type":29,"value":762},"kontraktu sterownika",{"type":29,"value":764},". LSP w ścisłym sensie mówi, że każdy podtyp musi honorować kontrakt\nnadtypu; jego praktyczną konsekwencją jest wymienność rodzeństwa — każde urządzenie (Tasmota,\nBleBox, Shelly… i ",{"type":24,"tag":165,"props":766,"children":768},{"className":767},[],[769],{"type":29,"value":770},"FakeDriver",{"type":29,"value":772},") honoruje ten sam ",{"type":24,"tag":32,"props":774,"children":775},{},[776],{"type":29,"value":777},"kontrakt telemetrii",{"type":29,"value":779},", emituje ten sam\nkształt pomiaru i jako źródło danych podstawia się za każde inne. Core jest\nnapisany przeciw tej abstrakcji, ",{"type":24,"tag":32,"props":781,"children":782},{},[783],{"type":29,"value":784},"ślepy na to, jakie urządzenie stoi po drugiej stronie",{"type":29,"value":786},".\nTo nie teoria:\n",{"type":24,"tag":165,"props":788,"children":790},{"className":789},[],[791],{"type":29,"value":770},{"type":29,"value":793}," jest w pełni substytuowalny za prawdziwy sterownik i ",{"type":24,"tag":32,"props":795,"children":796},{},[797],{"type":29,"value":798},"dlatego cały tor — od\ntelemetrii po wykonanie komendy — testujemy end-to-end bez sprzętu",{"type":29,"value":800},". Sam silnik jest jeszcze\nniżej: widzi wyłącznie ",{"type":24,"tag":165,"props":802,"children":804},{"className":803},[],[805],{"type":29,"value":806},"[]StationReading",{"type":29,"value":808},", więc sterowników w ogóle nie zna. Wydzielenie kontraktu\ndo jednej bramy telemetrii to konkretne miejsce, w którym ta wymienność jest egzekwowana:\ngdyby któryś sterownik „oszukiwał\" kontrakt, cała podstawialność by się sypnęła — więc\nkontrakt jest granicą, nie sugestią.",{"type":24,"tag":25,"props":810,"children":811},{},[812,814,819,821,827,829,835,837,843,845,851,853,858,860,865,867,873,875,881,883,888,890,895],{"type":29,"value":813},"Piąta — ",{"type":24,"tag":32,"props":815,"children":816},{},[817],{"type":29,"value":818},"Interface Segregation",{"type":29,"value":820}," — kosztowała refaktor, i to uczciwsza część tej historii.\n",{"type":24,"tag":165,"props":822,"children":824},{"className":823},[],[825],{"type":29,"value":826},"Driver",{"type":29,"value":828}," był początkowo za szeroki: łączył telemetrię (",{"type":24,"tag":165,"props":830,"children":832},{"className":831},[],[833],{"type":29,"value":834},"Start",{"type":29,"value":836},") ze sterowaniem (",{"type":24,"tag":165,"props":838,"children":840},{"className":839},[],[841],{"type":29,"value":842},"Send",{"type":29,"value":844},"\u002F",{"type":24,"tag":165,"props":846,"children":848},{"className":847},[],[849],{"type":29,"value":850},"Handles",{"type":29,"value":852},").\nCzysty czujnik (BleBox, ESPHome, stacja pogodowa) był więc ",{"type":24,"tag":32,"props":854,"children":855},{},[856],{"type":29,"value":857},"zmuszany implementować metody\nsterujące, których nie używa",{"type":29,"value":859}," — choćby jako puste zaślepki. Rozbiliśmy ",{"type":24,"tag":165,"props":861,"children":863},{"className":862},[],[864],{"type":29,"value":826},{"type":29,"value":866}," na ",{"type":24,"tag":165,"props":868,"children":870},{"className":869},[],[871],{"type":29,"value":872},"TelemetrySource",{"type":29,"value":874},"\n(każdy sterownik) i ",{"type":24,"tag":165,"props":876,"children":878},{"className":877},[],[879],{"type":29,"value":880},"Controllable",{"type":29,"value":882}," (tylko przekaźniki\u002FHVAC): czujnik implementuje teraz sam\n",{"type":24,"tag":165,"props":884,"children":886},{"className":885},[],[887],{"type":29,"value":872},{"type":29,"value":889},", a wysyłanie komend zależy od wąskiego ",{"type":24,"tag":165,"props":891,"children":893},{"className":892},[],[894],{"type":29,"value":880},{"type":29,"value":896},", nie od tłustego interfejsu.\nKonsument zależy teraz dokładnie od tego, czego używa — wąska zależność zamiast tłustej.",{"type":24,"tag":25,"props":898,"children":899},{},[900,902,907,909,914],{"type":29,"value":901},"Zysk jest namacalny: całą noc mrozu — spadek temperatury, awarię czujnika, przegrzanie —\nodtwarzamy ",{"type":24,"tag":32,"props":903,"children":904},{},[905],{"type":29,"value":906},"deterministycznie",{"type":29,"value":908},": pojedynczy tick w mikrosekundy, całą noc w ułamku sekundy,\nbo wystarczy podać inne wejście i inny czas. Test „czy przy −8 °C grzałka rusza, a przy przekroczeniu progu gaśnie\" nie potrzebuje\nani urządzenia, ani czekania, ani prawdziwego zegara. A skoro potrafimy to przetestować\ntak tanio — testujemy ",{"type":24,"tag":66,"props":910,"children":911},{},[912],{"type":29,"value":913},"każdą",{"type":29,"value":915}," ścieżkę survival, zanim dotknie żywej rośliny.",{"type":24,"tag":25,"props":917,"children":918},{},[919,921,926,928,933,935,940,942,947,949,954,956,961],{"type":29,"value":920},"Dlaczego pilnować ",{"type":24,"tag":32,"props":922,"children":923},{},[924],{"type":29,"value":925},"wszystkich pięciu",{"type":29,"value":927},", a nie „większości\"? Bo w systemie, w którym błąd = martwa\nroślina, każda zasada kupuje konkretną własność, nie punkt w recenzji: SRP i wstrzykiwanie wejść → ",{"type":24,"tag":32,"props":929,"children":930},{},[931],{"type":29,"value":932},"testowalność",{"type":29,"value":934},"\n(noc mrozu odtwarzana offline), Liskov → ",{"type":24,"tag":32,"props":936,"children":937},{},[938],{"type":29,"value":939},"substytuowalność",{"type":29,"value":941}," (fake zastępuje realny, więc harness\nw ogóle istnieje), Open\u002FClosed → ",{"type":24,"tag":32,"props":943,"children":944},{},[945],{"type":29,"value":946},"rozszerzalność bez dotykania rdzenia",{"type":29,"value":948},", ISP → ",{"type":24,"tag":32,"props":950,"children":951},{},[952],{"type":29,"value":953},"wąskie zależności",{"type":29,"value":955},"\n(zmiana sterownika nie przecieka do decyzji o grzaniu). Nie twierdzę, że złamanie którejś zasady\n= deterministyczna awaria — to byłby ten sam cargo-cult, przed którym przestrzegam. Twierdzę, że\nkażda kupiła tu ",{"type":24,"tag":32,"props":957,"children":958},{},[959],{"type":29,"value":960},"konkretną, sprawdzalną własność",{"type":29,"value":962},", więc „cztery z pięciu prawie spełnione\" nie\nznaczy „80% bezpieczeństwa\" — znaczy „jedną z tych własności właśnie oddaliśmy, świadomie albo nie\".\nDlatego zasadę spełnioną tylko „prawie\" domyka się refaktorem, zanim dołoży się następną\nfunkcję — nie potem.",{"type":24,"tag":72,"props":964,"children":966},{"id":965},"wniosek-architektura-jako-kontrakt-bezpieczeństwa",[967],{"type":29,"value":968},"Wniosek: architektura jako kontrakt bezpieczeństwa",{"type":24,"tag":25,"props":970,"children":971},{},[972,974,979],{"type":29,"value":973},"Te trzy decyzje nie są niezależne. Bounded contexts gwarantują, że logika ochrony nie\nzależy od sprzętu. Event bus gwarantuje, że decyzja wyprzedza persystencję. Czysta funkcja\ngwarantuje, że decyzję da się w całości przetestować offline. Razem dają własność, o którą\nnaprawdę chodzi: ",{"type":24,"tag":32,"props":975,"children":976},{},[977],{"type":29,"value":978},"system, którego nie da się łatwo zmusić do złego zachowania",{"type":29,"value":980}," — złą decyzję\ntrzeba w tym kodzie wyrazić świadomie, przypadkiem się jej nie napisze.",{"type":24,"tag":25,"props":982,"children":983},{},[984,986,991],{"type":29,"value":985},"Tak budujemy w E4T: architektura ma najpierw ",{"type":24,"tag":66,"props":987,"children":988},{},[989],{"type":29,"value":990},"uniemożliwiać",{"type":29,"value":992}," błędy, a dopiero potem\nwyglądać ładnie. W systemie, w którym stawką jest żywa roślina, to nie luksus — to wymóg.",{"type":24,"tag":994,"props":995,"children":996},"style",{},[997],{"type":29,"value":998},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":217,"depth":217,"links":1000},[1001,1002,1003,1004],{"id":74,"depth":217,"text":77},{"id":362,"depth":217,"text":365},{"id":483,"depth":217,"text":486},{"id":965,"depth":217,"text":968},"markdown","content:blog:ddd-eda-w-systemie-iot.md","content","blog\u002Fddd-eda-w-systemie-iot.md","blog\u002Fddd-eda-w-systemie-iot","md",1783499642977]