CodeGym /Kursy /Go SELF /Kiedy embedding szkodzi

Kiedy embedding szkodzi

Go SELF
Poziom 23, Lekcja 4
Dostępny

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.

1
Zadanie
Go SELF, poziom 23, lekcja 4
Niedostępne
Generator identyfikatorów
Generator identyfikatorów
1
Zadanie
Go SELF, poziom 23, lekcja 4
Niedostępne
Notatnik pod maską
Notatnik pod maską
1
Zadanie
Go SELF, poziom 23, lekcja 4
Niedostępne
Bezpieczne notatki
Bezpieczne notatki
1
Zadanie
Go SELF, poziom 23, lekcja 4
Niedostępne
Projekt zadań
Projekt zadań
1
Ankieta/quiz
Embedding i kompozycja, poziom 23, lekcja 4
Niedostępny
Embedding i kompozycja
Embedding i kompozycja
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION