FunPlayer - Development Guidelines

July 19, 2025 · View on GitHub

đŸ—ïž Architecture Patterns

✅ Pattern Core + Managers Autonomes

// ✅ BON: class FunPlayerCore centralisant l'accĂšs aux managers avec notifier unique passĂ© en prop
class FunPlayerCore {
  get buttplug() {
    if (!this._buttplug) {
      this._buttplug = new ButtPlugManager(this._notify);
    }
    return this._buttplug;
  }
}

// ✅ BON: Managers autonomes, pas de dĂ©pendances entre eux
class ButtPlugManager {
  constructor(notify) {
    this.notify = notify;
    // Logique autonome device/actuators
  }
}
// ❌ ÉVITER: DĂ©pendances directes entre managers
class ButtPlugManager {
  constructor(funscriptManager) {  // ❌ Couplage tight
    this.funscript = funscriptManager;
  }
}

✅ Pattern Notification CentralisĂ©

// ✅ BON: Notifications directes dans les mĂ©thodes devant avoir un impact rĂ©actif
setGlobalScale(scale) {
  this.globalScale = scale;
  // Notification immĂ©diate lĂ  oĂč ça se passe
  this.notify?.('buttplug:globalScale', { scale });
}

// ✅ BON: Event handlers centralisĂ©s dans Core 
handleCoreEvents(event, data) {
  switch (event) {
    case 'buttplug:device':
      this._handleButtplugDevice(data);
      break;
  }
}
// ❌ ÉVITER: Indirections inutiles
setGlobalScale(scale) {
  this.globalScale = scale;
  this._notifyGlobalScaleChanged(scale);  // ❌ indirection supplĂ©mentaire
}

_notifyGlobalScaleChanged(scale) {        // ❌ Code verbeux
  if (this.notify) {
    this.notify('buttplug:globalScale', { scale });
  }
}

🎹 SĂ©paration UI / Business Logic

✅ Composants UI Purs

// ✅ BON: Composant UI avec appels directs + events pour re-render
class HapticSettingsComponent extends Component {

  constructor(props) {
    super(props);

    this.core=props.core // core as unique business dependency
    
    ...
    
    this.coreListener = null;
  }

  componentDidMount() {
    // S'abonner aux événements pour déclencher re-render
    this.coreListener = this.core.addListener(this.handleCoreEvent);
  }
  
  handleCoreEvent = (event, data) => {
    // Les composants UI ne font QUE déclencher re-render
    const eventsToReact = ['buttplug:globalScale', 'buttplug:device'];
    if (eventsToReact.includes(event)) {
      this._triggerRender();  // ✅ Juste re-render, c'est tout
    }
  }
  
  handleGlobalScaleChange = (scale) => {
    // Appel direct au core/manager
    this.core.buttplug.setGlobalScale(scale);  //  ✅ Bon, mais pas optimal! indirection suppĂ©lementaire alors qu'il suffit d'appler directement l'outil core au bon endroit
  }
  
  render() {
    // UI pure basée sur l'état actuel des managers
    const globalScale = this.core.buttplug.getGlobalScale();// Acceptable, mĂȘme si c'est encore une indirection
    return <input value={globalScale} onChange={this.handleGlobalScaleChange}/>; // pas optimal !

    return <input value={globalScale} onChange={this.core.buttplug.setGlobalScale}/> // ✅ Bon, pas d'indirection vers le bon outil

    return <input value={this.core.buttplug.getGlobalScale()} onChange={this.core.buttplug.setGlobalScale}/> //// ✅ visuellement plus lourd mais optimal, pas d'indrection du tout.

  }
}
// ❌ ÉVITER: Business logic dans les event handlers UI
handleCoreEvent = (event, data) => {
  switch (event) {
    case 'buttplug:device':
      // ❌ JAMAIS de business logic dans UI
      if (data.device && this.hasFunscript()) {
        const mapResult = this.autoMapChannels();
        this.setState({ status: `Mapped ${mapResult.mapped} channels` });
      }
      this._triggerRender();
      break;
  }
}

// ❌ ÉVITER: Props callback pour business logic
class HapticSettingsComponent extends Component {
  render() {
    return (
      <input 
        onChange={(scale) => this.props.onGlobalScaleChange(scale)}  // ❌
      />
    );
  }
}

⚠ RÈGLE SIMPLE: Les composants UI s'abonnent aux Ă©vĂ©nements choisis pour dĂ©clencher leur triggerRender(), c'est tout. Aucune business logic dans les event handlers UI.

✅ Business Logic CentralisĂ©e

// ✅ BON: Logique business dans Core avec handlers spĂ©cialisĂ©s, dĂ©clenchĂ©s par le bus d'Ă©venement
handleButtplugDevice(data) {
  const { device } = data;
  
  // Logique business réactive : auto-map si device + funscript
  if (device && this.funscript.getChannels().length > 0) {
    setTimeout(() => {
      const mapResult = this.autoMapChannels();
      console.log(`Auto-mapped ${mapResult.mapped} channels`);
    }, 100);
  }
}

