Ezeket a tervezési mintákat minden fejlesztőnek ismernie kell

Ezeket a tervezési mintákat minden fejlesztőnek ismernie kell

Ha tetszett, oszd meg ismerőseiddel!

Akár kezdőként, akár tapasztalt fejlesztőként dolgozol, biztosan találkoztál már ismétlődő programozási problémákkal, amelyekre újra és újra megoldást kellett keresned. A szoftvertervezési minták pontosan ezekre a visszatérő helyzetekre kínálnak jól bevált, újrahasznosítható sablonokat. Ezek a minták nemcsak leegyszerűsítik a fejlesztési folyamatot, hanem segítenek abban is, hogy a kódod átláthatóbb, karbantarthatóbb és bővíthetőbb legyen.

Ebben a bejegyzésben bemutatok 7 olyan tervezési mintát, amelyeket minden fejlesztőnek érdemes ismernie – hiszen ezek a megoldások szinte minden modern szoftverprojektben előkerülnek, függetlenül a választott programozási nyelvtől vagy platformtól.

A Gangs of Four és a tervezési minták születése

A tervezési minták egyáltalán nem új keletű dolgok, de a szoftverfejlesztésben való elterjedésük mégis viszonylag „fiatal” történet. Maga a minták (patterns) gondolata az építészetből származik: Christopher Alexander már az 1970-es években leírta, hogyan lehet visszatérő problémákra általános, újrahasznosítható megoldásokat adni. Ezeket a gondolatokat vették át a szoftverfejlesztők, és végül 1994-ben jelent meg a legendás “Design Patterns: Elements of Reusable Object-Oriented Software” könyv, amely 23 klasszikus szoftvertervezési mintát formalizált és tett közkinccsé.

A könyv szerzői – Erich Gamma, Richard Helm, Ralph Johnson és John Vlissides – a szoftverfejlesztés egyik alapművét alkották meg, és a tervezési minták azóta is a modern objektumorientált programozás alapvető eszköztárát jelentik. Ezek a minták nem bonyolult elméletek, hanem tipikus, bevált megoldások olyan problémákra, amelyekkel a fejlesztők újra és újra találkoznak. A minták lényege, hogy strukturált, közös nyelvet adnak a fejlesztők kezébe, leegyszerűsítik a kommunikációt, növelik a kód újrafelhasználhatóságát és olvashatóságát.

Fun fact: A négy szerzőre utalva gyakran használják a Gangs of Four kifejezést, és a könyvben ismertetett tervezési mintákra pedig GoF Design Pattern-ként hivatkoznak.

A tervezési minták 3 fő kategóriája

A tervezési minták világában könnyű elveszni, hiszen rengeteg különböző megoldás létezik különféle problémákra. Szerencsére a könyv szerzői rendszert vittek ebbe a sokszínűségbe: a mintákat három nagy csoportba sorolták, attól függően, hogy milyen jellegű problémára kínálnak megoldást. Nézzük meg röviden, melyek ezek a fő kategóriák, és miben különböznek egymástól!

  1. Creational Patterns: Ezek a minták az objektumok létrehozásának módját szabályozzák. Céljuk, hogy rugalmasabbá, átláthatóbbá és újrafelhasználhatóvá tegyék az objektumok példányosítását.
  2. Structural Patterns: A szerkezeti minták az objektumok és osztályok egymáshoz való viszonyát, valamint ezek összekapcsolását, kompozícióját írják le. Segítenek abban, hogy a rendszer elemei hatékonyan együttműködjenek.
  3. Behavioral Patterns: Ezek a minták az objektumok vagy osztályok közötti kommunikációt, az együttműködés és a felelősség megosztásának módját határozzák meg.

A továbbiakban a fő kategóriákon végigmenve nézünk meg néhány gyakorlati példán keresztül bemutatva pár fontosabb tervezési mintát.

Creational Patterns: Singleton

A Singleton tervezési minta az egyik legismertebb és legegyszerűbb creational minta, amelynek célja, hogy egy adott osztályból csak egyetlen példány létezzen az alkalmazás során, és ehhez az egy példányhoz globális hozzáférést biztosítson

