Το Ιστολόγιό Μου

Vanilla JS patterns που αξίζουν (χωρίς frameworks)

Vanilla JavaScript
Εικόνα: AI-generated

Στο σύγχρονο οικοσύστημα του web development, η πρώτη αντίδραση για την κατασκευή οποιασδήποτε web εφαρμογής είναι η εγκατάσταση ενός βαρύ framework (React, Vue, Angular). Ωστόσο, η υπερβολική εξάρτηση από αυτά κρύβει αρχιτεκτονικές παγίδες: διογκωμένα bundle sizes, vendor lock-in, και ένα συνεχώς μεταβαλλόμενο API. Η κατανόηση και η εφαρμογή των παραδοσιακών Design Patterns (Σχεδιαστικά Μοτίβα) σε καθαρή Vanilla JavaScript προσφέρει απόλυτο έλεγχο, μέγιστο performance και κώδικα που παραμένει αθάνατος στο χρόνο.

Ας αναλύσουμε σε βάθος τα 5 κορυφαία αρχιτεκτονικά patterns της JavaScript, εξετάζοντας ταυτόχρονα το memory footprint και την πρακτική τους εφαρμογή στην παραγωγή[cite: 3].

1. Modern Module Pattern (ES Modules)

Το Πρόβλημα: Στις παλιές μέρες της JavaScript, κάθε μεταβλητή ή συνάρτηση που δηλωνόταν σε ένα αρχείο κατέληγε στο παγκόσμιο πεδίο ορισμού (Global Scope / window object). Αυτό οδηγούσε σε "name collisions" (συγκρούσεις ονομάτων) και spaghetti κώδικα που ήταν αδύνατο να γίνει debug.

Η Λύση: Παλαιότερα χρησιμοποιούσαμε IIFEs (Immediately Invoked Function Expressions). Το 2026 όμως, η JavaScript διαθέτει native υποστήριξη για ES Modules (ESM) μέσω των keywords import και export[cite: 3]. Κάθε αρχείο JS είναι εξ ορισμού ένα απομονωμένο module, προστατεύοντας το global namespace.

stateManager.js (Module)
// Ιδιωτικό state (δεν είναι προσβάσιμο έξω από το αρχείο)
let _state = { theme: 'dark', user: null };

// Δημόσιες συναρτήσεις που εξάγονται ελέγχοντας τη μεταβολή του state
export function updateTheme(newTheme) {
    if (['dark', 'light'].includes(newTheme)) {
        _state.theme = newTheme;
        document.body.className = `${newTheme}-mode`;
    }
}

export function getTheme() {
    return _state.theme;
}

// Εξαγωγή ενός read-only snapshot του state
export const appState = {
    get current() { return { ..._state }; }
};
app.js (Εισαγωγή και Χρήση)
import { updateTheme, getTheme, appState } from './stateManager.js';

updateTheme('light');
console.log(getTheme()); // "light"
console.log(appState.current); // {_state snapshot}

Memory & Performance Insight: Τα ES Modules εκτελούνται σε αυστηρό mode ("use strict") εξ ορισμού. Το V8 Engine (Chrome/Node) κάνει compile και evaluate τα modules μόνο μία φορά (Singleton-like behavior κατά το φόρτωμα). Επιπλέον, επιτρέπουν στους modern bundlers (Vite, Webpack) να εκτελέσουν Tree Shaking – δηλαδή να αφαιρέσουν εντελώς από το τελικό αρχείο παραγωγής τον κώδικα που δεν έγινε ποτέ `import`.

2. Advanced Observer Pattern

Το Πρόβλημα: Όταν ένα component της εφαρμογής αλλάζει (π.χ. ο χρήστης προσθέτει ένα προϊόν στο καλάθι), πρέπει να ενημερωθούν ταυτόχρονα πολλά άλλα ανεξάρτητα σημεία του UI (το counter στο header, η λίστα της σελίδας, το total price box). Η απευθείας σύνδεση αυτών των components δημιουργεί σφιχτά δεμένο κώδικα (tight coupling).

