I v SOLIDe

Milan Herda, 06 / 2023

SOLID princípy v JavaScripte

O čom budeme hovoriť

  • Čo je SOLID
  • Interface Segregation Principle
  • Výhody ISP
  • Ako rozpoznať porušenia princípu
  • Ako refaktorovať, aby sme dodržali princíp

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

Interface Segregation Principle

Princíp oddelenia rozhraní

Interfejsy by mali byť jemne granulované a špecifické pre klienta

Interface Segregation Principle

Jemne granulované

  • malé množstvo položiek
  • najmenšie možné, ktoré dáva zmysel
  • áno, častokrát iba jedna položka
  • a áno, dokonca aj nula

Špecifické pre klienta

  • klient by nemal závisieť na položkách, ktoré nepoužíva
  • do interface-u sa dajú iba položky, ktoré potrebuje
  • ak viacerí klienti pracujú s triedou/objektom rôzne, tak vytvoríme viacero interface-ov
  • interface definuje skupinu položiek, ktoré "chodia spolu"

Klient je kód používajúci iný kód

Výhody dodržiavania

Interface Segregation Principle

  • časti kódu sú navzájom ľahko vymeniteľné
  • klienti závisia iba na tom, čo potrebujú

Symptómy porušenia princípu

  • interface má priveľa položiek
  • deravé abstrakcie (leaky abstractions)
  • žiaden kód nepoužíva všetky položky interface-u
  • interface obsahuje položky, ktoré "nechodia spolu"
  • triedy bez interface-u
interface UserDocument {
    id: number;
    name: string;
    type: string;
    width?: number;
    height?: number;
}
























interface UserDocument {
    id: number;
    name: string;
    type: string;
    width?: number;
    height?: number;
}

function Files({ files }: { files: UserDocument[] }) {
    return (
        <ul>
            {files.map((file) => {
                return <li key={file.id}>{file.name}</li>;
            })}
        </ul>
    );
}














interface UserDocument {
    id: number;
    name: string;
    type: string;
    width?: number;
    height?: number;
}

function Files({ files }: { files: UserDocument[] }) {
    return (
        <ul>
            {files.map((file) => {
                return <li key={file.id}>{file.name}</li>;
            })}
        </ul>
    );
}

function Images({ images }: { images: UserDocument[] }) {
    return (
        <ul>
            {images.map((image) => {
                return (
                    <li key={image.id}>
                        {image.name} {image.width}x{image.height}
                    </li>
                );
            })}
        </ul>
    );
}

Časté riešenie je pridanie has/can/is metódy alebo property

interface UserDocument {
    id: number;
    name: string;
    type: string;
    isImage: boolean;
    width?: number;
    height?: number;
}

function Images({ images }: { images: UserDocument[] }) {
    return (
        <ul>
            {images.filter(i => i.isImage).map((image) => {
                return (
                    <li key={image.id}>
                        {image.name} {image.width}x{image.height}
                    </li>
                );
            })}
        </ul>
    );
}

Časté riešenie je pridanie has/can/is metódy alebo property

  • každý potomok UserDocument musí implementovať isImage
  • isImage a width/height sa musia volať spolu = veľa duplikácií v kóde (+ temporal coupling)

Takéto riešenie nie je dobré, lebo:

Refactoring time!

Upravte kód tak, aby si komponent Images mohol byť istý, že môže bez kontrol pristúpiť k width a height

Výsledok

// src/services/documents/types.ts
export interface UserDocument {
    id: number;
    name: string;
    type: string;
}

export interface ImageDocument extends UserDocument {
    width: number;
    height: number;
}

Výsledok

// src/components/documents/Images.tsx
import { ImageDocument } from '@local/services/documents/types';

function Images({ images }: { images: ImageDocument[] }) {
    return (
        <ul>
            {images.map((image) => {
                return (
                    <li key={image.id}>
                        {image.name} {image.width}x{image.height}
                    </li>
                );
            })}
        </ul>
    );
}

export default Images;

Hotovo

Príklad 2

interface ChannelRegistry {
    setChannel: (newChannel: string) => void;
    getChannel: () => string;
}

function createDetector(registry: ChannelRegistry) {
    function detect() {
        // ...
        registry.setChannel(channel);
    }
    // ...
}