Mikor érdemes használni?
A Singleton mintát akkor érdemes alkalmazni, ha:

  • Egy adott erőforráshoz (például adatbázis-kapcsolat, naplózó/log rendszer, konfigurációs beállítások) csak egyetlen központi példányra van szükség az egész alkalmazásban.
  • Szeretnéd elkerülni, hogy véletlenül több példány jöjjön létre, ami hibákhoz, memóriafogyasztáshoz vagy inkonzisztens állapothoz vezethet.

Hogyan működik?
A Singleton minta lényege, hogy:

  • Az osztály konstruktorát priváttá teszi, így kívülről nem lehet példányosítani.
  • Egy statikus (osztályszintű) metódust (pl. getInstance()) biztosít, amely vagy létrehozza az első példányt, vagy visszaadja a már létezőt.
  • A példányt egy privát statikus változóban tárolja, így minden kérés ugyanazt az objektumot kapja vissza.

Példa pszeudókód:

let instance: Logger | null = null;

class Logger {
  private constructor() {
    if (instance) {
      throw new Error("New instance cannot be created! Use Logger.getInstance() instead.");
    }
    instance = this;
  }

  public static getInstance(): Logger {
    if (!instance) {
      instance = new Logger();
    }
    return instance;
  }

  public log(message: string): void {
    console.log(`[LOG]: ${message}`);
  }
}

export default Logger;

Creation Patterns: Builder

A Builder pattern egy creational tervezési minta, amely lehetővé teszi összetett objektumok lépésről lépésre történő, rugalmas és átlátható felépítését. A fő célja, hogy szétválassza az objektum összeállításának folyamatát a végső reprezentációjától, így ugyanazzal az építési folyamattal többféle változatot is létrehozhatsz.

  • Mikor érdemes használni a Builder mintát?
    Ha egy objektum sok opcionális vagy konfigurálható paraméterrel rendelkezik, és a hagyományos konstruktorok túl bonyolultak vagy átláthatatlanok lennének (ún. „telescoping constructor” probléma).
  • Ha ugyanazt a bonyolult építési folyamatot többféle végtermékhez (különböző reprezentációkhoz) szeretnéd használni.
  • Ha szeretnéd elkerülni a sokféle, egymásba ágyazott (overloaded) konstruktort, és tisztább, karbantarthatóbb kódot akarsz írni.

Hogyan működik a Builder minta?

  • A Builder minta lépésről lépésre építi fel az objektumot, minden egyes lépés egy külön metódus (pl. setRoof(), addGarage()).
  • Az építési folyamatot egy ún. builder objektum végzi, amely minden lépésnél módosítja a készülő objektumot, majd a végén visszaadja azt.
  • A director (irányító) opcionális szereplő, amely meghatározza az építés lépéseinek sorrendjét, és így különböző konfigurációkat tud előállítani ugyanazzal a builderrel.
  • A végső objektum csak akkor készül el, amikor a builder befejezi az összes lépést, és meghívod a build() vagy getResult() metódust.

Ez így talán túlságosan is elméleti, ezért hoztam nektek egy kis gyakorlati példát, hogy mikor lehet érdemes a Builder mintát követni. Tegyük fel, hogy egy Car osztályunk, ami így néz ki:

class Car {
  constructor(
    public brand: string,
    public model: string,
    public year: number,
    public color?: string,
    public hasSunroof: boolean = false,
    public engineType?: string,
    public mileage?: number
  ) {}
}

// Használat:
const myCar = new Car('Toyota', 'Corolla', 2023, 'blue', true, 'hybrid', 0);

Mint látható, a constructorban számtalan paramétert kell át adni, mind különböző típusú, és van, ami opcionális. Nehéz megjegyezni egy idő után, hogy melyik paraméter után mi következik, csökkenti a kód olvashatóságát és nehezen megérthetővé teszi azt. Ráadásul bővíteni is kész rémálom lehet. Ekkor jöhet jól, ha az alábbi formára alakítjuk át a kódunkat.

// Product class
class Car {
  constructor(
    public brand: string,
    public seats: number,
    public color: string,
    public hasSunroof: boolean
  ) {}
}

// Builder class
class CarBuilder {
  private brand: string = '';
  private seats: number = 4;
  private color: string = 'white';
  private hasSunroof: boolean = false;

  setBrand(brand: string): this {
    this.brand = brand;
    return this;
  }

  setSeats(seats: number): this {
    this.seats = seats;
    return this;
  }

