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
coreou 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"