Η Λύση: Το Observer Pattern δημιουργεί μια σχέση ένα-προς-πολλά (one-to-many)[cite: 3]. Το κεντρικό αντικείμενο (Subject/Observable) διατηρεί μια λίστα από "παρατηρητές" (Observers) και τους ειδοποιεί αυτόματα όταν αλλάζει η κατάστασή του[cite: 3].

Observer Implementation
class StoreObservable {
    constructor() {
        this.observers = []; // Λίστα με callback functions
    }

    // Εγγραφή νέου παρατηρητή
    subscribe(fn) {
        this.observers.push(fn);
        // Επιστροφή συνάρτησης unsubscribe για εύκολο clean-up
        return () => {
            this.observers = this.observers.filter(subscriber => subscriber !== fn);
        };
    }

    // Ειδοποίηση όλων των παρατηρητών με νέα δεδομένα
    notify(data) {
        this.observers.forEach(fn => fn(data));
    }
}

// Χρήση στην πράξη
const cartStore = new StoreObservable();

// Component 1: Header Counter
const unsubscribeHeader = cartStore.subscribe(items => {
    console.log(`UI Header updated: Counter is now ${items.length}`);
});

// Component 2: Analytics Tracking
cartStore.subscribe(items => {
    console.log(`Analytics Log: Items count changed to ${items.length}`);
});

// Προσομοίωση προσθήκης προϊόντος
cartStore.notify(['HTML5 Book', 'CSS3 Complete Guide']);

Memory & Performance Insight: Το Observer pattern είναι εξαιρετικά αποδοτικό (O(n) πολυπλοκότητα κατά το notify). Ωστόσο, κρύβει τον κίνδυνο των Memory Leaks (Διαρροές Μνήμης), γνωστών ως "Lapsed Listener Problem". Αν ένα DOM element διαγραφεί αλλά δεν καλέσει την `unsubscribe()` μέθοδο, η συνάρτησή του παραμένει δεσμευμένη στον πίνακα `observers`, εμποδίζοντας τον Garbage Collector να ελευθερώσει τη μνήμη.

3. Factory Functions με Closures

Το Πρόβλημα: Οι κλάσεις (Classes) της ES6 εισήγαγαν το αντικειμενοστραφές στυλ στην JS, αλλά κουβαλούν το μεγάλο πρόβλημα του dynamic binding του keyword this. Αν περάσετε μια μέθοδο κλάσης ως event listener, το this χάνει το context του εκτός αν κάνετε χειροκίνητα `.bind(this)` παντού. Επίσης, οι ιδιότητες των κλάσεων δεν είναι πραγματικά ιδιωτικές (μέχρι πρόσφατα με το `#`), επιτρέποντας εξωτερικές παραποιήσεις.

Η Λύση: Οι Factory Functions είναι απλές συναρτήσεις που επιστρέφουν ένα αντικείμενο χωρίς τη χρήση του new[cite: 3]. Αξιοποιούν τα Closures για να δημιουργήσουν 100% ασφαλείς, ιδιωτικές μεταβλητές, αποφεύγοντας πλήρως το this[cite: 3].

Factory Function Guide
function createSecureWallet(ownerName, initialBalance) {
    // Ιδιωτικές μεταβλητές εντός του lexical scope της συνάρτησης
    let _balance = initialBalance;
    const _transactions = [];

    return {
        getOwner: () => ownerName,
        getBalance: () => _balance,
        deposit: (amount) => {
            if (amount > 0) {
                _balance += amount;
                _transactions.push({ type: 'DEPOSIT', amount, date: new Date() });
                return true;
            }
            return false;
        },
        getHistory: () => [..._transactions] // Επιστροφή κλώνου για αποτροπή mutation
    };
}