  setColor(color: string): this {
    this.color = color;
    return this;
  }

  setSunroof(hasSunroof: boolean): this {
    this.hasSunroof = hasSunroof;
    return this;
  }

  build(): Car {
    return new Car(this.brand, this.seats, this.color, this.hasSunroof);
  }
}

// Usage
const myCar = new CarBuilder()
  .setBrand('Toyota')
  .setSeats(2)
  .setColor('red')
  .setSunroof(true)
  .build();

console.log(myCar);

Sokkal jobban követhető, hogy mi történik az osztály létrehozásakor, és bármennyi új tulajdonsággal bővíthető, mindössze egy új private adattagot kell létrehoznunk és hozzá egy új set metódust. Mivel minden metódus az osztállyal tér vissza, ezért egymásba láncolhatók.

Creational Patterns: Factory

A Factory Pattern egy creational tervezési minta, amelynek célja, hogy az objektumok példányosítását elkülönítse attól a kódtól, amely ezeket az objektumokat használja. Ez azt jelenti, hogy ahelyett, hogy közvetlenül hívnád a konstruktorokat (new), egy úgynevezett „gyár” (factory) metódus vagy osztály felelős az objektumok létrehozásáért.

Mikor érdemes használni a Factory mintát?

  • Amikor egy alkalmazásban több, közös ősosztályból vagy interfészből származó objektumot kell létrehozni, de mindig más-más konkrét típust szeretnél példányosítani a futás során.
  • Ha a példányosítás logikája bonyolult, vagy gyakran változik (pl. különböző konfigurációk, verziók, platformok szerint).
  • Ha szeretnéd elrejteni a példányosítás részleteit, és csak egy közös interfészt vagy absztrakciót szeretnél használni a kód többi részében.

Hogyan működik a Factory minta?

  • Általában van egy közös interfész vagy absztrakt osztály (pl. Car), amelyből többféle konkrét osztály származik (pl. ElectricCar, GasCar).
  • A Factory metódus vagy osztály egy paraméter (például típusnév vagy konfiguráció) alapján eldönti, hogy melyik konkrét osztály példányát hozza létre és adja vissza.
  • A kliens kód csak a Factory metódust hívja, nem tudja és nem is érdekli, pontosan melyik osztály példánya jön létre.

Egyszerű példakód:

// Közös interfész
interface Car {
  drive(): void;
}

// Konkrét osztályok
class ElectricCar implements Car {
  drive() {
    console.log("Driving an electric car...");
  }
}

class GasCar implements Car {
  drive() {
    console.log("Driving a gas car...");
  }
}

// Factory osztály
class CarFactory {
  static createCar(type: "electric" | "gas"): Car {
    if (type === "electric") {
      return new ElectricCar();
    } else if (type === "gas") {
      return new GasCar();
    }
    throw new Error("Unknown car type");
  }
}

// Használat
const myCar = CarFactory.createCar("electric");
myCar.drive(); // "Driving an electric car..."

Előnyök

  • Laza csatolás: A kliens kód nem függ a konkrét osztályoktól, csak az interfésztől.
  • Egyszerűbb karbantartás: Új típusok hozzáadása könnyű, nem kell a kliens kódot módosítani.
  • Központosított példányosítás: A példányosítás logikája egy helyen kezelhető.

Hátrányok

  • Több osztály és kód: Egyszerű esetekben túlzás lehet a Factory minta használata.
  • Bonyolultabb kód: Ha csak egy-két típusod van, felesleges lehet a plusz réteg.

Structural Patterns: Facade

A Facade egy szerkezeti tervezési minta, amelynek célja, hogy egy bonyolult rendszerhez vagy alrendszerhez egyetlen, egyszerű és átlátható felületet biztosítson. Ezáltal a kliensnek nem kell a rendszer összes részletével, osztályával, metódusával foglalkoznia: elég, ha a Facade-on keresztül kommunikál, ami a háttérben elvégzi a szükséges műveleteket.

Mikor érdemes használni a Facade mintát?

  • Ha egy rendszer vagy könyvtár túl összetett, sok egymással összefüggő osztállyal, és csak egy részét szeretnéd használni.
  • Ha szeretnéd csökkenteni a kliens és a rendszer közötti szoros kapcsolódást (loose coupling).
  • Ha egyszerűsíteni akarod a kódod olvashatóságát, karbantarthatóságát, vagy el akarod rejteni a rendszer belső működését.

