Detection de navigateur et Design Patterns en JavaScript : Singleton et Factory

Maitrisez la detection de navigateur et les patterns creatifs en JavaScript : Singleton, Factory et methodes de feature detection.

Mahmoud DEVO
Mahmoud DEVO
December 27, 2025 12 min read
Detection de navigateur et Design Patterns en JavaScript : Singleton et Factory

Détection de navigateur et Design Patterns créatifs en JavaScript

Dans le monde du développement web moderne, deux défis majeurs se présentent constamment aux développeurs JavaScript : assurer la compatibilité cross-browser et structurer le code de manière maintenable et évolutive. Ces deux problématiques, bien que distinctes, partagent un objectif commun : créer des applications robustes et professionnelles.

La détection de navigateur a considérablement évolué depuis les débuts du web. À l’époque des “guerres des navigateurs”, les développeurs devaient jongler avec des implémentations radicalement différentes de JavaScript. Aujourd’hui, bien que les navigateurs modernes soient beaucoup plus standardisés, des différences subtiles persistent, notamment pour les nouvelles APIs et fonctionnalités expérimentales.

Les design patterns créatifs, quant à eux, constituent le fondement d’une architecture logicielle solide. Ces patterns, issus du célèbre livre “Gang of Four”, ont été adaptés au paradigme JavaScript et sont devenus essentiels pour tout développeur souhaitant écrire du code professionnel. Ils permettent de résoudre des problèmes récurrents de création d’objets tout en favorisant la réutilisabilité et la testabilité du code.

Dans cet article approfondi, nous explorerons les techniques modernes de détection de navigateur, puis nous plongerons dans les patterns créatifs les plus utilisés : Singleton, Factory, Builder et Prototype. Chaque concept sera illustré par des exemples pratiques en JavaScript pur, directement applicables dans vos projets.


Détection de navigateur : approches et bonnes pratiques

La détection de navigateur est essentielle pour garantir que votre application offre une expérience optimale sur toutes les plateformes. Cependant, toutes les approches ne se valent pas. Examinons les différentes méthodes disponibles, de la plus recommandée à la moins conseillée.

Feature Detection : la méthode recommandée

La feature detection (détection de fonctionnalités) est la méthode privilégiée par la communauté JavaScript moderne. Plutôt que de demander “Quel navigateur utilises-tu ?”, on demande “Supportes-tu cette fonctionnalité ?”. Cette approche est plus fiable et future-proof.

Détection native en JavaScript

// Vérifier le support de localStorage
function supportsLocalStorage() {
    try {
        var storage = window.localStorage;
        var testKey = '__storage_test__';
        storage.setItem(testKey, testKey);
        storage.removeItem(testKey);
        return true;
    } catch (e) {
        return false;
    }
}

// Vérifier le support de Fetch API
function supportsFetch() {
    return 'fetch' in window;
}

// Vérifier le support des Promises
function supportsPromises() {
    return typeof Promise !== 'undefined' &&
           Promise.toString().indexOf('[native code]') !== -1;
}

// Vérifier le support de Service Workers
function supportsServiceWorkers() {
    return 'serviceWorker' in navigator;
}

