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

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?

  1. 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

  1. Poriešime Single Responsibility Principle
  2. 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

  1. Poriešime Single Responsibility Principle
  2. Poriešime Open-Closed Principle
  3. 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)

  • 231