Hogyan működik?

  • A Facade egyetlen, magasabb szintű osztályt vagy interfészt biztosít, amely a háttérben több alrendszert vagy objektumot kezel.
  • A kliens csak a Facade-ot használja, nem kell ismernie az alrendszer részleteit, például hogy milyen sorrendben kell metódusokat hívni, vagy hogyan kell inicializálni az objektumokat.
  • Az alrendszer osztályai nem tudnak a Facade létezéséről, csak a Facade delegálja feléjük a kéréseket.

Egyszerű TypeScript példa

Képzeljünk el egy okosotthon rendszert, ahol több alrendszert kellene külön-külön vezérelni:

// Alrendszerek
class LightSystem {
  turnOn() { console.log("Lights turned on"); }
  turnOff() { console.log("Lights turned off"); }
}

class SecuritySystem {
  arm() { console.log("Security system armed"); }
  disarm() { console.log("Security system disarmed"); }
}

// Facade
class HomeAutomationFacade {
  constructor(
    private light: LightSystem,
    private security: SecuritySystem
  ) {}

  goodMorning() {
    this.light.turnOn();
    this.security.disarm();
  }

  goodNight() {
    this.light.turnOff();
    this.security.arm();
  }
}

// Használat
const facade = new HomeAutomationFacade(new LightSystem(), new SecuritySystem());
facade.goodMorning(); // "Lights turned on" + "Security system disarmed"
facade.goodNight();   // "Lights turned off" + "Security system armed"

Ebben a példában a felhasználónak nem kell tudnia, hogyan kell külön vezérelni a világítást és a riasztót – elég a Facade-ot hívni.

Előnyök

  • Egyszerűbb, átláthatóbb felület a kliens számára.
  • Csökkenti a kód szoros kapcsolódását az alrendszerhez.
  • Könnyebb karbantartás: ha az alrendszer változik, elég csak a Facade-ot módosítani.

Hátrány

Ha túl sok, egymástól független funkciót zsúfolsz egy Facade-ba, az is bonyolulttá válhat, és könnyen ún. God-object lehet belőle. Ez egy olyan osztály, amely túl sok felelősséget hordoz magában, és szorosan kapcsolódik a rendszer számos részéhez, megsértve az egyetlen felelősség elvét (Single Responsibility Principle).

Structural Patterns: Adapter

Az Adapter Pattern egy szerkezeti tervezési minta, amely lehetővé teszi, hogy két, eredetileg nem kompatibilis interfészt használó komponens együtt tudjon működni anélkül, hogy bármelyikük forráskódját módosítani kellene. Ez a minta gyakran „wrapper” néven is ismert, mert egy köztes, „adapter” osztály veszi át a fordítást az egyik interfész és a másik között.

Mikor érdemes használni az Adapter mintát?

  • Ha egy meglévő (pl. régi vagy harmadik féltől származó) osztályt vagy könyvtárat szeretnél integrálni, de annak interfésze nem egyezik az általad elvárt vagy használt interfésszel.
  • Ha újrafelhasználnád egy komponens funkcionalitását, de az nem illeszkedik a rendszeredbe az eltérő metódusnevek vagy paraméterek miatt.
  • Ha szeretnéd elrejteni a rendszeredben használt különböző interfészeket, és egységes felületet biztosítani a kliensnek.

Hogyan működik az Adapter minta?

  • Target interface: Az az interfész, amit a kliens elvár és használni szeretne.
  • Adaptee: Az a meglévő osztály, amelynek az interfésze nem kompatibilis a klienssel, de a szükséges funkcionalitást tartalmazza.
  • Adapter: Egy köztes osztály, amely implementálja a Target interfészt, és belül az Adaptee példányát használja, hogy a hívásokat lefordítsa az elvárt formára.

Egyszerű TypeScript példa

Tegyük fel, hogy van egy régi osztályod, amelynek metódusa nem egyezik az új rendszered elvárásaival:

class WeatherAdapter implements WeatherApp {
  private weatherAPI: ThirdPartyWeatherAPI;

  constructor(weatherAPI: ThirdPartyWeatherAPI) {
    this.weatherAPI = weatherAPI;
  }