// Vérifier le support de WebGL
function supportsWebGL() {
    try {
        var canvas = document.createElement('canvas');
        return !!(
            window.WebGLRenderingContext &&
            (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
        );
    } catch (e) {
        return false;
    }
}

// Utilisation
if (supportsLocalStorage()) {
    localStorage.setItem('user', JSON.stringify({ name: 'John' }));
} else {
    // Fallback avec cookies ou autre mécanisme
    console.log('localStorage non supporté, utilisation du fallback');
}

Utilisation de Modernizr

Modernizr est une bibliothèque populaire qui automatise la détection de fonctionnalités et ajoute des classes CSS au document.

// Installation: npm install modernizr
// Ou inclusion via CDN

// Détection de multiples fonctionnalités
if (Modernizr.flexbox) {
    console.log('Flexbox est supporté');
}

if (Modernizr.webgl) {
    initWebGLApplication();
} else {
    initCanvasFallback();
}

// Chargement conditionnel de polyfills
Modernizr.on('fetch', function(result) {
    if (!result) {
        // Charger le polyfill fetch
        var script = document.createElement('script');
        script.src = 'https://cdn.polyfill.io/v3/polyfill.min.js?features=fetch';
        document.head.appendChild(script);
    }
});

// Exemple de configuration personnalisée Modernizr
// modernizr-config.json
var modernizrConfig = {
    "minify": true,
    "options": ["setClasses"],
    "feature-detects": [
        "css/flexbox",
        "css/grid",
        "storage/localstorage",
        "serviceworker"
    ]
};

CSS @supports pour la détection côté styles

La règle CSS @supports permet de détecter les fonctionnalités directement dans les feuilles de style.

/* Détection de Grid Layout */
@supports (display: grid) {
    .container {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
        gap: 1rem;
    }
}

/* Fallback pour navigateurs sans Grid */
@supports not (display: grid) {
    .container {
        display: flex;
        flex-wrap: wrap;
    }
    .container > * {
        flex: 1 1 250px;
        margin: 0.5rem;
    }
}

/* Détection de backdrop-filter */
@supports (backdrop-filter: blur(10px)) {
    .modal-overlay {
        backdrop-filter: blur(10px);
        background-color: rgba(0, 0, 0, 0.5);
    }
}

/* Détection de position: sticky */
@supports (position: sticky) {
    .header {
        position: sticky;
        top: 0;
    }
}
// Équivalent JavaScript de @supports
function cssSupports(property, value) {
    if ('CSS' in window && 'supports' in window.CSS) {
        return window.CSS.supports(property, value);
    }
    // Fallback pour anciens navigateurs
    var element = document.createElement('div');
    element.style[property] = value;
    return element.style[property] === value;
}

// Utilisation
if (cssSupports('display', 'grid')) {
    document.body.classList.add('grid-supported');
}

if (cssSupports('backdrop-filter', 'blur(10px)')) {
    document.body.classList.add('backdrop-supported');
}

Tableau des APIs modernes de détection

FonctionnalitéMéthode de détectionSupport navigateur
Fetch API'fetch' in windowChrome 42+, Firefox 39+, Safari 10.1+
Service Workers'serviceWorker' in navigatorChrome 40+, Firefox 44+, Safari 11.1+
Web Components'customElements' in windowChrome 54+, Firefox 63+, Safari 10.1+
Intersection Observer'IntersectionObserver' in windowChrome 51+, Firefox 55+, Safari 12.1+
ResizeObserver'ResizeObserver' in windowChrome 64+, Firefox 69+, Safari 13.1+
WebAssemblytypeof WebAssembly === 'object'Chrome 57+, Firefox 52+, Safari 11+
CSS GridCSS.supports('display', 'grid')Chrome 57+, Firefox 52+, Safari 10.1+
ES6 Modules'noModule' in document.createElement('script')Chrome 61+, Firefox 60+, Safari 11+

User Agent Parsing : quand et comment l’utiliser

Bien que déconseillé comme méthode principale, le parsing du User Agent reste parfois nécessaire pour des cas spécifiques comme l’analytics ou les workarounds de bugs connus.

// Parser de User Agent complet
var BrowserDetector = (function() {
    var ua = navigator.userAgent;
    var browserInfo = {
        name: 'Unknown',
        version: 'Unknown',
        os: 'Unknown',
        mobile: false
    };

    // Détection du système d'exploitation
    function detectOS() {
        if (ua.indexOf('Windows') !== -1) return 'Windows';
        if (ua.indexOf('Mac') !== -1) return 'macOS';
        if (ua.indexOf('Linux') !== -1) return 'Linux';
        if (ua.indexOf('Android') !== -1) return 'Android';
        if (ua.indexOf('iOS') !== -1 || ua.indexOf('iPhone') !== -1 || ua.indexOf('iPad') !== -1) return 'iOS';
        return 'Unknown';
    }

    // Détection du navigateur et version
    function detectBrowser() {
        var match;

        // Edge (Chromium)
        if ((match = ua.match(/Edg\/(\d+)/))) {
            return { name: 'Edge', version: match[1] };
        }

        // Chrome
        if ((match = ua.match(/Chrome\/(\d+)/) ) && ua.indexOf('Edg') === -1) {
            return { name: 'Chrome', version: match[1] };
        }

        // Firefox
        if ((match = ua.match(/Firefox\/(\d+)/))) {
            return { name: 'Firefox', version: match[1] };
        }

        // Safari
        if ((match = ua.match(/Version\/(\d+).*Safari/))) {
            return { name: 'Safari', version: match[1] };
        }

        // Opera
        if ((match = ua.match(/OPR\/(\d+)/))) {
            return { name: 'Opera', version: match[1] };
        }

        // Internet Explorer
        if ((match = ua.match(/MSIE\s(\d+)/)) || (match = ua.match(/Trident.*rv:(\d+)/))) {
            return { name: 'IE', version: match[1] };
        }

        return { name: 'Unknown', version: 'Unknown' };
    }

    // Détection mobile
    function isMobile() {
        return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
    }

    var browser = detectBrowser();
    browserInfo.name = browser.name;
    browserInfo.version = browser.version;
    browserInfo.os = detectOS();
    browserInfo.mobile = isMobile();

    return {
        info: browserInfo,
        isChrome: function() { return browserInfo.name === 'Chrome'; },
        isFirefox: function() { return browserInfo.name === 'Firefox'; },
        isSafari: function() { return browserInfo.name === 'Safari'; },
        isEdge: function() { return browserInfo.name === 'Edge'; },
        isIE: function() { return browserInfo.name === 'IE'; },
        isMobile: function() { return browserInfo.mobile; },
        versionAtLeast: function(minVersion) {
            return parseInt(browserInfo.version) >= minVersion;
        }
    };
})();

// Utilisation
console.log('Navigateur:', BrowserDetector.info.name);
console.log('Version:', BrowserDetector.info.version);
console.log('OS:', BrowserDetector.info.os);
console.log('Mobile:', BrowserDetector.info.mobile);

if (BrowserDetector.isChrome() && BrowserDetector.versionAtLeast(90)) {
    console.log('Chrome moderne détecté');
}

Pourquoi éviter le UA sniffing comme méthode principale

  1. Fragile : Les User Agents peuvent être facilement modifiés ou falsifiés
  2. Non maintenable : Chaque nouveau navigateur ou version nécessite une mise à jour
  3. Imprécis : Ne reflète pas les capacités réelles du navigateur
  4. Obsolète : Les navigateurs modernes convergent vers des standards communs
// MAUVAISE PRATIQUE - À éviter
if (navigator.userAgent.indexOf('Chrome') !== -1) {
    // Code spécifique Chrome - FRAGILE !
}

// BONNE PRATIQUE - Feature detection
if ('IntersectionObserver' in window) {
    // Utiliser IntersectionObserver
    var observer = new IntersectionObserver(callback, options);
} else {
    // Fallback avec scroll events
    window.addEventListener('scroll', handleScrollFallback);
}

Singleton Pattern : garantir une instance unique

Le Singleton est l’un des patterns créatifs les plus utilisés. Il garantit qu’une classe n’a qu’une seule instance et fournit un point d’accès global à cette instance.

Cas d’utilisation typiques

  • Gestionnaire de configuration : Une seule source de vérité pour les paramètres
  • Pool de connexions : Contrôler le nombre de connexions à une base de données
  • Logger : Un seul point d’entrée pour la journalisation
  • Cache : Un cache partagé entre tous les modules
  • État global : Store Redux-like simplifié

Implémentation avec closure

// Singleton classique avec closure
var ConfigurationManager = (function() {
    var instance = null;
    var config = {};

    function createInstance() {
        return {
            get: function(key) {
                return config[key];
            },
            set: function(key, value) {
                config[key] = value;
                return this;
            },
            getAll: function() {
                return Object.assign({}, config);
            },
            load: function(newConfig) {
                config = Object.assign({}, config, newConfig);
                return this;
            },
            reset: function() {
                config = {};
                return this;
            }
        };
    }

    return {
        getInstance: function() {
            if (!instance) {
                instance = createInstance();
            }
            return instance;
        }
    };
})();

// Utilisation
var config1 = ConfigurationManager.getInstance();
config1.set('apiUrl', 'https://api.example.com')
       .set('timeout', 5000)
       .set('debug', true);

var config2 = ConfigurationManager.getInstance();
console.log(config2.get('apiUrl')); // 'https://api.example.com'
console.log(config1 === config2); // true - même instance

Implémentation avec classe ES6

// Singleton avec classe ES6
class Logger {
    constructor() {
        if (Logger.instance) {
            return Logger.instance;
        }

        this.logs = [];
        this.level = 'info';
        this.levels = { debug: 0, info: 1, warn: 2, error: 3 };

        Logger.instance = this;
    }

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

    setLevel(level) {
        if (this.levels.hasOwnProperty(level)) {
            this.level = level;
        }
        return this;
    }

    _shouldLog(level) {
        return this.levels[level] >= this.levels[this.level];
    }

    _formatMessage(level, message, data) {
        var timestamp = new Date().toISOString();
        return {
            timestamp: timestamp,
            level: level.toUpperCase(),
            message: message,
            data: data || null
        };
    }

    _log(level, message, data) {
        if (this._shouldLog(level)) {
            var logEntry = this._formatMessage(level, message, data);
            this.logs.push(logEntry);
            console[level]('[' + logEntry.timestamp + '] [' + logEntry.level + ']', message, data || '');
        }
        return this;
    }

    debug(message, data) { return this._log('debug', message, data); }
    info(message, data) { return this._log('info', message, data); }
    warn(message, data) { return this._log('warn', message, data); }
    error(message, data) { return this._log('error', message, data); }

    getLogs(level) {
        if (level) {
            return this.logs.filter(function(log) {
                return log.level === level.toUpperCase();
            });
        }
        return this.logs.slice();
    }

    clear() {
        this.logs = [];
        return this;
    }
}

// Utilisation
var logger1 = Logger.getInstance();
logger1.setLevel('debug');
logger1.info('Application démarrée');
logger1.debug('Chargement des modules', { modules: ['auth', 'api', 'ui'] });

var logger2 = new Logger(); // Retourne la même instance
console.log(logger1 === logger2); // true

Module Pattern : Singleton naturel

En JavaScript, les modules ES6 sont naturellement des singletons. Chaque module n’est évalué qu’une seule fois.

// cache.js - Module singleton naturel
var cache = {};
var maxSize = 100;
var accessOrder = [];

function get(key) {
    if (cache.hasOwnProperty(key)) {
        // Mettre à jour l'ordre d'accès (LRU)
        var index = accessOrder.indexOf(key);
        if (index > -1) {
            accessOrder.splice(index, 1);
        }
        accessOrder.push(key);
        return cache[key].value;
    }
    return undefined;
}

function set(key, value, ttl) {
    // Vérifier la taille du cache
    if (!cache.hasOwnProperty(key) && Object.keys(cache).length >= maxSize) {
        // Supprimer l'élément le moins récemment utilisé
        var oldestKey = accessOrder.shift();
        delete cache[oldestKey];
    }

    cache[key] = {
        value: value,
        expiry: ttl ? Date.now() + ttl : null
    };

    var index = accessOrder.indexOf(key);
    if (index > -1) {
        accessOrder.splice(index, 1);
    }
    accessOrder.push(key);

    return true;
}

function has(key) {
    if (!cache.hasOwnProperty(key)) {
        return false;
    }
    // Vérifier l'expiration
    if (cache[key].expiry && Date.now() > cache[key].expiry) {
        remove(key);
        return false;
    }
    return true;
}

function remove(key) {
    if (cache.hasOwnProperty(key)) {
        delete cache[key];
        var index = accessOrder.indexOf(key);
        if (index > -1) {
            accessOrder.splice(index, 1);
        }
        return true;
    }
    return false;
}

function clear() {
    cache = {};
    accessOrder = [];
}

function size() {
    return Object.keys(cache).length;
}

function setMaxSize(newSize) {
    maxSize = newSize;
    // Purger si nécessaire
    while (Object.keys(cache).length > maxSize) {
        var oldestKey = accessOrder.shift();
        delete cache[oldestKey];
    }
}

// Export du module (singleton naturel)
var Cache = {
    get: get,
    set: set,
    has: has,
    remove: remove,
    clear: clear,
    size: size,
    setMaxSize: setMaxSize
};

// Utilisation
Cache.set('user:123', { name: 'John', email: 'john@example.com' }, 60000); // TTL 60s
Cache.set('settings', { theme: 'dark', language: 'fr' });

console.log(Cache.get('user:123')); // { name: 'John', ... }
console.log(Cache.has('settings')); // true
console.log(Cache.size()); // 2

Factory Pattern : création flexible d’objets

Le Factory Pattern permet de créer des objets sans spécifier leur classe exacte. Il encapsule la logique de création et favorise le découplage.

Simple Factory

La Simple Factory est une classe ou fonction qui crée des objets basés sur un paramètre.

// Simple Factory pour les notifications
function NotificationFactory() {}

NotificationFactory.create = function(type, options) {
    var notification;

    switch (type) {
        case 'email':
            notification = new EmailNotification(options);
            break;
        case 'sms':
            notification = new SMSNotification(options);
            break;
        case 'push':
            notification = new PushNotification(options);
            break;
        case 'slack':
            notification = new SlackNotification(options);
            break;
        default:
            throw new Error('Type de notification inconnu: ' + type);
    }

    return notification;
};

// Classes de notification
function EmailNotification(options) {
    this.to = options.to;
    this.subject = options.subject || 'Notification';
    this.body = options.body;
}

EmailNotification.prototype.send = function() {
    console.log('Envoi email à ' + this.to + ': ' + this.subject);
    // Logique d'envoi email
    return Promise.resolve({ success: true, type: 'email' });
};

function SMSNotification(options) {
    this.phone = options.phone;
    this.message = options.message;
}

SMSNotification.prototype.send = function() {
    console.log('Envoi SMS à ' + this.phone);
    // Logique d'envoi SMS
    return Promise.resolve({ success: true, type: 'sms' });
};

function PushNotification(options) {
    this.token = options.token;
    this.title = options.title;
    this.body = options.body;
    this.data = options.data || {};
}

PushNotification.prototype.send = function() {
    console.log('Envoi push notification: ' + this.title);
    // Logique d'envoi push
    return Promise.resolve({ success: true, type: 'push' });
};

function SlackNotification(options) {
    this.webhook = options.webhook;
    this.channel = options.channel;
    this.message = options.message;
}

SlackNotification.prototype.send = function() {
    console.log('Envoi message Slack sur ' + this.channel);
    // Logique d'envoi Slack
    return Promise.resolve({ success: true, type: 'slack' });
};

// Utilisation
var emailNotif = NotificationFactory.create('email', {
    to: 'user@example.com',
    subject: 'Bienvenue',
    body: 'Merci de votre inscription!'
});

var smsNotif = NotificationFactory.create('sms', {
    phone: '+33612345678',
    message: 'Code de vérification: 123456'
});

emailNotif.send();
smsNotif.send();

Factory Method

Le Factory Method définit une interface pour créer un objet, mais laisse les sous-classes décider quelle classe instancier.

// Factory Method pour les composants UI
function UIComponent(options) {
    this.id = options.id || 'component-' + Date.now();
    this.className = options.className || '';
    this.element = null;
}

UIComponent.prototype.render = function() {
    throw new Error('La méthode render() doit être implémentée');
};

UIComponent.prototype.mount = function(container) {
    this.element = this.render();
    container.appendChild(this.element);
    return this;
};

UIComponent.prototype.unmount = function() {
    if (this.element && this.element.parentNode) {
        this.element.parentNode.removeChild(this.element);
    }
    return this;
};

// Factory Method - à surcharger
UIComponent.prototype.createElement = function() {
    return document.createElement('div');
};

// Implémentation Button
function ButtonComponent(options) {
    UIComponent.call(this, options);
    this.text = options.text || 'Button';
    this.onClick = options.onClick || function() {};
    this.variant = options.variant || 'primary';
}

ButtonComponent.prototype = Object.create(UIComponent.prototype);
ButtonComponent.prototype.constructor = ButtonComponent;

ButtonComponent.prototype.render = function() {
    var button = document.createElement('button');
    button.id = this.id;
    button.className = 'btn btn-' + this.variant + ' ' + this.className;
    button.textContent = this.text;
    button.addEventListener('click', this.onClick);
    return button;
};

// Implémentation Input
function InputComponent(options) {
    UIComponent.call(this, options);
    this.type = options.type || 'text';
    this.placeholder = options.placeholder || '';
    this.value = options.value || '';
    this.onChange = options.onChange || function() {};
}

InputComponent.prototype = Object.create(UIComponent.prototype);
InputComponent.prototype.constructor = InputComponent;

InputComponent.prototype.render = function() {
    var wrapper = document.createElement('div');
    wrapper.className = 'input-wrapper ' + this.className;

    var input = document.createElement('input');
    input.id = this.id;
    input.type = this.type;
    input.placeholder = this.placeholder;
    input.value = this.value;
    input.addEventListener('input', this.onChange);

    wrapper.appendChild(input);
    return wrapper;
};

// Implémentation Modal
function ModalComponent(options) {
    UIComponent.call(this, options);
    this.title = options.title || '';
    this.content = options.content || '';
    this.onClose = options.onClose || function() {};
}

ModalComponent.prototype = Object.create(UIComponent.prototype);
ModalComponent.prototype.constructor = ModalComponent;

ModalComponent.prototype.render = function() {
    var self = this;
    var overlay = document.createElement('div');
    overlay.className = 'modal-overlay ' + this.className;
    overlay.id = this.id;

    var modal = document.createElement('div');
    modal.className = 'modal';

    var header = document.createElement('div');
    header.className = 'modal-header';
    header.innerHTML = '<h3>' + this.title + '</h3>';

    var closeBtn = document.createElement('button');
    closeBtn.className = 'modal-close';
    closeBtn.textContent = '×';
    closeBtn.addEventListener('click', function() {
        self.close();
    });
    header.appendChild(closeBtn);

    var body = document.createElement('div');
    body.className = 'modal-body';
    body.innerHTML = this.content;

    modal.appendChild(header);
    modal.appendChild(body);
    overlay.appendChild(modal);

    overlay.addEventListener('click', function(e) {
        if (e.target === overlay) {
            self.close();
        }
    });

    return overlay;
};

ModalComponent.prototype.close = function() {
    this.onClose();
    this.unmount();
};

// Factory pour créer les composants
var ComponentFactory = {
    create: function(type, options) {
        switch (type) {
            case 'button': return new ButtonComponent(options);
            case 'input': return new InputComponent(options);
            case 'modal': return new ModalComponent(options);
            default: throw new Error('Type de composant inconnu: ' + type);
        }
    }
};

// Utilisation
var loginButton = ComponentFactory.create('button', {
    text: 'Se connecter',
    variant: 'primary',
    onClick: function() { console.log('Login clicked'); }
});

var emailInput = ComponentFactory.create('input', {
    type: 'email',
    placeholder: 'Votre email',
    onChange: function(e) { console.log('Email:', e.target.value); }
});

var welcomeModal = ComponentFactory.create('modal', {
    title: 'Bienvenue !',
    content: '<p>Merci de votre visite.</p>',
    onClose: function() { console.log('Modal fermée'); }
});

Abstract Factory

L’Abstract Factory fournit une interface pour créer des familles d’objets liés sans spécifier leurs classes concrètes.

// Abstract Factory pour les thèmes UI
function UIThemeFactory() {}

UIThemeFactory.prototype.createButton = function() {
    throw new Error('Méthode abstraite');
};

UIThemeFactory.prototype.createInput = function() {
    throw new Error('Méthode abstraite');
};

UIThemeFactory.prototype.createCard = function() {
    throw new Error('Méthode abstraite');
};

// Thème Light
function LightThemeFactory() {
    UIThemeFactory.call(this);
}

LightThemeFactory.prototype = Object.create(UIThemeFactory.prototype);
LightThemeFactory.prototype.constructor = LightThemeFactory;

LightThemeFactory.prototype.createButton = function(options) {
    return {
        type: 'button',
        theme: 'light',
        styles: {
            backgroundColor: '#ffffff',
            color: '#333333',
            border: '1px solid #cccccc',
            borderRadius: '4px',
            padding: '8px 16px'
        },
        text: options.text,
        render: function() {
            var btn = document.createElement('button');
            Object.assign(btn.style, this.styles);
            btn.textContent = this.text;
            return btn;
        }
    };
};

LightThemeFactory.prototype.createInput = function(options) {
    return {
        type: 'input',
        theme: 'light',
        styles: {
            backgroundColor: '#ffffff',
            color: '#333333',
            border: '1px solid #cccccc',
            borderRadius: '4px',
            padding: '8px 12px'
        },
        placeholder: options.placeholder,
        render: function() {
            var input = document.createElement('input');
            Object.assign(input.style, this.styles);
            input.placeholder = this.placeholder;
            return input;
        }
    };
};

LightThemeFactory.prototype.createCard = function(options) {
    return {
        type: 'card',
        theme: 'light',
        styles: {
            backgroundColor: '#ffffff',
            color: '#333333',
            border: '1px solid #e0e0e0',
            borderRadius: '8px',
            padding: '16px',
            boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
        },
        title: options.title,
        content: options.content,
        render: function() {
            var card = document.createElement('div');
            Object.assign(card.style, this.styles);
            card.innerHTML = '<h3>' + this.title + '</h3><p>' + this.content + '</p>';
            return card;
        }
    };
};

// Thème Dark
function DarkThemeFactory() {
    UIThemeFactory.call(this);
}

DarkThemeFactory.prototype = Object.create(UIThemeFactory.prototype);
DarkThemeFactory.prototype.constructor = DarkThemeFactory;

DarkThemeFactory.prototype.createButton = function(options) {
    return {
        type: 'button',
        theme: 'dark',
        styles: {
            backgroundColor: '#333333',
            color: '#ffffff',
            border: '1px solid #555555',
            borderRadius: '4px',
            padding: '8px 16px'
        },
        text: options.text,
        render: function() {
            var btn = document.createElement('button');
            Object.assign(btn.style, this.styles);
            btn.textContent = this.text;
            return btn;
        }
    };
};

DarkThemeFactory.prototype.createInput = function(options) {
    return {
        type: 'input',
        theme: 'dark',
        styles: {
            backgroundColor: '#2d2d2d',
            color: '#ffffff',
            border: '1px solid #555555',
            borderRadius: '4px',
            padding: '8px 12px'
        },
        placeholder: options.placeholder,
        render: function() {
            var input = document.createElement('input');
            Object.assign(input.style, this.styles);
            input.placeholder = this.placeholder;
            return input;
        }
    };
};

DarkThemeFactory.prototype.createCard = function(options) {
    return {
        type: 'card',
        theme: 'dark',
        styles: {
            backgroundColor: '#1e1e1e',
            color: '#ffffff',
            border: '1px solid #333333',
            borderRadius: '8px',
            padding: '16px',
            boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
        },
        title: options.title,
        content: options.content,
        render: function() {
            var card = document.createElement('div');
            Object.assign(card.style, this.styles);
            card.innerHTML = '<h3>' + this.title + '</h3><p>' + this.content + '</p>';
            return card;
        }
    };
};

// Gestionnaire de thème
var ThemeManager = (function() {
    var currentFactory = null;

    return {
        setTheme: function(theme) {
            switch (theme) {
                case 'light':
                    currentFactory = new LightThemeFactory();
                    break;
                case 'dark':
                    currentFactory = new DarkThemeFactory();
                    break;
                default:
                    throw new Error('Thème inconnu: ' + theme);
            }
        },
        getFactory: function() {
            if (!currentFactory) {
                this.setTheme('light'); // Thème par défaut
            }
            return currentFactory;
        }
    };
})();

// Utilisation
ThemeManager.setTheme('dark');
var factory = ThemeManager.getFactory();

var darkButton = factory.createButton({ text: 'Valider' });
var darkInput = factory.createInput({ placeholder: 'Rechercher...' });
var darkCard = factory.createCard({ title: 'Dashboard', content: 'Bienvenue sur votre tableau de bord.' });

Autres Patterns Créatifs

Builder Pattern

Le Builder Pattern sépare la construction d’un objet complexe de sa représentation, permettant de créer différentes représentations avec le même processus de construction.

// Builder pour les requêtes HTTP
function HttpRequestBuilder() {
    this.request = {
        method: 'GET',
        url: '',
        headers: {},
        body: null,
        timeout: 30000,
        credentials: 'same-origin'
    };
}

HttpRequestBuilder.prototype.setMethod = function(method) {
    this.request.method = method.toUpperCase();
    return this;
};

HttpRequestBuilder.prototype.setUrl = function(url) {
    this.request.url = url;
    return this;
};

HttpRequestBuilder.prototype.addHeader = function(key, value) {
    this.request.headers[key] = value;
    return this;
};

HttpRequestBuilder.prototype.setBody = function(body) {
    this.request.body = body;
    return this;
};

HttpRequestBuilder.prototype.setJson = function(data) {
    this.request.headers['Content-Type'] = 'application/json';
    this.request.body = JSON.stringify(data);
    return this;
};

HttpRequestBuilder.prototype.setTimeout = function(timeout) {
    this.request.timeout = timeout;
    return this;
};

HttpRequestBuilder.prototype.withCredentials = function() {
    this.request.credentials = 'include';
    return this;
};

HttpRequestBuilder.prototype.withAuth = function(token) {
    this.request.headers['Authorization'] = 'Bearer ' + token;
    return this;
};

HttpRequestBuilder.prototype.build = function() {
    if (!this.request.url) {
        throw new Error('URL est requise');
    }
    return Object.assign({}, this.request);
};

HttpRequestBuilder.prototype.execute = function() {
    var config = this.build();

    return fetch(config.url, {
        method: config.method,
        headers: config.headers,
        body: config.body,
        credentials: config.credentials,
        signal: AbortSignal.timeout(config.timeout)
    });
};

// Utilisation
var getUserRequest = new HttpRequestBuilder()
    .setMethod('GET')
    .setUrl('https://api.example.com/users/123')
    .withAuth('jwt-token-here')
    .addHeader('Accept', 'application/json')
    .setTimeout(10000)
    .build();

console.log(getUserRequest);

// Exécution directe
new HttpRequestBuilder()
    .setMethod('POST')
    .setUrl('https://api.example.com/users')
    .setJson({ name: 'John', email: 'john@example.com' })
    .withAuth('jwt-token-here')
    .execute()
    .then(function(response) {
        return response.json();
    })
    .then(function(data) {
        console.log('Utilisateur créé:', data);
    })
    .catch(function(error) {
        console.error('Erreur:', error);
    });

// Builder pour la construction de formulaires
function FormBuilder(formId) {
    this.form = document.createElement('form');
    this.form.id = formId;
    this.fields = [];
}

FormBuilder.prototype.addTextField = function(name, label, options) {
    options = options || {};
    this.fields.push({
        type: 'text',
        name: name,
        label: label,
        placeholder: options.placeholder || '',
        required: options.required || false,
        validation: options.validation || null
    });
    return this;
};

FormBuilder.prototype.addEmailField = function(name, label, options) {
    options = options || {};
    this.fields.push({
        type: 'email',
        name: name,
        label: label,
        placeholder: options.placeholder || '',
        required: options.required || false
    });
    return this;
};

FormBuilder.prototype.addPasswordField = function(name, label, options) {
    options = options || {};
    this.fields.push({
        type: 'password',
        name: name,
        label: label,
        minLength: options.minLength || 8,
        required: options.required || false
    });
    return this;
};

FormBuilder.prototype.addSelectField = function(name, label, options, selectOptions) {
    options = options || {};
    this.fields.push({
        type: 'select',
        name: name,
        label: label,
        options: selectOptions || [],
        required: options.required || false
    });
    return this;
};

FormBuilder.prototype.addSubmitButton = function(text) {
    this.submitText = text || 'Envoyer';
    return this;
};

FormBuilder.prototype.onSubmit = function(callback) {
    this.submitCallback = callback;
    return this;
};

FormBuilder.prototype.build = function() {
    var self = this;

    this.fields.forEach(function(field) {
        var wrapper = document.createElement('div');
        wrapper.className = 'form-field';

        var label = document.createElement('label');
        label.textContent = field.label;
        label.setAttribute('for', field.name);
        wrapper.appendChild(label);

        var input;
        if (field.type === 'select') {
            input = document.createElement('select');
            field.options.forEach(function(opt) {
                var option = document.createElement('option');
                option.value = opt.value;
                option.textContent = opt.label;
                input.appendChild(option);
            });
        } else {
            input = document.createElement('input');
            input.type = field.type;
            input.placeholder = field.placeholder || '';
        }

        input.name = field.name;
        input.id = field.name;
        if (field.required) input.required = true;

        wrapper.appendChild(input);
        self.form.appendChild(wrapper);
    });

    if (this.submitText) {
        var button = document.createElement('button');
        button.type = 'submit';
        button.textContent = this.submitText;
        this.form.appendChild(button);
    }

    if (this.submitCallback) {
        this.form.addEventListener('submit', function(e) {
            e.preventDefault();
            var formData = new FormData(self.form);
            var data = {};
            formData.forEach(function(value, key) {
                data[key] = value;
            });
            self.submitCallback(data);
        });
    }

    return this.form;
};

// Utilisation du FormBuilder
var loginForm = new FormBuilder('login-form')
    .addEmailField('email', 'Adresse email', { required: true, placeholder: 'you@example.com' })
    .addPasswordField('password', 'Mot de passe', { required: true, minLength: 8 })
    .addSubmitButton('Se connecter')
    .onSubmit(function(data) {
        console.log('Formulaire soumis:', data);
    })
    .build();

Prototype Pattern

Le Prototype Pattern crée de nouveaux objets en clonant un prototype existant plutôt qu’en utilisant un constructeur.

// Prototype Pattern avec Object.create
var VehiclePrototype = {
    type: 'vehicle',
    wheels: 4,
    engine: null,

    init: function(options) {
        this.brand = options.brand || 'Unknown';
        this.model = options.model || 'Unknown';
        this.year = options.year || new Date().getFullYear();
        this.color = options.color || 'white';
        return this;
    },

    clone: function() {
        var cloned = Object.create(Object.getPrototypeOf(this));
        return Object.assign(cloned, this);
    },

    getInfo: function() {
        return this.brand + ' ' + this.model + ' (' + this.year + ') - ' + this.color;
    },

    start: function() {
        console.log(this.brand + ' ' + this.model + ' démarre...');
    },

    stop: function() {
        console.log(this.brand + ' ' + this.model + ' s\'arrête...');
    }
};

// Création de prototypes spécialisés
var CarPrototype = Object.create(VehiclePrototype);
CarPrototype.type = 'car';
CarPrototype.doors = 4;
CarPrototype.openTrunk = function() {
    console.log('Coffre ouvert');
};

var MotorcyclePrototype = Object.create(VehiclePrototype);
MotorcyclePrototype.type = 'motorcycle';
MotorcyclePrototype.wheels = 2;
MotorcyclePrototype.hasSidecar = false;

// Utilisation
var myCar = Object.create(CarPrototype).init({
    brand: 'Toyota',
    model: 'Corolla',
    year: 2023,
    color: 'blue'
});

var myBike = Object.create(MotorcyclePrototype).init({
    brand: 'Honda',
    model: 'CB500',
    year: 2022,
    color: 'red'
});

console.log(myCar.getInfo()); // Toyota Corolla (2023) - blue
console.log(myBike.getInfo()); // Honda CB500 (2022) - red

// Clonage
var carClone = myCar.clone();
carClone.color = 'green';
console.log(myCar.color); // blue (non affecté)
console.log(carClone.color); // green

// Registry de prototypes
var PrototypeRegistry = (function() {
    var prototypes = {};

    return {
        register: function(name, prototype) {
            prototypes[name] = prototype;
        },
        unregister: function(name) {
            delete prototypes[name];
        },
        create: function(name, options) {
            var prototype = prototypes[name];
            if (!prototype) {
                throw new Error('Prototype non trouvé: ' + name);
            }
            return Object.create(prototype).init(options);
        },
        clone: function(name) {
            var prototype = prototypes[name];
            if (!prototype) {
                throw new Error('Prototype non trouvé: ' + name);
            }
            return prototype.clone();
        },
        list: function() {
            return Object.keys(prototypes);
        }
    };
})();

// Enregistrement des prototypes
PrototypeRegistry.register('car', CarPrototype);
PrototypeRegistry.register('motorcycle', MotorcyclePrototype);

// Création via le registry
var sedan = PrototypeRegistry.create('car', {
    brand: 'Honda',
    model: 'Civic',
    year: 2024,
    color: 'silver'
});

console.log(sedan.getInfo()); // Honda Civic (2024) - silver

Bonnes Pratiques

Pour la détection de navigateur

  1. Privilégiez toujours la feature detection - Testez les capacités, pas les noms de navigateurs
  2. Utilisez des polyfills - Ajoutez les fonctionnalités manquantes plutôt que de les contourner
  3. Progressive enhancement - Construisez une base fonctionnelle, puis enrichissez
  4. Testez sur de vrais appareils - Les émulateurs ne reproduisent pas tous les comportements
  5. Documentez vos workarounds - Expliquez pourquoi un UA sniffing est nécessaire si vous l’utilisez

Pour les design patterns

  1. Ne sur-architecturez pas - Un pattern doit résoudre un problème, pas en créer
  2. Singleton avec modération - L’état global peut rendre le code difficile à tester
  3. Factory pour la flexibilité - Utilisez-la quand les types d’objets peuvent évoluer
  4. Builder pour les objets complexes - Plus de 3-4 paramètres ? Pensez Builder
  5. Prototype pour les performances - Cloner est souvent plus rapide que construire
// Exemple de bonne pratique : combiner patterns
var ApplicationBuilder = (function() {
    var instance = null; // Singleton

    function Application() {
        this.config = {};
        this.modules = [];
        this.logger = Logger.getInstance(); // Réutilisation du singleton Logger
    }

    Application.prototype.configure = function(config) {
        this.config = Object.assign({}, this.config, config);
        return this;
    };

    Application.prototype.addModule = function(name, factory) {
        this.modules.push({ name: name, factory: factory }); // Factory pattern
        return this;
    };

    Application.prototype.build = function() {
        var self = this;
        this.modules.forEach(function(mod) {
            self[mod.name] = mod.factory(self.config);
        });
        this.logger.info('Application construite avec ' + this.modules.length + ' modules');
        return this;
    };

    return {
        getInstance: function() {
            if (!instance) {
                instance = new Application();
            }
            return instance;
        }
    };
})();

Pièges Courants

Détection de navigateur

PiègeProblèmeSolution
UA sniffing exclusifNavigateurs non reconnus exclusFeature detection en complément
Cache du UALe UA peut changer (mises à jour)Détecter à chaque chargement si critique
Oublier les botsCrawlers ont des UAs différentsGérer les cas “inconnus” gracieusement
Tests incompletsNe tester que Chrome/FirefoxAutomatiser les tests cross-browser

Design Patterns

PiègeProblèmeSolution
Singleton partoutCouplage fort, tests difficilesInjection de dépendances
Factory trop génériqueSwitch case énormeAbstract Factory ou Strategy
Builder sans resetRéutilisation problématiqueMéthode reset() ou nouvelle instance
Prototype mutationModifications affectent tous les clonesDeep clone pour objets imbriqués
// Piège : Singleton qui empêche les tests
// MAUVAIS
var Database = (function() {
    var instance = new RealDatabase(); // Connexion immédiate !
    return { getInstance: function() { return instance; } };
})();

// MIEUX : Lazy initialization + injection possible
var Database = (function() {
    var instance = null;
    var DatabaseClass = RealDatabase; // Peut être remplacé pour les tests

    return {
        setImplementation: function(impl) { DatabaseClass = impl; },
        getInstance: function() {
            if (!instance) { instance = new DatabaseClass(); }
            return instance;
        },
        resetForTesting: function() { instance = null; }
    };
})();

// En test
Database.setImplementation(MockDatabase);
Database.resetForTesting();

Conclusion et Récapitulatif

La maîtrise de la détection de navigateur et des design patterns créatifs est essentielle pour tout développeur JavaScript souhaitant créer des applications robustes et maintenables.

Tableau récapitulatif des patterns

PatternCas d’usageAvantagesInconvénients
SingletonConfiguration, Cache, LoggerInstance unique garantie, accès globalCouplage fort, tests difficiles
Simple FactoryCréation basée sur un typeSimple, centraliséeSwitch case peut grandir
Factory MethodCréation déléguée aux sous-classesExtensible, découpléPlus de classes à maintenir
Abstract FactoryFamilles d’objets liésCohérence garantie, thèmesComplexité accrue
BuilderObjets avec nombreux paramètresLisible, flexibleVerbeux pour objets simples
PrototypeClonage d’objets configurésPerformance, flexibilitéAttention aux références

Points clés à retenir

  1. Feature detection > UA sniffing - Testez les capacités, pas les navigateurs
  2. Progressive enhancement - Base fonctionnelle + enrichissements
  3. Patterns = outils - Utilisez-les pour résoudre des problèmes spécifiques
  4. Testabilité - Concevez vos singletons et factories pour être testables
  5. KISS - Keep It Simple, Stupid - Ne sur-architecturez pas

Ces techniques vous permettront de créer des applications JavaScript professionnelles, compatibles avec une large gamme de navigateurs et structurées de manière à faciliter la maintenance et l’évolution du code.

Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

Mahmoud DEVO

Senior Full-Stack Developer

I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.

Enjoyed this article?

Subscribe to get more tech content delivered to your inbox.

Related Articles