// Δημιουργία instances
const wallet = createSecureWallet('Sotirios', 500);
wallet.deposit(150);
console.log(wallet.getBalance()); // 650
console.log(wallet._balance); // undefined (απόλυτα προστατευμένο)

Memory & Performance Insight: Υπάρχει ένα trade-off εδώ. Οι κλάσεις (Classes) μοιράζονται τις μεθόδους τους μέσω του Prototype Chain, πράγμα που σημαίνει ότι όσα instances και να φτιάξετε, οι μέθοδοι υπάρχουν στη μνήμη μόνο μία φορά. Οι Factory Functions, αντίθετα, δημιουργούν νέες εκδοχές των συναρτήσεων (methods) για κάθε instance λόγω του Closure. Αν σκοπεύετε να δημιουργήσετε 10.000 instances, οι κλάσεις είναι πιο οικονομικές στη μνήμη. Για αρχιτεκτονικά components όμως (π.χ. services, APIs), οι Factory Functions προσφέρουν ασύγκριτη ασφάλεια.

4. Thread-Safe Singleton Pattern

Το Πρόβλημα: Ορισμένα αντικείμενα σε μια εφαρμογή πρέπει να είναι μοναδικά. Για παράδειγμα, η σύνδεση με μια βάση δεδομένων (IndexedDB), η διαχείριση των configuration flags της εφαρμογής ή το caching layer. Αν δημιουργηθούν πολλαπλά instances, θα έχουμε ασυνέπεια δεδομένων (state desynchronization).

Η Λύση: Το Singleton Pattern διασφαλίζει ότι μια κλάση ή ένα αντικείμενο θα δημιουργηθεί ακριβώς μία φορά σε ολόκληρο τον κύκλο ζωής της εφαρμογής, παρέχοντας ένα global σημείο πρόσβασης[cite: 3].

Singleton Implementation (Modern JS)
class DatabaseConnection {
    constructor() {
        if (DatabaseConnection.instance) {
            return DatabaseConnection.instance; // Επιστρέφει το ήδη υπάρχον instance
        }
        
        this.connectionId = Math.random().toString(36).substr(2, 9);
        this.createdAt = new Date();
        
        // Αποθήκευση του instance στη στατική ιδιότητα της κλάσης
        DatabaseConnection.instance = this;
        Object.freeze(this); // Απαγόρευση τροποποίησης του instance
    }

    query(sql) {
        console.log(`Executing on conn [${this.connectionId}]: ${sql}`);
    }
}

// Έλεγχος Μοναδικότητας
const conn1 = new DatabaseConnection();
const conn2 = new DatabaseConnection();

console.log(conn1 === conn2); // true (Είναι το ίδιο ακριβώς αντικείμενο στη μνήμη)
conn2.query('SELECT * FROM users');

Memory & Performance Insight: Το Singleton είναι εξαιρετικό για τη διατήρηση ενός "single source of truth". Χρησιμοποιώντας το Object.freeze(this), προστατεύουμε το instance από runtime μεταλλάξεις (mutations). Στο memory heap, το αντικείμενο αυτό δεν πρόκειται ποτέ να περάσει από Garbage Collection μέχρι να κλείσει η καρτέλα του browser, οπότε πρέπει να χρησιμοποιείται με σύνεση.

5. decoupled Pub/Sub (Publish-Subscribe) Pattern

Το Πρόβλημα: Το Observer pattern απαιτεί από τους παρατηρητές να γνωρίζουν το Subject (να έχουν απευθείας αναφορά στο `cartStore` object). Σε μεγάλα components, αυτό δημιουργεί πάλι ένα επίπεδο εξάρτησης. Τι γίνεται αν θέλουμε δύο components να επικοινωνούν χωρίς να γνωρίζει καθόλου το ένα την ύπαρξη του άλλου;

