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
Zdrojové súbory:
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ť
I v SOLIDe
By Milan Herda
I v SOLIDe
- 252