Milan Herda, 02 / 2023
SOLID princípy v JavaScripte
Milan Herda, 02 / 2023
SOLID princípy v JavaScripte
Milan Herda, 02 / 2023
SOLID princípy v TypeScripte
a v Reacte
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é
Trieda by mala mať jeden (a iba jeden) dôvod pre svoju zmenu.
Trieda by mala mať jeden (a iba jeden) dôvod pre svoju zmenu.
Modul by mal mať jeden (a iba jeden) dôvod pre svoju zmenu.
Objekt by mal mať jeden (a iba jeden) dôvod pre svoju zmenu.
Funkcia by mala mať jeden (a iba jeden) dôvod pre svoju zmenu.
Jednotka kódu
by mala mať
jeden (a iba jeden) dôvod pre svoju zmenu.
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Character } from './types';
const commanders: Character[] = [];
const ambassadors: Character[] = [];
const result = await fetch('/api/list.json');
const data = await result.json();
data.forEach((character: Character) => {
if (character.role === 'commander') {
commanders.push(character);
} else if (character.role === 'ambassador') {
ambassadors.push(character);
}
});
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App commanders={commanders} ambassadors={ambassadors} />
</React.StrictMode>
);
Tento modul má dve zodpovednosti a teda aj dva dôvody pre zmenu
Skúsime modul zrefaktorovať
Stiahnite si zdrojáky
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
async function getInitialData() {
const commanders: Character[] = [];
const ambassadors: Character[] = [];
const result = await fetch('/api/list.json');
const data = await result.json();
data.forEach((character: Character) => {
if (character.role === 'commander') {
commanders.push(character);
} else if (character.role === 'ambassador') {
ambassadors.push(character);
}
});
return {
commanders,
ambassadors,
};
}
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
function mountApp(
initialData: {
commanders: Character[];
ambassadors: Character[];
}
) {
ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
).render(
<React.StrictMode>
<App
commanders={initialData.commanders}
ambassadors={initialData.ambassadors}
/>
</React.StrictMode>
);
}
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Character, getInitialData } from './services/repository/characterRepository';
async function getInitialData() {
// ...
}
function mountApp(
initialData: {
commanders: Character[];
ambassadors: Character[];
}
) {
// ...
}
mountApp(await getInitialData());
Krok 2: identifikovať skrytých kolaborantov
V našom prípade patria getInitialData aj mountApp do iných modulov
Čo je primárnou zodpovednosťou modulu?
Spustenie aplikácie
Krok 3: presun kolaborantov do samostatných modulov
Funkcia pracuje s údajmi získanými z nejakého zdroja, preto by sme v názve modulu mali mať slovíčko repository
getInitialData presunieme do samostatného modulu
services/character/characterRepository.ts
import { Character } from '@local/types';
export async function getInitialData() {
const commanders: Character[] = [];
const ambassadors: Character[] = [];
const result = await fetch('/api/list.json');
const data = await result.json();
data.forEach((character: Character) => {
if (character.role === 'commander') {
commanders.push(character);
} else if (character.role === 'ambassador') {
ambassadors.push(character);
}
});
return {
commanders,
ambassadors,
};
}
src/services/ui/mountApp.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Character } from '@local/types';
import App from '@local/App';
function mountApp(initialData: { commanders: Character[]; ambassadors: Character[] }) {
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App commanders={initialData.commanders} ambassadors={initialData.ambassadors} />
</React.StrictMode>
);
}
export default mountApp;
src/main.tsx
import { getInitialData } from '@local/services/character/characterRepository';
import mountApp from '@local/services/ui/mountApp';
mountApp(await getInitialData());
services/repository/characterRepository.ts
import { Character } from '@local/types';
export async function getInitialData() {
const commanders: Character[] = [];
const ambassadors: Character[] = [];
const result = await fetch('/api/list.json');
const data = await result.json();
data.forEach((character: Character) => {
if (character.role === RoleName.Commander) {
commanders.push(character);
} else if (character.role === RoleName.ambassador) {
ambassadors.push(character);
}
});
return {
commanders,
ambassadors,
};
}
Koľko zodpovedností má getInitialData?
Získavame dáta
Filtrujeme dáta
Pre funkciu getInitialData tak postupujeme rovnakým algoritmom, aby sme splnili SRP
services/repository/characterRepository.ts
import { filterCharactersByRole } from './filterCharacters';
export async function getInitialData() {
const characters = await loadCharacters();
return filterCharactersByRole(characters);
}
async function loadCharacters() {
const result = await fetch('/api/list.json');
return (await result.json()) as Promise<Character[]>;
}
services/repository/filterCharacters.ts
export function filterCharactersByRole(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 };
}
services/repository/characterRepository.ts
export async function getInitialData() {
const characters = await loadCharacters();
return filterCharactersByRole(characters);
}
async function loadCharacters() {
const result = await fetch('/api/list.json');
return (await result.json()) as Promise<Character[]>;
}
Čo je toto za názov?
getInitialData?
Dáva to v kontexte characterRepository zmysel?
services/repository/characterRepository.ts
export async function getFilteredCharacters() {
const characters = await loadCharacters();
return filterCharactersByRole(characters);
}
async function loadCharacters() {
const result = await fetch('/api/list.json');
return (await result.json()) as Promise<Character[]>;
}
src/main.tsx
import { getFilteredCharacters } from '@local/services/repository/characterRepository';
import mountApp from '@local/services/ui/mountApp';
mountApp(await getFilteredCharacters());
import { useContext, useState } from 'react';
import CharacterContext from './characters/CharacterContext';
import ToastContext, { ToastType } from './toast/ToastContext';
import { RoleName } from './types';
function App() {
const charactersCtx = useContext(CharacterContext);
const toastCtx = useContext(ToastContext);
const [characterName, setCharacterName] = useState<string>('');
const [characterRole, setCharacterRole] = useState<RoleName>(RoleName.Ambassador);
const roles = [RoleName.Commander, RoleName.Ambassador];
return (
<div>
<h1>Solid: Single Responsibility Principle</h1>
<h2>Babylon 5 Characters</h2>
{toastCtx.toasts.map((toast) => (
<div key={toast.id} className={`toast toast-${toast.type}`}>
{toast.message}
</div>
))}
<h3>Commanders</h3>
<ul>
{charactersCtx.commanders.map((commander) => {
return <li key={commander.id}>{commander.name}</li>;
})}
</ul>
<h3>Ambassadors</h3>
<ul>
{charactersCtx.ambassadors.map((ambassador) => {
return <li key={ambassador.id}>{ambassador.name}</li>;
})}
</ul>
<h3>Add new character</h3>
<form
onSubmit={(e) => {
e.preventDefault();
if (characterName === '') {
return;
}
charactersCtx.addCharacter(characterName, characterRole);
toastCtx.addToast(ToastType.Success, 'New character created');
setCharacterName('');
setCharacterRole(RoleName.Ambassador);
}}
>
<label>Name:</label>{' '}
<input
type="text"
value={characterName}
onChange={(e) => setCharacterName(e.target.value)}
/>
<label>Role:</label>{' '}
<select
value={characterRole}
onChange={(e) => setCharacterRole(e.target.value as RoleName)}
>
{roles.map((role) => (
<option key={role}>{role}</option>
))}
</select>
<button type="submit">Add</button>
</form>
</div>
);
}
export default App;
src/App.tsx
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
function App() {
return (
<div>
<h1>Solid: Single Responsibility Principle</h1>
<Toasts />
<CharactersPage />
</div>
);
}
function CharactersPage() {
const charactersCtx = useContext(CharacterContext);
return (
<>
<h2>Babylon 5 Characters</h2>
<ListOfCharacters title="Commanders" characters={charactersCtx.commanders} />
<ListOfCharacters title="Ambassadors" characters={charactersCtx.ambassadors} />
<NewCharacterForm />
</>
);
}
function Toasts() {}
function ListOfCharacters({ title, characters }: { title: string; characters: Character[] }) {}
function NewCharacterForm() {}
export default App;
Krok 2: identifikovať skrytých kolaborantov
Čo ostáva v module a čo doň nepatrí?
Ponecháme funkciu App, všetko ostatné ide do svojich modulov.
Krok 3: presun kolaborantov
src/characters/NewCharacterForm.tsx
function NewCharacterForm() {
const charactersCtx = useContext(CharacterContext);
const toastCtx = useContext(ToastContext);
const [characterName, setCharacterName] = useState<string>('');
const [characterRole, setCharacterRole] = useState<RoleName>(RoleName.Ambassador);
const roles = [RoleName.Commander, RoleName.Ambassador];
return (
<>
<h3>Add new character</h3>
<form
onSubmit={(e) => {
e.preventDefault();
if (characterName !== '') {
charactersCtx.addCharacter(characterName, characterRole);
toastCtx.addToast(ToastType.Success, 'New character created');
setCharacterName('');
setCharacterRole(RoleName.Ambassador);
}
}}
>
<label>Name:</label>{' '}
<input
type="text"
value={characterName}
onChange={(e) => setCharacterName(e.target.value)}
/>
<label>Role:</label>{' '}
<select
value={characterRole}
onChange={(e) => setCharacterRole(e.target.value as RoleName)}
>
{roles.map((role) => (
<option key={role}>{role}</option>
))}
</select>
<button type="submit">Add</button>
</form>
</>
);
}
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
function NewCharacterForm() {
// začiatok funkcie bezo zmeny
function validateForm() {
if (characterName === '') {
return false;
}
return true;
}
function resetForm() {
setCharacterName('');
setCharacterRole(RoleName.Ambassador);
}
function submitHandler(event: FormEvent) {
event.preventDefault();
if (validateForm()) {
charactersCtx.addCharacter(characterName, characterRole);
toastCtx.addToast(ToastType.Success, 'New character created');
resetForm();
}
}
return (
<>
<h3>Add new character</h3>
<form onSubmit={submitHandler}>
{/* zvysok formularu bezo zmeny */}
</form>
</>
);
}
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
function useCharacterForm(onSuccess?: (name: string, role: RoleName) => void) {
const [characterName, setCharacterName] = useState<string>('');
const [characterRole, setCharacterRole] = useState<RoleName>(RoleName.Ambassador);
function validateForm() {
// ostal pôvodný kód
}
function resetForm() {
// ostal pôvodný kód
}
function submitHandler(event: FormEvent) {
event.preventDefault();
if (validateForm()) {
onSuccess?.(characterName, characterRole);
resetForm();
}
}
return {
characterName: {
value: characterName,
setValue: setCharacterName,
},
characterRole: {
value: characterRole,
setValue: setCharacterRole,
},
submitHandler,
};
}
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
function NewCharacterForm() {
const charactersCtx = useContext(CharacterContext);
const toastCtx = useContext(ToastContext);
const roles = [RoleName.Commander, RoleName.Ambassador];
const form = useCharacterForm((characterName, characterRole) => {
charactersCtx.addCharacter(characterName, characterRole);
toastCtx.addToast(ToastType.Success, 'New character created');
});
return (
<>
<h3>Add new character</h3>
<form onSubmit={form.submitHandler}>
<label>Name:</label>{' '}
<input
type="text"
value={form.characterName.value}
onChange={(e) => form.characterName.setValue(e.target.value)}
/>
<label>Role:</label>{' '}
<select
value={form.characterRole.value}
onChange={(e) => form.characterRole.setValue(e.target.value as RoleName)}
>
{roles.map((role) => (
<option key={role}>{role}</option>
))}
</select>
<button type="submit">Add</button>
</form>
</>
);
}
Krok 2: identifikovať skrytých kolaborantov
Krok 3: presun kolaborantov
function useCharacterForm(onSuccess?: (name: string, role: RoleName) => void) {
const [characterName, setCharacterName] = useState<string>('');
const [characterRole, setCharacterRole] = useState<RoleName>(RoleName.Ambassador);
function validateForm() {
if (characterName === '') {
return false;
}
return true;
}
function resetForm() {
setCharacterName('');
setCharacterRole(RoleName.Ambassador);
}
function submitHandler(event: FormEvent) {
event.preventDefault();
if (validateForm()) {
onSuccess?.(characterName, characterRole);
resetForm();
}
}
return {
characterName: {
value: characterName,
setValue: setCharacterName,
},
characterRole: {
value: characterRole,
setValue: setCharacterRole,
},
submitHandler,
};
}
Koľko dôvodov na zmenu vieme nájsť pre useCharacterForm?
Koľko dôvodov na zmenu viem nájsť pre useCharacterForm?
správa mena
správa role
function useNameInput() {
const [characterName, setCharacterName] = useState('');
return {
value: characterName,
setValue: setCharacterName,
validate: () => characterName !== '',
reset: () => setCharacterName(''),
};
}
function useRoleInput() {
const [characterRole, setCharacterRole] = useState<RoleName>(RoleName.Ambassador);
return {
value: characterRole,
setValue: setCharacterRole,
validate: () => true,
reset: () => setCharacterRole(RoleName.Ambassador),
};
}
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
function useCharacterForm(onSuccess?: (name: string, role: RoleName) => void) {
const characterName = useNameInput();
const characterRole = useRoleInput();
function validateForm() {
return (characterName.validate() && characterRole.validate());
}
function resetForm() {
characterName.reset();
characterRole.reset();
}
return {
characterName,
characterRole,
submitHandler: (event: FormEvent) => {
event.preventDefault();
if (validateForm()) {
onSuccess?.(characterName.value, characterRole.value);
resetForm();
}
},
};
}
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
Krok 2: identifikovať skrytých kolaborantov
Krok 3: presun kolaborantov
Nemusíme v tomto prípade riešiť
import type { Station } from './types';
export function createFromData(data: Record<string, string>): Station {
const { id, name } = data;
return {
getId: function () {
return id;
},
getName: function () {
return name;
},
save: async function () {
const result = await fetch(`/api/station/${id}`, {
method: 'POST',
body: JSON.stringify({ id, name }),
headers: {
'Content-Type': 'application/json',
},
});
if (result.status === 201) {
return true;
} else {
throw new Error(`Server responded with code ${result.status}`);
}
},
};
}
src/station/domain/entity/station.ts
Krok 1: extrahovať špecifické úlohy do pomocných funkcií
Už je to pekne oddelené
Krok 2: identifikovať skrytých kolaborantov
Krok 3: presun kolaborantov
V prípade anemického modelu aj DDD presunieme implementáciu metódy save do repozitáru
Ukážme si DDD prístup
import { Station } from '../entity/types';
export type PersistStationFunc = (station: Station) => Promise<boolean>;
src/station/domain/repository/stationRepository.ts
import { Station } from '@local/station/domain/entity/types';
import { PersistStationFunc } from '@local/station/domain/repository/stationRepository';
export const saveStation: PersistStationFunc = async function saveStation(station: Station) {
const result = await fetch(`/api/station/${station.getId()}`, {
method: 'POST',
body: JSON.stringify({ id: station.getId(), name: station.getName() }),
headers: {
'Content-Type': 'application/json',
},
});
if (result.status === 201) {
return true;
} else {
throw new Error(`Server responded with code ${result.status}`);
}
return false;
};
src/station/infrastructure/repository/stationRepository.ts
import type { Station } from './types';
import type { PersistStationFunc } from '../repository/stationRepository';
export function createFromData(data: Record<string, string>): Station {
const { id, name } = data;
return {
getId: function () {
return id;
},
getName: function () {
return name;
},
save: async function (persist: PersistStationFunc) {
if (await persist(this)) {
return true;
}
return false;
},
};
}
src/station/domain/entity/station.ts
import { PersistStationFunc } from '../repository/stationRepository';
export interface Station {
getId: () => string;
getName: () => string;
save: (persist: PersistStationFunc) => Promise<boolean>;
}
src/station/domain/entity/types.ts
Môžeme ešte rozoberať rôzne pohľady a implementácie DDD
Ale to už nepatrí do debaty
o Single Responsibility Principle
Jednotka by mala mať jeden (a iba jeden) dôvod pre svoju zmenu.
Symptómy porušenia princípu
Námietka voči Single Responsibility Principle
SRP nás núti vytvárať priveľa funkcií a súborov a tým sa stráca prehľadnosť