1. Embedding rozszerza publiczne API
Na poczatku embedding wyglada jak male zwyciestwo nad liczba znakow: mniej .Inner., mniej „otoczki”, mniej kodu. Ale za te oszczednosc placi sie cena: typ zewnetrzny nagle zaczyna wygladac tak, jakby umial znacznie wiecej, niz naprawde powinien. Kod staje sie trudniejszy do czytania i latwiejszy do blednej interpretacji, a wszystko, co zle sie czyta, zwykle staje sie przytulnym domem dla bugow.
Kluczowa mysl wykladu: embedding to nie tylko „wygodniej sie odwolac”. Embedding zmienia powierzchnie typu: to, co jest dostepne dla uzytkownika przez .. A powierzchnia typu to wasz mini‑kontrakt: „co ten typ moze robic”.
Utrwalmy to na malym fragmencie naszej aplikacji edukacyjnej „task tracker” (lista zadan). Zaczniemy od prostej modelu:
package main
type Task struct {
ID int
Title string
Done bool
}
Na razie wszystko jest nudne i bezpieczne. Niebezpieczenstwo zaczyna sie tam, gdzie decydujemy: „wbudujmy cos przydatnego”.
2. Glowne problemy embedding
Wyciek wewnetrznej zaleznosci
Bardzo czesty scenariusz: piszecie strukture „serwis/aplikacja” i wewnatrz potrzebujecie pomocnika — generatora ID, loggera, walidatora, licznika. W pewnym momencie chce sie nie pisac s.idGen.Next() i robicie embedding. I wtedy zaczyna sie magia, ktora w produkcji zwykle nazywa sie: „dlaczego to w ogole mozna wywolac?!”.
Stworzmy maly generator ID:
package main
type IDGen struct {
next int
}
func (g *IDGen) Next() int {
g.next++
return g.next
}
Teraz zrobmy „projekt” (kontener zadan) i wbudujmy IDGen, zeby nie pisac niepotrzebnie:
package main
type Project struct {
Name string
IDGen // embedded
}
Uzycie wyglada przyjemnie:
package main
import "fmt"
func main() {
p := Project{Name: "Home"}
fmt.Println(p.Next()) // 1
fmt.Println(p.Next()) // 2
}
I oto pytanie, ktore powinno lekko zaniepokoic: dlaczego uzytkownik Project w ogole powinien miec dostep do Next()? Przeciez to wewnetrzny mechanizm wydawania ID, a nie „funkcja projektu”.
W embedding tak jakby mowicie: „Projekt umie Next”. I czytelnik kodu zaczyna myslec, ze to normalna operacja domenowa. A to juz problem projektu, nie skladni.
Rozdmuchiwanie powierzchni typu
Rozdmuchiwanie powierzchni typu to sytuacja, w ktorej typ zewnetrzny dostaje dziesiatki metod/pol, ktore nie odpowiadaja jego odpowiedzialnosci. Jest to szczegolnie nieprzyjemne, gdy wbudowujecie nie maly „value object”, ale cos „ciezkiego” z duzym zestawem metod.
Pokażmy to na naszym typie listy zadan. Zrobmy typ‑opakowanie nad slice’em (potrafimy juz tworzyc metody na „slice‑typach”):
package main
type TaskList []Task
func (tl *TaskList) Add(t Task) {
*tl = append(*tl, t)
}
func (tl TaskList) DoneCount() int {
count := 0
for _, t := range tl {
if t.Done {
count++
}
}
return count
}
Teraz „projekt” przechowuje zadania. I mozemy zrobic embedding:
package main
type Project struct {
Name string
TaskList // embedded
}
Wyglada milo: p.Add(...), p.DoneCount(). Ale embedding ma efekt uboczny: na zewnatrz „wychodzi” nie tylko zachowanie, ktore chcieliscie, ale tez sam fakt, ze przechowujecie zadania dokladnie w ten sposob (przez TaskList), a dodatkowo pojawia sie bezposredni dostep do pola p.TaskList.
Na zewnatrz teraz mozna zrobic tak:
package main
func main() {
p := Project{Name: "Home"}
// Kod zewnetrzny moze bezposrednio podmienic cala liste.
p.TaskList = TaskList{}
}
Czasami to jest w porzadku. Ale czesto lamie to wasze przyszle plany: na przyklad zechcecie przechowywac zadania nie w slice, lecz w strukturze z indeksem po ID, albo utrzymywac invarianty („ID sa unikalne”, „pusty Title jest zabroniony”). A kod zewnetrzny juz przyzwyczail sie, ze mozna recznie grzebac w TaskList.
I to jest klasyczny przyklad ukrytej ceny embedding: przypadkiem usztywniliscie wewnetrzna strukture typu w jego publicznym zachowaniu.
Nieoczywiste pochodzenie pol i metod
Kiedy kod czytaja poczatkujacy (i nie tylko poczatkujacy), dzieje sie prosta rzecz: czlowiek widzi p.Next() i probuje zrozumiec, gdzie to zdefiniowano. Jesli metoda jest „zewnetrzna” — wszystko proste: szukamy func (Project) Next. Jesli jest „promoted” (promoted) — trzeba pamietac o embedding, znalezc wbudowany typ, a potem metode tam.
To nie koniec swiata, ale obciazenie poznawcze rosnie. A im wieksze obciazenie, tym latwiej o blad. I nie jest to teoretyczna estetyka: zle czytelny kod naprawde latwiej zle utrzymywac.
Z punktu widzenia utrzymania, metody promoted czesto zamieniaja sie w „niewidzialna magie”: kod sie kompiluje, ale nie tlumaczy sam siebie.
Dlatego prosta zasada dla poczatkujacych (i dla zmeczonych doroslych developerow tez): jesli pochodzenie metody jest wazne dla zrozumienia — lepiej zrobic dostep jawny.
Porownajcie:
p.TaskList.DoneCount()
z:
p.DoneCount()
Drugie jest krotsze, ale pierwsze od razu odpowiada na pytanie „to dotyczy zadan”.
Embedding ukrywa wazne parametry i miesza odpowiedzialnosci
Czasami embedding jest uzywany jako „kontener na wszystko”: „wloze do struktury i niech tam zyje”. Ale jesli wewnatrz ukrywa sie rzecz, ktora powinna byc kontrolowana przez kod wywolujacy, dostajecie API, w ktorym nie da sie normalnie zarzadzac zachowaniem.
W spolecznosci Go znana jest mysl: nie ukrywajcie tego, co sensownie powinno byc parametrem wywolania, wewnatrz struktury. To robi zachowanie niejasnym i miesza rozne czasy zycia (lifetime) oraz obszary odpowiedzialnosci.
Nie bedziemy zaglebiać sie w ten konkretny mechanizm (pojawi sie pozniej w kursie), ale lekcja projektowa jest nam juz teraz przydatna: jesli zaleznosc wplywa na to, „jak dziala metoda”, to bardzo czesto lepiej przekazac ja jawnie (jako parametr albo przez kontrolowane pole), a nie „podnosic” na zewnatrz przez embedding tak, ze nie wiadomo, skad pochodzi i kto za nia odpowiada.
W terminach prostego task trackera brzmi to tak: jesli „proces tworzenia zadan” zalezy od generatora ID i walidacji, sa to wasze wewnetrzne detale. Uzytkownik powinien widziec metode AddTask(title) — a nie miec mozliwosc bezposredniego krecenia Next() u generatora.
3. Praktyczny refaktoring bez embedding
Teraz zrobimy „poprawna” wersje Project. Usuniemy embedding, ukryjemy zaleznosci i otworzymy na zewnatrz tylko to, co nalezy do odpowiedzialnosci projektu.
Zacznijmy od nowej struktury:
package main
type Project struct {
Name string
tasks TaskList // not embedded and not exported
idGen IDGen // also internal
}
Od razu dwie rzeczy sa lepsze.
Po pierwsze, kod zewnetrzny nie widzi juz, ze wewnatrz Project zadania sa przechowywane dokladnie jako TaskList. Po drugie, kod zewnetrzny nie moze „pobawic sie” generatorem ID.
Teraz dodajmy „normalna” metode domenowa: dodawanie zadania po tytule.
package main
func (p *Project) AddTask(title string) Task {
t := Task{
ID: p.idGen.Next(),
Title: title,
}
p.tasks.Add(t)
return t
}
Uzycie wyglada tak:
package main
import "fmt"
func main() {
var p Project
p.Name = "Home"
t := p.AddTask("Buy milk")
fmt.Println(t.ID, t.Title) // 1 Buy milk
}
Zwróćcie uwage na efekt: uzytkownik teraz nie wie i nie powinien wiedziec, jak dokladnie wydajecie ID. Wie tylko: „dodaj zadanie”. I to jest dobry API.
4. Jak bezpiecznie zwracac zadania na zewnatrz
Gdy zamykacie wnetrze, natychmiast pojawia sie pytanie: „Okej, ale jak pokazac zadania?” I tutaj bardzo latwo znowu nadepnac na grabie, tylko w innych butach.
Jesli zwrocicie wewnetrzny slice tak po prostu, uzytkownik bedzie mogl zmieniac jego elementy (a to znowu jest wyciek wlasnosci). Wiemy juz o aliasingu slice’ow i o tym, dlaczego „oddanie swojego bufora na zewnatrz” bywa niebezpieczne. Dlatego zrobimy staranna metode, ktora zwraca kopie.
package main
func (p Project) Tasks() []Task {
out := make([]Task, len(p.tasks))
copy(out, p.tasks)
return out
}
Uzycie:
package main
import "fmt"
func main() {
p := Project{Name: "Home"}
p.AddTask("Buy milk")
p.AddTask("Clean room")
for _, t := range p.Tasks() {
fmt.Println(t.ID, t.Title)
// 1 Buy milk
// 2 Clean room
}
}
Teraz kod zewnetrzny dostal dane, ale nie dostal prawa do bezposredniej zmiany wewnetrznego stanu projektu. To jest wlasnie „kontrolowana powierzchnia typu”: oddajecie na zewnatrz to, co potrzebne, i utrzymujecie invarianty u siebie.
5. Sciagawka: kiedy embedding szkodzi
Czasem przydaje sie prosta sciagawka, ale bez zamieniania wykladu w liste na trzy strony. Zrobmy wiec zwięzla tabele, ktora warto miec w glowie, kiedy reka siega po kolejny typ do wbudowania.
| Sytuacja | Co zwykle sie dzieje | Dlaczego to zle | Co zrobic zamiast |
|---|---|---|---|
| Wbudowujemy typ „serwisowy” (generator, logger, walidator) | Metody zaleznosci staja sie „jakby metodami domeny” | Ukrywa sie sens, miesza sie odpowiedzialnosci | Nazwane pole + metody‑opakowania |
| Wbudowujemy strukture z duzym API | Typ zewnetrzny „umie wszystko”, co umie zaleznosc | Rozdmuchana powierzchnia typu, trudniej utrzymac | Wyodrebnic waskie metody, ktore naprawde sa potrzebne |
| Wbudowujemy typ, ktory chcemy zmienic w przyszlosci | Uzytkownicy zaczynaja opierac sie na detalach implementacji | Trudno zmieniac wnatrze bez zlamania kompatybilnosci | Ukryc pole, udostepnic stabilne API |
| Embedding czyni pochodzenie metody nieoczywistym | Czytelnik nie rozumie „skad to sie wzielo” | Kod trudniej czytac i latwiej zle uzywac | Jawny dostep x.inner.M() albo jawne metody na typie zewnetrznym |
Ta tabela nie jest „prawem”, ale dobrym filtrem: jesli rozpoznajecie swoj przypadek — warto sie zatrzymac i pomyslec.
6. Mini‑schemat rozszerzenia typu
Czasami latwiej jeden raz zobaczyc niz dziesiec razy przeczytac. Wyobrazmy sobie naszego Project w dwoch wariantach jako powierzchnie metod.
flowchart TD
A["Project (z embedding)"] --> B["Metody Project"]
A --> C["Promoted metody TaskList"]
A --> D["Promoted metody IDGen"]
E["Project (bez embedding)"] --> F["Metody Project (AddTask, Tasks, ...)"]
E --> G["TaskList i IDGen ukryte wewnatrz"]
Wariant z embedding prawie zawsze daje wiecej „punktow wejscia”. A wiecej punktow wejscia — to wiecej sposobow na nieprawidlowe uzycie typu, nawet bez zlej woli.
7. Typowe bledy
Blad nr 1: embedding robi sie „bo tak krocej”.
Gdy wybieracie embedding tylko po to, by skrocic x.inner.M() → x.M(), placicie czytelnoscia i sterowalnoscia API. Po miesiacu (albo po pol roku, kiedy projekt zacznie zyc wlasnym zyciem) okaze sie, ze na zewnatrz wystaje kupa metod, za ktore wasz typ w ogole nie powinien odpowiadac.
Blad nr 2: przez embedding na zewnatrz „wycieka” zaleznosc, ktora chcieliscie trzymac wewnetrznie.
Czestym przykladem jest wbudowany generator ID albo „techniczny” pomocnik. W efekcie uzytkownik typu dostaje dostep do metod takich jak Next(), ktore nie maja sensu w modelu domenowym. To zamienia wewnetrzny mechanizm w publiczna obietnice, a takie obietnice pozniej bolesnie sie zmienia.
Blad nr 3: embedding jest uzywany w eksportowanym typie bez sprawdzenia „co stalo sie dostepne przez kropke”.
Jesli typ jest publiczny, jego powierzchnia to w praktyce wasz kontrakt z kodem zewnetrznym. Embedding moze rozdmuchac ten kontrakt przypadkowo. A przypadkowe kontrakty zwykle nie przezywaja pierwszego powaznego przepisania architektury.
Blad nr 4: zwracanie stanu wewnetrznego na zewnatrz pod przykrywka wygody.
Gdy wbudowujecie TaskList i pozwalacie na bezposredni dostep do p.TaskList, ulatwiacie zycie uzytkownikowi tylko do momentu, gdy potrzebne stana sie invarianty, walidacja albo zmiana struktury przechowywania. Jesli trzeba oddac dane na zewnatrz, lepiej zwrocic kopie (przez make + copy), niz dzielic sie wewnetrznym buforem.
Blad nr 5: promoted metody pogarszaja czytelnosc, ale to jest ignorowane.
Kod, w ktorym nie wiadomo, skad wzial sie dany metoda, czyta sie trudniej i utrzymuje z wiekszym ryzykiem bledow — szczegolnie w zespolach i szczegolnie dla poczatkujacych. W takich miejscach lepiej albo pisac jawna kwalifikacje (p.tasks.DoneCount()), albo stworzyc staranna metode‑opakowanie na typie zewnetrznym.
GO TO FULL VERSION