Η Λύση: Το Pub/Sub Pattern εισάγει έναν τρίτο ενδιάμεσο, έναν Event Broker (ή Bus)[cite: 3]. Ο Publisher απλά εκπέμπει ένα event με ένα string-name, και ο Subscriber ακούει αυτό το string-name μέσω του broker. Δεν υπάρχει καμία άμεση επαφή μεταξύ τους[cite: 3].

Global Event Bus (Pub/Sub)
const EventBus = {
    _events: {},

    // Εγγραφή σε event (Subscribe)
    on(event, callback) {
        if (!this._events[event]) {
            this._events[event] = [];
        }
        this._events[event].push(callback);
    },

    // Ακύρωση εγγραφής (Unsubscribe)
    off(event, callback) {
        if (!this._events[event]) return;
        this._events[event] = this._events[event].filter(cb => cb !== callback);
    },

    // Εκπομπή event (Publish)
    emit(event, data) {
        if (!this._events[event]) return;
        this._events[event].forEach(callback => callback(data));
    }
};

// Χρήση σε απομακρυσμένα components
// Component A (Ακούει για ειδοποιήσεις συστήματος)
function notificationHandler(msg) {
    console.log(`[Notification Modal]: ${msg}`);
}
EventBus.on('SHOW_ALERT', notificationHandler);

// Component B (Ένας απομονωμένος κώδικας στο backend ή σε άλλο section)
// Εκπέμπει το event χωρίς να ξέρει ποιος ή αν κάποιος ακούει
EventBus.emit('SHOW_ALERT', 'Η συνεδρία σας λήγει σε 5 λεπτά.');

// Καθαρισμός μνήμης όταν το Component A γίνει destroy
EventBus.off('SHOW_ALERT', notificationHandler);

Memory & Performance Insight: Το Pub/Sub επιτρέπει το απόλυτο decoupling (αποσύνδεση) των components[cite: 3]. Αποτελεί τη ραχοκοκαλιά των micro-frontends αρχιτεκτονικών. Από άποψη performance, η εκπομπή ενός event απαιτεί απλό iteration σε έναν πίνακα συναρτήσεων, καθιστώντας το αστραπιαίο (sub-millisecond execution).

Σύνοψη και Αρχιτεκτονική Επιλογή

Η συγγραφή Vanilla JavaScript κώδικα δεν σημαίνει ότι ανακαλύπτουμε τον τροχό από την αρχή[cite: 3]. Σημαίνει ότι χτίζουμε εφαρμογές με ελάχιστο overhead, γλιτώνοντας τους χρήστες μας από megabytes άχρηστου framework κώδικα. Χρησιμοποιήστε τον παρακάτω συγκριτικό οδηγό για τις επιλογές σας:

Design Pattern Κύριος Στόχος Ιδανικό Use Case
Module Pattern[cite: 3] Απομόνωση κώδικα και Scope encapsulation.[cite: 3] Δομή αρχείων, βιβλιοθήκες, reusable utilities.
Observer Pattern[cite: 3] Συγχρονισμός UI components με ένα κεντρικό State.[cite: 3] Τοπικά stores, φόρμες, καλάθια αγορών.
Factory Functions[cite: 3] Ασφάλεια δεδομένων (Private variables) χωρίς το this.[cite: 3] API Clients, δημιουργία stateful αντικειμένων, security modules.
Singleton Pattern[cite: 3] Εγγύηση ενός και μοναδικού instance στη μνήμη.[cite: 3] Database Drivers, Global Configuration, Global Cache layers.
Pub/Sub Pattern[cite: 3] Πλήρης αποσύνδεση (Decoupling) ανεξάρτητων components.[cite: 3] Επικοινωνία cross-page, logging systems, global notification banners.

Μην φοβάστε να γράψετε κώδικα χωρίς frameworks![cite: 3] Η Vanilla JS εξελίσσεται ραγδαία και οι δυνατότητες που προσφέρει natively το Runtime Environment (Browsers/V8) επαρκούν για το 90% των enterprise εφαρμογών.

← Επιστροφή στο Blog