function ChannelProvider({ registry, children }: PropsWithChildren<{
  registry: ChannelRegistry
}>) {
    const contextData: ChannelContextData = {
        channel: registry.getChannel(),
    };
    // ....
}

Problém: klienti požadujú funkcionalitu, s ktorou nepracujú

  • createDetector používa najmä metódu setChannel
  • ChannelProvider používa iba metódu getChannel

Upravte kód tak, aby každý klient vyžadoval iba to, čo potrebuje

// src/services/channel/types.ts

export interface ChannelProvider {
    getChannel: () => string;
}

export interface ChannelRegistry extends ChannelProvider {
    setChannel: (newChannel: string) => void;
}

Riešenie

// src/components/channel/ChannelContext

export function ChannelContextProvider(
    { registry, children }:
    PropsWithChildren<{
        registry: ChannelProvider
    }>
) {
    const contextData: ChannelContextData = {
        channel: registry.getChannel(),
    };

    return (
        <ChannelContext.Provider value={contextData}>
            {children}
        </ChannelContext.Provider>
    );
}

Riešenie

Hotovo

Príklad 3

src/entity/user.ts

export interface User {
    id: number;
    firstname: string;
    lastname: string;
    email: string;
    phone: string;
    address: {
        street: string;
        city: string;
        zipCode: string;
    };
    education: {
        schoolName: string;
        yearFrom: number;
        yearTo?: number;
    }[];
    workExperience: {
        companyName: string;
        position: string;
        description: string;
        yearFrom: number;
        yearTo?: number;
    }[];
    hobby: string;
    additionalInfo: string;
}

src/components/user/LoggedInUserInfo.tsx

import { User } from '@local/entity/user';

function LoggedInUserInfo({ user }: { user: User }) {
    const name = `${user.firstname} ${user.lastname}`;

    return (
        <span>
            {name} {user.email}
        </span>
    );
}

export default LoggedInUserInfo;

Problém: klient požaduje funkcionalitu, s ktorou nepracuje

Upravte kód tak, aby klient vyžadoval iba to, čo potrebuje

src/components/user/LoggedInUserInfo.tsx

import { User } from '@local/entity/user';

type LoggedInUserInfoProps = Pick<User, 'firstname' | 'lastname' | 'email'>;

function LoggedInUserInfo({ user }: { user: LoggedInUserInfoProps }) {
    const name = `${user.firstname} ${user.lastname}`;

    return (
        <span>
            {name} {user.email}
        </span>
    );
}

export default LoggedInUserInfo;

alebo

src/components/user/LoggedInUserInfo.tsx

interface LoggedInUserInfoProps {
    firstname: string;
    lastname: string;
    email: string;
}

function LoggedInUserInfo({ user }: { user: LoggedInUserInfoProps }) {
    const name = `${user.firstname} ${user.lastname}`;

    return (
        <span>
            {name} {user.email}
        </span>
    );
}

export default LoggedInUserInfo;

Výhody použitia utility Pick

  • vyberieme si len to, čo potrebujeme
  • položky sú definované presne tak ako v rodičovi

Nevýhody použitia utility Pick

  • kód je závislý na pôvodnom type
  • ak pôvodný typ zmení definíciu položiek, musíme sa prispôsobiť

Výhody a nevýhody si musíme vždy zvážiť a nepoužívať naslepo jednu alebo druhú variantu.

Hotovo

Opakovanie

Interfejsy by mali byť jemne granulované a špecifické pre klienta

Interface Segregation Principle

Jemne granulované

  • malé množstvo položiek
  • najmenšie možné, ktoré dáva zmysel
  • áno, častokrát iba jedna položka
  • a áno, dokonca aj nula

Špecifické pre klienta

  • klient by nemal závisieť na položkách, ktoré nepoužíva
  • do interfacu sa dajú iba položky, ktoré potrebuje
  • ak viacerí klienti pracujú s triedou/objektom rôzne, tak vytvoríme viacero interfejsov
  • interface definuje skupinu položiek, ktoré "chodia spolu"

Klient je kód používajúci iný kód

Výhody dodržiavania

Interface Segregation Principle

  • časti kódu sú navzájom ľahko vymeniteľné
  • klienti závisia iba na tom, čo potrebujú

Ďakujem za pozornosť