Milan Herda, 04 / 2023
SOLID princípy v JavaScripte
Skratka predstavujúca 5 základných princípov dobrého softvérového návrhu
Nie je iba pre OOP
Princípy sú všeobecne použiteľné
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.
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.
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
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ť.
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
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
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
// 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
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);
}
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ť.
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
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
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
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
Refaktorujte, aby sme spĺňali Single Responsibility Principle
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
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