SOLID princípy a JavaScript

2026-10

SOLID princípy a JavaScript

Milan Herda

2026-10

SOLID princípy a TypeScript

Milan Herda

2026-10

Čo je SOLID?

Skratka, pod ktorou sa skrýva

päť základných princípov

dobrej softvérovej architektúry

Čo je SOLID?

Päť základných princípov = mali by sme všetci poznať

Poznáme? 😅

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Koľkí z vás by vedeli kolegovi vysvetliť:

Čo je SOLID?

 Nepoznáme definíciu ❌

Ale aplikujeme intuitívne ✔️

Je to zle? 🤷‍♂️

Nemusíme poznať definíciu na to, aby sme niečo úspešne používali.

Čo je SOLID?

Nemusíme poznať definíciu na to, aby sme niečo úspešne používali.

Avšak, keď tú definíciu poznáme, tak:

  • premýšľame jasnejšie
  • pýtame sa a hľadáme presnejšie
  • vidíme ďalej

SOLID a OOP

SOLID princípy sú často vysvetľované na triedach.

Takže vzniká dojem, že sa týkajú iba OOP programovania.

Sú však aplikovateľné aj mimo tried a objektov.

1. Single Responsibility Principle

Princíp jedinej zodpovednosti

Definícia

Modul by mal mať jeden (a iba jeden) dôvod pre svoju zmenu.

SRP

Definícia

Modul by mal mať jeden (a iba jeden) dôvod pre svoju zmenu.

💡Pomôcka pre uvažovanie:

Veci, ktoré sa menia z rovnakých dôvodov, dávame k sebe.

Tie, ktoré sa menia z odlišných dôvodov, patria do odlišných modulov.

📝 Príklady:

  • zmena dizajnu by nemala ovplyvniť databázovú štruktúru
  • výmena frameworku nesmie vyžadovať zmeny v biznis modeli firmy

SRP

Definícia

Modul by sa mal zodpovedať jednému (a iba jednému) aktérovi.

🤔 Kto je aktér?

Aktér je jedna alebo viac osôb,

ktoré chcú rovnakú zmenu v systéme.

📝 Príklady:

  • finančné oddelenie
  • právne oddelenie
  • UX oddelenie
  • IT bezpečnosť
  • technický architekt
  • používatelia

SRP

SRP

// src/main.tsx

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

const topTenBooks = [];
const result = await fetch("/api/books.json");

if (result.ok) {
	const data = await result.json();

	topTenBooks.push(...data.items);
}

createRoot(document.getElementById("root")!).render(
	<StrictMode>
		<App topTenBooks={topTenBooks} />
	</StrictMode>,
);

Koľko zodpovedností alebo dôvodov na zmenu vidíte?

🔍

  • zodpovednosť: získanie dát
  • dôvod zmeny: spôsob získavania dát
  • zodpovednosť: inicializácia aplikácie
  • dôvod zmeny: výmena frameworku

SRP

Ako refaktorovať?

🤔

Oddelenie do samostatných modulov

// src/main.ts

import loadTopTenBooks from './books/repo/loadTopTen';
import mountApp from './infra/fw/mountApp';

const books = loadTopTenBooks();

mountApp(books);

SRP

// src/book/repo/loadTopTen.ts

export async function loadTopTenBooks() {
  const topTenBooks: Book[] = [];

  const result = await fetch("/api/books.json");

  if (result.ok) {
    const data = await result.json();

    topTenBooks.push(...data.items);
  }
  
  return topTenBooks;
}

🎉

// src/infra/fw/mountApp.tsx

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { type Book } from "./dto/Book";

export function mountApp(
  topTenBooks: Book[]
) {
  const root = createRoot(
    document.getElementById("root")!
  );
  
  root.render(
    <StrictMode>
      <App topTenBooks={topTenBooks} />
    </StrictMode>
  ),
};

Po oddelení do vlastných modulov

SRP

// src/main.ts

import loadTopTenBooks from './books/repo/loadTopTen';
import mountApp from './infra/fw/mountApp';

const books = loadTopTenBooks();

mountApp(books);

👍

Čo sme týmto získali?

  • jasnejšie oddelenie hraníc
  • bezpečnejšie zmeny
  • potenciál na jednoduchšie výmeny modulov alebo celých aplikačných vrstiev

2. Open-Closed Principle

Princíp otvorenej uzavretosti

Definícia

Každý softvérový artefakt (trieda, modul, funkcia) by mal byť otvorený pre rozširovanie, ale uzatvorený pre úpravy.

💡Pomôcka pre uvažovanie:

Píšme kód tak, aby sme vedeli modifikovať jeho správanie aj bez jeho prepisu.

📝 Príklady:

  • používajme parametre a konfiguráciu
  • vyhýbajme sa detailom a konkrétnostiam v kóde (adresy, texty, hodnoty...)

K tomu potrebujeme oddeliť konkrétne časti od všeobecných.

OCP

OCP

// src/book/repo/loadTopTen.ts

export async function loadTopTenBooks() {
  const data = await loadData("/api/books.json");

  return data.items.map(
    (bookData) => createBook(bookData)
  );
}

🔍

Vidíte konkrétnosti, ktoré vieme oddeliť?

OCP

// src/book/repo/loadTopTen.ts

export async function loadTopTenBooks(sourceUrl: string = "/api/books.json") {
  const data = await loadData(sourceUrl);

  return data.items.map(
    (bookData) => createBook(bookData)
  );
}

🎉

Oddeľme túto konkrétnosť od všeobecného kódu

OCP

// src/book/repo/loadTopTen.ts

export async function loadTopTenBooks(sourceUrl: string = "/api/books.json") {
  const data = await loadData(sourceUrl);

  return data.items.map(
    (bookData) => createBook(bookData)
  );
}

Čo sme týmto získali?

👍

  • teraz už dokážeme načítať zoznam kníh aj z inej URL
  • takže už vieme rozšíriť správanie funkcie bez dodatočnej zmeny jej kódu

3. Liskov Substitution Principle

Liskovej princíp nahraditeľnosti

Definícia

💡Pomôcka pre uvažovanie:

Potomok musí byť dobrou náhradou svojho rodiča.

Ak z nejakého kódu odoberiem rodiča a na to isté miesto dosadím jeho potomka, kód musí fungovať bez chyby.

Nemôžeme sprísňovať požiadavky na argumenty a nemôžeme rozširovať typy návratovej hodnoty.

LSP

LSP

interface Encoder {
  encode: (value: string | number) => string;
}

export async function saveEncrypted(
  encoder: Encoder
) {
  const secrets = readRawSecrets();

  const encoded: string[] = secrets.map(
    (secret) => {
      return encoder.encode(secret);
    }
  );
  
  // ...
}

🔍

Ktoré objekty sú dobrým potomkom typu Encoder?

const encoderA: Encoder {
  encode(value: string | number): string {
    // ...
  }
}

const encoderB: Encoder {
  encode(value: string): string {
    // ...
  }
}

const encoderC: Encoder {
  encode(
    value: string | number | boolean
  ): string {
    // ...
  }
}

LSP

🔍

Ktoré funkcie sú dobrým potomkom typu Encode?

type Encode = (value: string | number) => string;

function encode(value: string | number): string {
  // ...
}

function encode(value: string): string {
  // ...
}

function encode(
  value: string | number | boolean
): string {
  // ...
}

function encode(value: string | number): string | number {
  // ...
}

function encode(value: string | number): "foo" | "bar" {
  // ...
}

Totožný podpis

Sprísnený vstupný kontrakt

Zvoľnený vstupný kontrakt

Zvoľnený výstupný kontrakt

Sprísnený výstupný kontrakt

LSP

Čo sme týmto získali?

👍

  • časti kódu sú navzájom ľahko vymeniteľné
  • vieme jednoducho upraviť správanie výmenou modulu (triedy, objektu, funkcie)

4. Interface Segregation Principle

Princíp oddeľovania rozhraní

Definícia

Žiaden kód by nemal závisieť na niečom, čo nepoužíva.

ISP

ISP

function isUnitEngaged(unit: Unit, gameState: GameState) {
  return (
    gameState.engagedUnitIds.find(id => id === unit.id) !== undefined
  );
}
interface Unit {
    id: number;
    typeId: UnitTypeId;
    label: string;
    sideId: SideId;
    position: Position;
    occupiedHexes: Position[];
    isInColumn: boolean;
    rotation: CounterRotation;
    // ...
}
interface GameState {
  stateName: PossibleStates;
  round: number;
  phase: PhaseName;
  activeSide: SideId;
  units: Scenario['units'];
  leaders: Scenario['leaders'];
  engagedUnitIds: Unit['id'][];
  // ...
}

Čo v skutočnosti potrebuje funkcia isUnitEngaged?

🔍

ISP

function isUnitEngaged(unit: Pick<Unit, 'id'>, gameState: Pick<GameState, 'engagedUnitIds'>) {
  return (
    gameState.engagedUnitIds.find(id => id === unit.id) !== undefined
  );
}

Čo v skutočnosti potrebuje funkcia isUnitEngaged?

🔍

Vypýtajme si iba tie property a metódy, s ktorými pracujeme

LSP

Čo sme týmto získali?

👍

  • kód závisí iba na tom, čo potrebuje
  • jednoduchšie testovanie
  • moduly vieme ľahšie vymieňať, lebo je medzi nimi menej závislostí a tieto sú viditeľnejšie

5. Dependency Inversion Principle

Princíp obrátenia závislostí

Definícia

Vysokoúrovňové moduly by nemali importovať nič z nízkoúrovňových. Oboje by mali závisieť na abstrakciách.

Abstrakcie by nemali závisieť na detailoch. Detaily by mali závisieť na abstrakciách.

DIP

Sme zvyknutí závisieť na detailoch

import prismaClient from '@local/infrastructure/prisma/client';

async function BookDetailPage({ bookId }: { bookId: string}) {
  // ...

  const book = await prismaClient.book.findUnique({
    where: {
      id: bookId,
    }
  });
  
  // ...
}

DIP

  • UI vrstva má priamu vedomosť o databáze
  • vie s ňou priamo komunikovať
  • stávajú sa tak pevne prepojené
  • a nič z toho nie je dobré

Odstráňme implementačný detail

import { findBook } from '@local/infrastructure/book/repository';


async function BookDetailPage({ bookId }: { bookId: string}) {
  // ...

  const book = await findBook(bookId);
  
  // ...
}

DIP

  • zrušili sme závislosť priamo na databáze
  • repozitárová funkcia findBook skrýva svoju implementáciu
  • medzi UI a databázovým konektorom tak neexistuje priame prepojenie

Skúsme ešte lepšie

type FindBookFunc = (bookId: string) => Promise<Book | undefined>;


async function BookDetailPage(
  { bookId, findBook }: { bookId: string, findBook: FindBookFunc }
) {
  // ...

  const book = await findBook(bookId);
  
  // ...
}

DIP

  • Funkcia teraz závisí iba na type
  • Typ je abstraktný
  • Zmizlo aj nepriame prepojenie na databázu

Skúsme ešte lepšie

type FindBookFunc = (bookId: string) => Promise<Book | undefined>;


async function BookDetailPage(
  { bookId, findBook }: { bookId: string, findBook: FindBookFunc }
) {
  // ...

  const book = await findBook(bookId);
  
  // ...
}

DIP

Kde ale teraz naimportujeme repozitár a pošleme ho ako argument do BookDetail Page?

  • To je starosť volajúceho kódu
  • controller, funkcia "main" a pod.
  • Dependency Injection Container

🤔

DIP

Čo sme týmto získali?

👍

  • jednotlivé časti kódu sú izolované
  • jednoduchšie testovanie
  • moduly vieme ľahšie vymieňať, lebo je jednoduchšie vyrobiť implementáciu niečoho abstraktného, ako kopírovať detailné API

Záver

Čo je SOLID?

  • Single Responsibility Principle
    • modul sa zodpovedá iba jednému aktérovi a má iba jeden dôvod pre zmenu
  • Open-Closed Principle
    • modul je otvorený pre rozširovanie bez potreby zmeny svojho kódu
  • Liskov Substitution Principle
    • modul je dobrým potomkom svojich rodičov
  • Interface Segregation Principle
    • modul závisí iba na tom, čo používa
  • Dependency Inversion Principle
    • modul nezávisí na konkrétnostiach

Päť základných princípov dobrej softvérovej architektúry:

Výhody

  • v kóde sa nám zrazu samé od seba objavujú konštrukcie známe ako Návrhové vzory
  • v architektúre aplikácie vznikajú viditeľné a nezávislé vrstvy
  • kód sa ľahko testuje
  • moduly sa ľahko nahradzujú
  • aplikácia sa ľahko udržiava a rozširuje

Pri dôslednej aplikácii všetkých princípov:

Ďakujem za pozornosť

Milan Herda

  • programátor
  • Alma Career
    • Lead Engineer
    • Vedúci "Expertného tímu pre frontend a JavaScript"
  • školím a vzdelávam
  • programujem webové hry
  • mám rád spoločenské hry
  • https://perunhq.org/