🔧 Code Quality & Style

✅ API SimplifiĂ©e et Directe

// ✅ BON: API claire et directe
core.buttplug.setGlobalScale(0.8);
core.funscript.load(data);
core.playlist.goTo(2);

// ✅ BON: MĂ©thodes business courtes avec responsabilitĂ© claire et pertinente
getActuator(index) {
  return this.actuators.find(actuator => actuator.index === index) || null;
}
// ❌ ÉVITER: VerbositĂ© excessive
const buttplugManager = core.getButtPlugManager();
const settings = buttplugManager.getGlobalSettings();
settings.setScale(0.8);
buttplugManager.applyGlobalSettings(settings);

// ❌ ÉVITER: MĂ©thodes trop longues
processComplexWorkflowWithMultipleStepsAndValidation() {
  // 50+ lignes de code...
}

// ❌ ÉVITER: Indirections inutiles qui ne font que rediriger
handleGlobalScaleChange(scale) {
  this.updateGlobalScale(scale);  // ❌ Wrapper inutile
}

updateGlobalScale(scale) {
  this.core.buttplug.setGlobalScale(scale);  // ❌ Juste une redirection
}

// ✅ À LA PLACE: Appel direct
handleGlobalScaleChange(scale) {
  this.core.buttplug.setGlobalScale(scale);  // ✅ Direct
}

⚠ RÈGLE IMPORTANTE: On Ă©vite Ă  tout prix les indirections vers des mĂ©thodes intermĂ©diaires qui ne font rien Ă  part rediriger. On appelle directement core ou ses managers avec la fonction business appropriĂ©e.

✅ Gestion d'État Locale

// ✅ BON: État technique UI dans les composants
class FunPlayer extends Component {
  state = {
    showVisualizer: true,    // État UI
    updateRate: 60,          // Config technique
    currentActuatorData: new Map()  // Cache UI
  }
}

// ✅ BON: État business dans les managers
class PlaylistManager {
  constructor() {
    this.currentIndex = -1;  // État business
    this.isPlaying = false;  // État business
  }
}

✅ Patterns de Nommage CohĂ©rents

// ✅ BON: Conventions cohĂ©rentes
// Getters simples
getActuators()
getCurrentItem()
getGlobalScale()

// Setters simples  
setGlobalScale(scale)
setIntifaceUrl(url)

// Actions métier
connect()
scan()
load()
reset()

// ÉvĂ©nements descriptifs
'buttplug:connection'
'funscript:load'
'playlist:itemChanged'

đŸš« Anti-Patterns Ă  Éviter

❌ Couplage Tight

// ❌ Managers qui se connaissent directement
// ❌ Props callback pour business logic  
// ❌ État business dupliquĂ© dans UI

❌ VerbositĂ© Excessive

// ❌ MĂ©thodes wrapper sans valeur ajoutĂ©e
// ❌ Indirections multiples pour un simple appel
// ❌ Callbacks en cascade

❌ ResponsabilitĂ©s MĂ©langĂ©es

// ❌ Business logic dans les composants React
// ❌ UI state dans les managers business
// ❌ Event handling Ă©parpillĂ© partout

📋 Checklist DĂ©veloppement

Avant d'ajouter du code :

  • La responsabilitĂ© est-elle dans le bon manager/composant ?
  • Peut-on faire plus simple/direct ?
  • Évite-t-on la duplication de code ?
  • L'API reste-t-elle intuitive ?

Pour les managers business :

  • Autonome (pas de dĂ©pendance vers autres managers)
  • Utilise this.notify() directement dans les mĂ©thodes
  • API simple et prĂ©visible
  • ResponsabilitĂ© claire et dĂ©limitĂ©e

Pour les composants UI :

  • Appels directs this.core.manager.method()
  • Pas de business logic (sauf technique UI)
  • État local uniquement pour UI/technique
  • Props callback uniquement pour coordination UI

Pour les événements :

  • Noms descriptifs et cohĂ©rents
  • Handlers centralisĂ©s dans Core
  • Business logic dans les handlers, pas dans UI

🎯 Objectifs QualitĂ©

ÉlĂ©gance mathĂ©matique : Moins de code, plus de fonctionnalitĂ©
Prévisibilité : Patterns cohérents partout
MaintenabilitĂ© : SĂ©paration claire des responsabilitĂ©s, dĂ©couplage des blocs logiques qui peuvent ĂȘtre autonomes EfficacitĂ© : Pas de verbositĂ©, pas de redondance, code peu saturĂ© de bruit visuel qui parasite sa comprĂ©hension et sa navigation.

"Le code parfait est celui qu'on n'a pas eu besoin d'écrire"