  // Átalakítja a Celsius-t Fahrenheit-re
  getTempInFahrenheit(): number {
    const tempC = this.weatherAPI.getTempC();
    return (tempC * 9/5) + 32; // °C → °F
  }

  // Közvetlenül átadja a páratartalmat (nincs átalakítás)
  getHumidity(): number {
    return this.weatherAPI.getHumidity();
  }
}

Behavioral Patterns: Strategy

A Strategy Pattern egy viselkedési tervezési minta, amely lehetővé teszi, hogy egy algoritmuscsaládot definiálj, mindegyiket külön osztályba helyezd, és futásidőben választhasd ki, melyiket szeretnéd használni. A fő célja, hogy az algoritmusokat egymástól függetlenül tartsd, és a kódod rugalmasan, egyszerűen bővíthető legyen új viselkedésekkel anélkül, hogy a meglévő logikát módosítanod kellene.

Mikor érdemes használni a Strategy mintát?

  • Ha egy objektum többféle módon tud végrehajtani egy feladatot (pl. különböző rendezési, fizetési vagy kedvezményszámítási algoritmusok), és ezeket a viselkedéseket futásidőben szeretnéd váltogatni.
  • Ha el akarod kerülni a hatalmas if vagy switch szerkezeteket, amelyek különböző algoritmusokat választanak ki.
  • Ha szeretnéd, hogy a kódod könnyen bővíthető legyen új algoritmusokkal anélkül, hogy a meglévő kódot módosítanod kellene (Open/Closed Principle).

Hogyan működik?

  • Strategy interfész: Meghatározza az algoritmusok közös metódusait.
  • Konkrét Strategy-k: Minden algoritmust külön osztály valósít meg, amely implementálja a Strategy interfészt.
  • Context: Az az objektum, amelyik használja a Strategy-t, és futásidőben kapja meg, hogy melyik algoritmust alkalmazza. A Context nem tudja, pontosan melyik algoritmust használja, csak azt, hogy az megfelel a Strategy interfésznek.

Egyszerű példa

Képzeld el, hogy egy webshopban különböző kedvezményszámítási stratégiákat szeretnél alkalmazni:

  • RegularDiscountStrategy: 5% kedvezmény
  • PremiumDiscountStrategy: 10% kedvezmény
  • PromotionDiscountStrategy: 20% kedvezmény

A Strategy mintával mindegyik kedvezményt külön osztályba teszed, és a vásárló típusától függően futásidőben választod ki, melyiket alkalmazod.

// Strategy interfész
interface DiscountStrategy {
  getDiscount(price: number): number;
}

// Konkrét stratégiák
class RegularDiscount implements DiscountStrategy {
  getDiscount(price: number): number {
    return price * 0.95;
  }
}

class PremiumDiscount implements DiscountStrategy {
  getDiscount(price: number): number {
    return price * 0.90;
  }
}

class PromotionDiscount implements DiscountStrategy {
  getDiscount(price: number): number {
    return price * 0.80;
  }
}

// Context
class ShoppingCart {
  constructor(private discountStrategy: DiscountStrategy) {}

  calculateTotal(price: number): number {
    return this.discountStrategy.getDiscount(price);
  }
}

// Használat
const cart = new ShoppingCart(new PremiumDiscount());
console.log(cart.calculateTotal(100)); // 90

Behavioral Patterns: Observer

A Observer egy viselkedési tervezési minta, amely lehetővé teszi, hogy egy objektum (a Subject) értesítse a hozzá tartozó függő objektumokat (Observers) minden állapotváltozásról. Ez a minta alapvető a reakcióprogramozásban, mivel laza kapcsolatot biztosít a komponensek között, miközben szinkronban tartja az állapotokat.

Mikor használjuk?

  • Ha több objektumnak reagálnia kell egy közös eseményre (pl. adatváltozás, felhasználói interakció).
  • Ha dinamikusan szeretnéd kezelni a függőségeket: az Observerek futásidőben regisztrálhatók és leiratkozhatnak.
  • Ha el akarod kerülni a szoros kapcsolódást a komponensek között (pl. a View és a Model MVC architektúrában).

Hogyan működik?

  • Subject: Az az objektum, amelynek állapotváltozásaiért felelős. Tartalmazza az Observer-ek listáját és kezeli a regisztrációt/leiratkozást.
  • Observer: Egy interfész, amely definiálja az update() metódust. Minden konkrét Observer implementálja ezt a metódust, hogy reagáljon a Subject változásaira.

