D v SOLIDe
Milan Herda, 07 / 2023
SOLID princípy v JavaScripte
D v SOLIDe
Milan Herda, 07 / 2023
SOLID princípy v JavaScripte
D v SOLIDe
Milan Herda, 07 / 2023
SOLID princípy v TypeScripte
a v Reacte
O čom budeme hovoriť
- Čo je SOLID
- Dependency Inversion Principle
- Výhody DIP
- Ako rozpoznať porušenia princípu
- Ako refaktorovať, aby sme dodržali princíp
- Príklad na implementáciu všetkých princípov
- Čo sme sa naučili
SOLID
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Skratka predstavujúca 5 základných princípov dobrého softvérového návrhu
Dependency Inversion Principle
Princíp obrátených závislostí
Ak váš kód potrebuje k svojej činnosti iný objekt (funkciu/triedu), tak tento objekt je závislosťou pre váš kód.
Čo je závislosť?
Najviac viditeľné sú parametre funkcií, property tried a importy modulov
Dependency Inversion Principle
- "objekt" na vyššej úrovni by nemal mať závislosť na nižšej úrovni
- "objekt" by mal závisieť iba na abstraktných "objektoch"
- "objekt" by nemal závisieť na konkrétnostiach
Prevedené do praxe v TypeScripte
- Interface nemôže závisieť na konkrétnej triede
- Triedy a interface-y by mali závisieť na interface-och
- Nič by nemalo mať konkrétnu triedu ako závislosť
- Doménová logika by nemala závisieť na infraštruktúre
Prečo?
Je jednoduchšie vymeniť inštanciu niečoho abstraktného, ako konkrétnu vec
Výhody dodržiavania
Dependency Inversion Principle
- časti kódu sú navzájom ľahko vymeniteľné
Symptómy porušenia princípu
- Chýbajúce interface-y
- Interface nezávisí na interface, ale na konkrétnej triede
- Konkrétna trieda závisí na konkrétnej triede
- Doménová logika pracuje s infraštruktúrou
Zdrojové súbory:
Príklad 1
class MojKamarat
{
// ...
}
interface StatnaZakazka
{
priradDodavatela: (dodavatel: MojKamarat) => void;
// ...
}
interface SplnajuciPodmienky
{
// ...
}
interface StatnaZakazka
{
priradDodavatela: (dodavatel: SplnajuciPodmienky) => void;
// ...
}
Nie je to takto lepšie?
Príklad 2
function CharactersPage() {
const [characters, setCharacters] = useState<Character[]>([]);
const { restConnection } = useContext(RestConnectionContext);
useEffect(() => {
async function fetchData() {
const loadedCharacters = await restConnection.loadJsonData<Character[]>(
'/api/list.json'
);
setCharacters(loadedCharacters);
}
fetchData();
}, []);
return (
<>
<h2>List of Characters</h2>
<ul>
{characters.map((character: Character) => {
return <li key={character.id}>{character.name}</li>;
})}
</ul>
</>
);
}
Komponent zobrazuje dáta a priamo získava dáta. To je priveľa zodpovedností.
/src/ui/pages/CharactersPage.tsx
function CharactersPage() {
const [characters, setCharacters] = useState<Character[]>([]);
const { restConnection } = useContext(RestConnectionContext);
useEffect(() => {
async function fetchData() {
const loadedCharacters = await restConnection.loadJsonData<Character[]>(
'/api/list.json'
);
setCharacters(loadedCharacters);
}
fetchData();
}, []);
return (
<>
<h2>List of Characters</h2>
<ul>
{characters.map((character: Character) => {
return <li key={character.id}>{character.name}</li>;
})}
</ul>
</>
);
}
URL je tu napevno, to je signál porušenia Open-Closed princípu
/src/ui/pages/CharactersPage.tsx
function CharactersPage() {
const [characters, setCharacters] = useState<Character[]>([]);
const { restConnection } = useContext(RestConnectionContext);
useEffect(() => {
async function fetchData() {
const loadedCharacters = await restConnection.loadJsonData<Character[]>(
'/api/list.json'
);
setCharacters(loadedCharacters);
}
fetchData();
}, []);
return (
<>
<h2>List of Characters</h2>
<ul>
{characters.map((character: Character) => {
return <li key={character.id}>{character.name}</li>;
})}
</ul>
</>
);
}
Potrebuje komponent vedieť, že dáta sú dostupné cez REST?
Nezávisí tak príliš na konkrétnosti?
/src/ui/pages/CharactersPage.tsx
RestConnection je interface, je to stále problém?
- Poriešime Single Responsibility Principle
function CharactersPage() {
const characters = useCharacters();
return (
<>
<h2>List of Characters</h2>
<ul>
{characters.map((character: Character) => {
return <li key={character.id}>{character.name}</li>;
})}
</ul>
</>
);
}
/src/ui/pages/CharactersPage.tsx
function useCharacters() {
const [characters, setCharacters] = useState<Character[]>([]);
const { restConnection } = useContext(RestConnectionContext);
useEffect(() => {
async function fetchData() {
const loadedCharacters =
await restConnection.loadJsonData<Character[]>(
'/api/list.json'
);
setCharacters(loadedCharacters);
}
fetchData();
}, []);
return characters;
}
/src/ui/hooks/useCharacters.ts
- Poriešime Single Responsibility Principle
- Poriešime Open-Closed Principle
function useCharacters() {
return useRestData<Character[]>('/api/list.json', []);
}
/src/ui/hooks/useCharacters.ts
function useRestData<T>(sourceUrl: string, initialData: T) {
const [data, setData] = useState<T>(initialData);
const { restConnection } = useContext(RestConnectionContext);
useEffect(() => {
async function fetchData() {
const loadedData =
await restConnection.loadJsonData<T>(sourceUrl);
setData(loadedData);
}
fetchData();
}, []);
return data;
}
/src/ui/hooks/useRestData.ts
- Poriešime Single Responsibility Principle
- Poriešime Open-Closed Principle
- Poriešime Dependency Inversion Principle
function CharactersPage() {
const characters = useCharacters();
return (
<>
<h2>List of Characters</h2>
<ul>
{characters.map((character: Character) => {
return <li key={character.id}>{character.name}</li>;
})}
</ul>
</>
);
}
/src/ui/pages/CharactersPage.tsx
Komponent už nemá závislosť na RestConnection, nemáme čo riešiť
Hotovo
Komponent už nemá závislosť na RestConnection...
To je síce pravda, ale problém je len presunutý.
Hooky useCharacters a useRestData oba vedia, že pracujú s RestConnection alebo REST API.
Nepotrebujú to vedieť a vieme to urobiť lepšie.
Musíme sa ale v zmenách vrátiť...
function useCharacters() {
const [characters, setCharacters] = useState<Character[]>([]);
const { restConnection } = useContext(RestConnectionContext);
useEffect(() => {
async function fetchData() {
const loadedCharacters =
await restConnection.loadJsonData<Character[]>(
'/api/list.json'
);
setCharacters(loadedCharacters);
}
fetchData();
}, []);
return characters;
}
/src/ui/hooks/useCharacters.ts
Čo keby sme mali službu, ktorá nám poskytuje prístup k postavám a tu ju len poprosíme o dáta?
Riešenie
function useCharacters() {
const [characters, setCharacters] = useState<Character[]>([]);
const { characterRepository } = useContext(CharacterRepositoryContext);
useEffect(() => {
async function fetchData() {
const loadedData = await characterRepository.loadCharacters();
setCharacters(loadedData);
}
fetchData();
}, []);
return characters;
}
/src/ui/hooks/useCharacters.ts
- Repozitár implementuje interface CharacterRepository, takže náš hook závisí na abstraktnosti
- useCharacters hook už nemá v sebe URL a vôbec netuší, že dáta idú cez REST API
- prepojenie abstraktného interfacu pre repozitár s jeho konkrétnou REST implementáciou sa deje v /src/application/bootstrap/getInitialData.ts
- ak budeme chcieť vymeniť REST implementáciu napr. za GraphQL, tak toto je jediné miesto, kde treba urobiť zmenu
Hotovo
Dependency Inversion Principle
Princíp obrátených závislostí
Dependency Inversion Principle
- "objekt" na vyššej úrovni by nemal mať závislosť na nižšej úrovni
- "objekt" by mal závisieť iba na abstraktných "objektoch"
- "objekt" by nemal závisieť na konkrétnostiach
Prevedené do praxe v TypeScripte
- Interface nemôže závisieť na konkrétnej triede
- Triedy a interface-y by mali závisieť na interface-och
- Nič by nemalo mať konkrétnu triedu ako závislosť
- Doménová logika by nemala závisieť na infraštruktúre
Prečo?
Je jednoduchšie vymeniť inštanciu niečoho abstraktného, ako konkrétnu vec
Voľné programovanie
Naprogramujte tzv. FizzBuzz generátor, ktorý:
- generuje zoznam celých čísel od 1 po n
- čísla deliteľné 3 nahradí reťazcom "Fizz"
- čísla deliteľné 5 nahradí reťazcom "Buzz"
- čísla deliteľné 3 aj 5 nahradí reťazcom "FizzBuzz"
- na každé číslo sa vždy aplikuje maximálne jedno pravidlo
Upravte váš FizzBuzz generátor:
- chceme si zvoliť začiatočné číslo (doteraz bola 1)
- pridajte pravidlo nahradzujúce čísla deliteľné 7 reťazcom Bar
- pridajte pravidlo nahradzujúce číslo 11 reťazcom "jedenásť"
- pridajte pravidlo vymieňajúce číslicu 4 za písmeno A
- vymeňte poradie pravidiel
- pridajte nové pravidlo bez potreby zmeny kódu generátoru
export interface Rule {
doesMatch: (num: number) => boolean;
getReplacement: (num: number) => number | string;
}
const fizzRule: Rule = {
doesMatch: (num) => num % 3 === 0,
getReplacement: () => 'Fizz',
};
export function createGenerator() {
const rules: Rule[] = [];
function getItem(num: number) {
for (const rule of rules) {
if (rule.doesMatch(num)) {
return rule.getReplacement(num);
}
}
return num;
}
return {
registerRule(rule: Rule) {
rules.push(rule);
return this;
},
generate(min: number, max: number) {
const items: (number | string)[] = [];
for (let i = min; i <= max; i += 1) {
items.push(getItem(i));
}
return items;
},
};
const generator = createGenerator();
generator.registerRule(fizzBuzzRule);
generator.registerRule(fizzRule);
generator.registerRule(buzzRule);
// ...
const list = generator.generate(1, 50);
Riešenie:
Opakovanie
SOLID - Záver
Aby bol kód ľahko udržiavateľný, testovateľný a rozširovateľný s minimálnym množstvom programovania, tak každá jednotka kódu by mala:
- riešiť iba jednu vec
- byť otvorená pre zmenu správania bez potreby zmeny kódu
- byť dobrým potomkom svojich rodičov
- implementovať a závisieť na malých a pre klientov špecifických interface-och
- závisieť na abstrakciách a nie konkrétnostiach
Ďakujem za pozornosť
D v SOLIDe (JavaScript)
By Milan Herda
D v SOLIDe (JavaScript)
- 246