O v SOLIDe
Milan Herda, 04 / 2023
SOLID princípy v JavaScripte
O čom budeme hovoriť
- Čo je SOLID
- Open-Closed Principle
- Výhody Open-Closed Principle
- 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
SOLID
Nie je iba pre OOP
Princípy sú všeobecne použiteľné
Open-Closed Principle
OCP
Open-Closed Principle
OCP
Jednotka kódu by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny.
Jednotka kódu by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny.
Trieda by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny.
Objekt by mal byť otvorený pre rozširovanie a zároveň uzatvorený pre zmeny.
Modul by mal byť otvorený pre rozširovanie a zároveň uzatvorený pre zmeny.
Funkcia by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny.
Jednotka kódu by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny.
Mali by sme byť schopní rozšíriť správanie aj bez potreby modifikácie kódu.
Výhody dodržiavania
Open-Closed Principle
- možnosť zmeniť správanie časti kódu aj bez jej úpravy
- bezpečnejšie úpravy (lebo nemeníme kód)
Symptómy porušenia princípu
- v kóde sú podmienky pre určenie stratégie (typicky napr. switch)
- podobné podmienky sa opakujú na viacerých miestach v kóde
- kód obsahuje hardkódovaný názov súboru, emailovú adresu alebo inú skalárnu hodnotu
- trieda obsahuje natvrdo nakódované názvy iných tried (v poli, podmienkach, stringoch)
- vo vnútri triedy sa vytvárajú objekty pomocou new
Ako refaktorovať
- zabezpečiť spĺňanie Single Responsibility Principle
- identifikovať všeobecné a špecifické časti úlohy
- oddeliť špecifické a všeobecné úlohy
- špecifické časti preniesť von
- prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"
function mountApp(
initialData: {
commanders: Character[];
ambassadors: Character[];
}
) {
ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
).render(
<App initialData={initialData} />
);
}
function mountApp(
initialData: {
commanders: Character[];
ambassadors: Character[];
}
) {
ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
).render(
<App initialData={initialData} />
);
}
Zmeňte element, do ktorého sa rendruje.
Nie je to možné bez modifikácie funkcie.
Refactoring time!
Stiahnite si zdrojáky
Krok 1: Zabezpečiť spĺňanie Single-Responsibility princípu
Funkcia už spĺňa SRP
Krok 2: Identifikovať všeobecné a špecifické časti úlohy
ID elementu je špecifická časť
A vlastne aj element samotný je špecifická časť
Krok 3: Oddeliť špecifické a všeobecné úlohy
Element nebude získavaný vo vnútri funkcie
Krok 4: Špecifické časti preniesť von
Element sa získa mimo tela funkcie a funkcia ho len použije
Krok 5: Prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"
Element sa do funkcie dostane ako argument pri volaní
function mountApp(
rootElement: HTMLElement,
initialData: { commanders: Character[]; ambassadors: Character[] }
) {
ReactDOM.createRoot(rootElement).render(
<App initialData={initialData} />
);
}
src/services/ui/mountApp.tsx
mountApp(
document.getElementById('root') as HTMLElement,
await getSortedCharacters()
);
src/main.tsx
Príklad 2
import type { Character } from '@local/types';
import { sortCharactersByRole } from './sortCharactersByRole';
export async function getSortedCharacters() {
const characters = await loadCharacters();
return sortCharactersByRole(characters);
}
async function loadCharacters() {
const result = await fetch('/api/list.json');
return (await result.json()) as Promise<Character[]>;
}
src/services/character/characterRepository.ts
Zmeňte URL, z ktorej sa načítava zoznam postáv.
Chceme rozšíriť funkciu o možnosť
načítavať zoznam postáv z inej URL
Nie je to možné bez zmeny jej kódu.
Funkcia tak nespĺňa Open-Closed Principle
Poďme ju zrefaktorovať.
- Zabezpečiť spĺňanie Single-Responsibility princípu
- Funkcia už spĺňa
- Identifikovať všeobecné a špecifické časti úlohy
- hardkódovaná URL je špecifická
- Oddeliť špecifické a všeobecné časti
- vložíme URL do premennej
- Špecifické časti preniesť von
- premenná z URL sa stane parametrom funkcie
- Prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"
- pri volaní funkcie nastavíme URL
Naprogramujte to sami
import type { Character } from '@local/types';
import { sortCharactersByRole } from './sortCharactersByRole';
export async function getSortedCharacters() {
const characters = await loadCharacters('/api/list.json');
return sortCharactersByRole(characters);
}
async function loadCharacters(url: string) {
const result = await fetch(url);
return (await result.json()) as Promise<Character[]>;
}
src/services/character/characterRepository.ts
Hotovo
Naozaj?
import type { Character } from '@local/types';
import { sortCharactersByRole } from './sortCharactersByRole';
export async function getSortedCharacters() {
const characters = await loadCharacters('/api/list.json');
return sortCharactersByRole(characters);
}
async function loadCharacters(url: string) {
const result = await fetch(url);
return (await result.json()) as Promise<Character[]>;
}
src/services/character/characterRepository.ts
-
spĺňa teraz getSortedCharacters OCP?
- nie
-
a má vôbec getFilteredCharacters niečo vedieť o nejakej URL?
- nemá
Krátke a jednoduché riešenie
- aby funkcia spĺňala OCP, tak nám stačí urobiť minimálne množstvo krokov
- nemusíme hneď využiť všetky možnosti, ktoré refaktoringom získame
- nemusíme vymýšľať novú architektúru aplikácie
- je úplne dostačujúce, pokiaľ máme k dispozícii potenciál
V našom prípade stačí dať parametru defaultnú hodnotu.
import type { Character } from '@local/types';
import { sortCharactersByRole } from './sortCharactersByRole';
export async function getSortedCharacters() {
const characters = await loadCharacters();
return sortCharactersByRole(characters);
}
async function loadCharacters(url = '/api/list.json') {
const result = await fetch(url);
return (await result.json()) as Promise<Character[]>;
}
src/services/character/characterRepository.ts
Hotovo
Dlhšie a komplikovanejšie riešenia
- URL bude uložená v enume, konštante alebo zozname URL v nejakom konfiguračnom súbore
- alebo: modul bude mať privátnu premennú držiacu URL a poskytne metódu na jej nastavenie zvonka
- alebo: funkcia si URL vypýta z nejakého "registra" (pozor na vznik veľkého Service Locator-a)
// config/api.ts
export enum ApiUrl {
LoadCharacters = '/api/list.json',
}
// src/services/character/characterRepository.ts
import type { Character } from '@local/types';
import { sortCharactersByRole } from './sortCharactersByRole';
import ApiUrl from '@local/config/api';
export async function getSortedCharacters() {
const characters = await loadCharacters();
return sortCharactersByRole(characters);
}
async function loadCharacters() {
const result = await fetch(ApiUrl.LoadCharacters);
return (await result.json()) as Promise<Character[]>;
}
src/services/character/characterRepository.ts
// src/services/character/characterRepository.ts
import type { Character } from '@local/types';
import { sortCharactersByRole } from './sortCharactersByRole';
let loadCharactersUrl = '/api/list.json';
export function setLoadCharactersUrl(url) {
loadCharactersUrl = url;
}
export async function getSortedCharacters() {
const characters = await loadCharacters();
return sortCharactersByRole(characters);
}
async function loadCharacters() {
const result = await fetch(loadCharactersUrl);
return (await result.json()) as Promise<Character[]>;
}
src/services/character/characterRepository.ts
// src/services/api/urlRegistry
const registry: Record<string, string> = {};
export function setUrl(name, url) {
registry[name] = url;
};
export function getUrl(name) {
if (registry[name] === undefined) {
throw new Error(`Unknown URL '${name}'`);
}
return registry[name];
}
// // src/services/character/characterRepository.ts
import type { Character } from '@local/types';
import { sortCharactersByRole } from './sortCharactersByRole';
import { getUrl } from '@local/services/api/urlRegistry';
// ...
async function loadCharacters() {
const result = await fetch(getUrl('loadCharactersUrl'));
return (await result.json()) as Promise<Character[]>;
}
src/services/character/characterRepository.ts
Aplikovaním SOLID princípov môžeme
odkryť nečakané abstrakcie
Aplikovaním SOLID princípov môžeme
odkryť nečakané abstrakcie
async function loadCharacters(url = '/api/list.json') {
const result = await fetch(url);
return (await result.json()) as Promise<Character[]>;
}
Aplikovaním SOLID princípov môžeme
odkryť nečakané abstrakcie
async function loadJson(url = '/api/list.json') {
const result = await fetch(url);
return (await result.json()) as Promise<Character[]>;
}
Aplikovaním SOLID princípov môžeme
odkryť nečakané abstrakcie
async function loadJson(url) {
const result = await fetch(url);
return (await result.json()) as Promise<Character[]>;
}
Aplikovaním SOLID princípov môžeme
odkryť nečakané abstrakcie
async function loadJson<T>(url) {
const result = await fetch(url);
return (await result.json()) as Promise<T>;
}
Aplikovaním SOLID princípov môžeme
odkryť nečakané abstrakcie
async function loadJson<T>(url) {
const result = await fetch(url);
return (await result.json()) as Promise<T>;
}
async function loadCharacters(url = '/api/list.json') {
return await loadJson<Character[]>(url);
}
Hotovo
Príklad 3
import { Character } from '@local/types';
export function sortCharactersByRole(characters: Character[]) {
const commanders: Character[] = [];
const ambassadors: Character[] = [];
characters.forEach((character: Character) => {
if (character.role === RoleName.Commander) {
commanders.push(character);
} else if (character.role === RoleName.Ambassador) {
ambassadors.push(character);
}
});
return {
commanders,
ambassadors,
};
}
src/services/character/sortCharactersByRole.ts
Pod kľúč "aides" pridajte sortovanie podľa role "diplomatic-attache"
import { Character } from '@local/types';
export function sortCharactersByRole(characters: Character[]) {
const commanders: Character[] = [];
const ambassadors: Character[] = [];
const aides: Character[] = [];
characters.forEach((character: Character) => {
if (character.role === 'commander') {
commanders.push(character);
} else if (character.role === 'ambassador') {
ambassadors.push(character);
} else if (character.role === 'diplomatic-attache') {
aides.push(character);
}
});
return {
commanders,
ambassadors,
aides,
};
}
src/services/character/sortCharactersByRole.ts
Síce sme funkciu rozšírili o možnosť triedenia podľa novej role,
ale iba tak, že sme zmenili jej kód.
Funkcia tak nespĺňa Open-Closed Principle.
Poďme ju zrefaktorovať.
- Zabezpečiť spĺňanie Single-Responsibility princípu
- povedzme, že spĺňa
- Identifikovať všeobecné a špecifické časti úlohy
- špecifické sú názvy rolí a sortovacie kľúče
- Oddeliť špecifické a všeobecné časti
- špecifické časti dáme do samostatnej "mapovacej" premennej
import { Character } from '@local/types';
export function sortCharactersByRole(characters: Character[]) {
const sortedCharacters: Record<string, Character[]> = {};
const roleToSortKey: Record<RoleName, string> = {
[RoleName.Commander]: 'commanders',
[RoleName.Ambassador]: 'ambassadors',
[RoleName.DiplomaticAttache]: 'aides',
};
characters.forEach((character: Character) => {
const sortKey = roleToSortKey[character.role];
if (!sortKey) {
return;
}
if (sortedCharacters[sortKey] === undefined) {
sortedCharacters[sortKey] = [];
}
sortedCharacters[sortKey].push(character);
});
return sortedCharacters;
}
src/services/character/sortCharactersByRole.ts
- Zabezpečiť spĺňanie Single-Responsibility princípu
- Povedzme, že spĺňa
- Identifikovať všeobecné a špecifické časti úlohy
- špecifické sú názvy rolí a sortovacie kľúče
- Oddeliť špecifické a všeobecné časti
- špecifické časti dáme do samostatnej "mapovacej" premennej
- Špecifické časti preniesť von
- premennú vytiahneme von z funkcie
import { Character } from '@local/types';
const roleToSortKey: Record<RoleName, string> = {
[RoleName.Commander]: 'commanders',
[RoleName.Ambassador]: 'ambassadors',
[RoleName.DiplomaticAttache]: 'aides',
};
export function sortCharactersByRole(characters: Character[]) {
const sortedCharacters: Record<string, Character[]> = {};
characters.forEach((character: Character) => {
const sortKey = roleToSortKey[character.role];
if (!sortKey) {
return;
}
if (sortedCharacters[sortKey] === undefined) {
sortedCharacters[sortKey] = [];
}
sortedCharacters[sortKey].push(character);
});
return sortedCharacters;
}
src/services/character/sortCharactersByRole.ts
- Zabezpečiť spĺňanie Single-Responsibility princípu
- Povedzme, že spĺňa
- Identifikovať všeobecné a špecifické časti úlohy
- špecifické sú názvy rolí a sortovacie kľúče
- Oddeliť špecifické a všeobecné časti
- špecifické časti dáme do samostatnej "mapovacej" premennej
- Špecifické časti preniesť von
- premennú vytiahneme von z funkcie
- Prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"
- pri volaní funkcie nastavíme parameter so špecifikáciou mapovania
- alebo: poskytneme setter pre premennú
- alebo: mapovacia premenná pôjde do konfiguračného súboru
- ...
import { Character } from '@local/types';
const defaultRoleToSortKey: Record<RoleName, string> = {
[RoleName.Commander]: 'commanders',
[RoleName.Ambassador]: 'ambassadors',
[RoleName.DiplomaticAttache]: 'aides',
};
export function sortCharactersByRole(
characters: Character[],
roleToSortKey = defaultRoleToSortKey
) {
const sortedCharacters: Record<string, Character[]> = {};
characters.forEach((character: Character) => {
const sortKey = roleToSortKey[character.role];
if (!sortKey) {
return;
}
if (sortedCharacters[sortKey] === undefined) {
sortedCharacters[sortKey] = [];
}
sortedCharacters[sortKey].push(character);
});
return sortedCharacters;
}
src/services/character/sortCharactersByRole.ts
Hotovo
Naozaj?
Koľko má teraz funkcia zodpovedností?
import { Character } from '@local/types';
const defaultRoleToSortKey: Record<RoleName, string> = {
[RoleName.Commander]: 'commanders',
[RoleName.Ambassador]: 'ambassadors',
[RoleName.DiplomaticAttache]: 'aides',
};
export function sortCharactersByRole(
characters: Character[],
roleToSortKey = defaultRoleToSortKey
) {
const sortedCharacters: Record<string, Character[]> = {};
characters.forEach((character: Character) => {
const sortKey = roleToSortKey[character.role];
if (!sortKey) {
return;
}
if (sortedCharacters[sortKey] === undefined) {
sortedCharacters[sortKey] = [];
}
sortedCharacters[sortKey].push(character);
});
return sortedCharacters;
}
src/services/character/sortCharactersByRole.ts
- získavanie sortovacieho kľúča
- pridávanie postáv do správneho poľa
- triedenie postáv
Refaktorujte, aby sme spĺňali Single Responsibility Principle
- extrahovať špecifické úlohy do pomocných funkcií
- identifikovať skrytých kolaborantov
- preniesť kód skrytých kolaborantov do samostatných modulov
const defaultRoleToSortKey: Record<RoleName, string> = { ... };
export function getKeyByRole(role: RoleName) {
return sortKeyByRole[role];
}
export function addToSortedCharacters(
character: Character,
sortedCharacters: Record<string, Character[]>,
sortKey: string
) {
if (sortedCharacters[sortKey]) {
sortedCharacters[sortKey].push(character);
} else {
sortedCharacters[sortKey] = [character];
}
}
export function sortCharactersByRole(
characters: Character[],
roleToSortKey = defaultRoleToSortKey
) {
const sortedCharacters: Record<string, Character[]> = {};
characters.forEach((character: Character) => {
const sortKey = getKeyByRole(character.role);
if (sortKey) {
addToSortedCharacters(character, sortedCharacters, sortKey);
}
});
return sortedCharacters;
}
src/services/character/sortCharactersByRole.ts
Hotovo
Opakovanie
Jednotka kódu by mala byť otvorená pre rozširovanie a zároveň uzatvorená pre zmeny
Mali by sme byť schopní rozšíriť správanie jednotky aj bez potreby modifikácie jej kódu
Výhody dodržiavania
Open-Closed Principle
- možnosť zmeniť správanie jednotky kódu aj bez jej úpravy
- bezpečnejšie úpravy (lebo nemeníme kód)
Symptómy porušenia princípu
- v kóde sú podmienky pre určenie stratégie (typicky napr. switch)
- podobné podmienky sa opakujú na viacerých miestach v kóde
- kód obsahuje hardkódovaný názov súboru, emailovú adresu alebo inú skalárnu hodnotu
- trieda obsahuje natvrdo nakódované názvy iných tried (v poli, podmienkach, stringoch)
- vo vnútri triedy sa vytvárajú objekty pomocou new
Ako refaktorovať
- zabezpečiť spĺňanie Single Responsibility Principle
- identifikovať všeobecné a špecifické časti úlohy
- oddeliť špecifické a všeobecné úlohy
- špecifické časti preniesť von
- prepojiť všeobecnú a špecifickú časť pomocou "konfigurácie"
Ďakujem za pozornosť
O v SOLIDe (JavaScript)
By Milan Herda
O v SOLIDe (JavaScript)
- 270