Egy egyszerű példa

Képzeld el egy időjárásállomást (Subject), amely méri a hőmérsékletet. A mobilalkalmazás és a digitális kijelző (Observers) feliratkozik az állomásra. Amikor a hőmérséklet változik, az állomás értesíti mindkét megfigyelőt, akik frissítik a megjelenítést. Így a megfigyelők nem kérdezgetik folyamatosan az állomást – csak a változás pillanatában kapnak információt.

// Observer interfész
interface Observer {
  update(temp: number): void;
}

// Subject interfész
interface WeatherStation {
  attach(observer: Observer): void;
  detach(observer: Observer): void;
  notify(): void;
}

// Konkrét Subject: Időjárásállomás
class ConcreteWeatherStation implements WeatherStation {
  private observers: Observer[] = [];
  private temperature: number = 0;

  attach(observer: Observer): void {
    if (!this.observers.includes(observer)) {
      this.observers.push(observer);
    }
  }

  detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(): void {
    for (const observer of this.observers) {
      observer.update(this.temperature);
    }
  }

  setTemperature(temp: number): void {
    this.temperature = temp;
    this.notify();
  }
}

// Konkrét Observerek
class TemperatureDisplay implements Observer {
  update(temp: number): void {
    console.log(`Kijelző frissítve: ${temp}°C`);
  }
}

class MobileApp implements Observer {
  update(temp: number): void {
    console.log(`Push értesítés: ${temp}°C`);
  }
}

// Használat
const station = new ConcreteWeatherStation();
const display = new TemperatureDisplay();
const app = new MobileApp();

station.attach(display);
station.attach(app);

station.setTemperature(25); 
// Kimenet:
// "Kijelző frissítve: 25°C"
// "Push értesítés: 25°C"

station.detach(app);
station.setTemperature(20); // Csak a kijelző frissül

Előnyök:

  • Laza kapcsolódás: A Subject és az Observerek nem ismerik egymás belső működését.
  • Dinamikus feliratkozás: Observerek futásidőben adhatók hozzá vagy távolíthatók el.
  • Központosított vezérlés: Az állapotváltozások egy helyről kezelhetők.

Hátrányok:

  • Túl sok Observer lassíthatja a rendszert.
  • Egy Observer véletlenül módosíthatja a Subject állapotát.

A Observer minta nélkülözhetetlen olyan helyzetekben, ahol egy objektum állapotváltozásait számos más objektumnak kell követnie. Különösen értékes reakcióprogramozásban és eseményvezérelt rendszerekben, ahol a rugalmasság és a karbantarthatóság kulcsfontosságú.

Végszó

Ahogy láthattuk, a szoftvertervezési minták nem elvont elméletek, hanem kipróbált, újrahasznosítható megoldások a fejlesztés során gyakran visszatérő problémákra. Legyen szó objektumok létrehozásáról, rendszerek egyszerűsítéséről vagy a viselkedések rugalmas kezeléséről, a megfelelő minta alkalmazása jelentősen javíthatja a kódod olvashatóságát, karbantarthatóságát és bővíthetőségét.
A bemutatott példák – Singleton, Builder, Factory, Facade, Adapter, Strategy és Observer – mind azt mutatják, hogy a minták segítenek strukturáltan gondolkodni, csökkentik a hibalehetőségeket és megkönnyítik a csapatmunkát is.

Ha tudatosan választasz és alkalmazol tervezési mintákat, nemcsak a saját munkádat teszed könnyebbé, hanem a jövőbeli fejlesztők számára is átláthatóbbá, érthetőbbé és stabilabbá válik a projekted.
Érdemes tehát kísérletezni, tanulni, és a mintákat a saját igényeidhez igazítani – így minden fejlesztői tarsolyban ott lehet egy-egy jól bevált megoldás a leggyakoribb kihívásokra.

Neked melyek a kedvenc tervezési mintáid?

Források, olvasnivalók a témában:

Refactoring.guru – Design Patterns

ForrestKnight – 7 Design Patterns EVERY Developer Should Know

Hogy tetszett a poszt?

Van bármi észrevételed?


Ha tetszett, oszd meg ismerőseiddel!

Szólj hozzá!

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük