Scrivere codice Better React con la separazione del principio di comando e query

May 03 2023
Uno dei principi che utilizzo costantemente nel mio codice è una forma speciale o un uso concreto del principio di responsabilità unica al suo interno. Questo principio è il principio di separazione tra comando e interrogazione.

Uno dei principi che utilizzo costantemente nel mio codice è una forma speciale o un uso concreto del principio di responsabilità unica al suo interno. Questo principio è il principio di separazione tra comando e interrogazione .

Foto di Charles G su Unsplash

Il principio di separazione dei comandi e delle query è un principio di progettazione del software che suggerisce che i metodi o le funzioni dovrebbero essere comandi che modificano lo stato del sistema o query che restituiscono informazioni sullo stato del sistema, ma non entrambi.

I comandi (o modificatori) sono metodi che eseguono un'azione o modificano lo stato di un oggetto senza restituire un valore. Le query, d'altra parte, sono metodi che modificano lo stato di un oggetto. La separazione di comandi e query può aiutare a ridurre l'accoppiamento tra i componenti, semplificando il test, la manutenzione e la modifica del codice. Rende anche più facile ragionare sul comportamento del codice e può migliorare la progettazione complessiva di un sistema.

Il codice sottostante è considerato errato perché addIteme removeItemsta facendo più di una cosa: modificare i dati, aggiornare il prezzo totale e restituire il valore aggiornato.

class ShoppingCart {
  constructor() {
    this.items = [];
    this.totalPrice = 0;
  }

  addItem(item) {
    this.items.push(item);
    this.updateTotalPrice();
    
    return this.totalPrice;
  }

  removeItem(item) {
    const index = this.items.indexOf(item);
    if (index > -1) {
      this.items.splice(index, 1);
      this.updateTotalPrice();
    }

    return this.totalPrice;
  }

  updateTotalPrice() {
    for (let i = 0; i < items.length; i++) {
      this.totalPrice += items[i].price;
    }
  }
}

class ShoppingCart {
  constructor() {
    this.items = [];
    this.totalPrice = 0;
  }

  addItem(item) {
    this.items.push(item);
  }

  removeItem(item) {
    const index = this.items.findIndex((item) => item.id === id);
    if (index > -1) {
      this.items.splice(index, 1);
    }
  }

  get totalPrice () {
      this.items.reduce((total, item) => total + item.price, 0) 
  }
}

In un'applicazione React, possiamo separare modificatori e query utilizzando una libreria di gestione dello stato come Redux o React Context API. Ciò consente di separare la logica per la modifica dello stato dell'applicazione dalla logica per il rendering dell'interfaccia utente.

Per saperne di più su Context e altri hook, puoi scaricare qui un cheat sheet contenente gli hook React più comuni con esempi e illustrazioni.

Questo principio di separazione delle responsabilità della query di comando può essere applicato a diversi livelli. A livello di architettura, potresti aver sentito parlare di CQRS in quel contesto. Essenzialmente, sono lo stesso principio applicato a diversi livelli di astrazione.

Esempio: carrello della spesa

Un'applicazione per il carrello della spesa potrebbe avere una ShoppingCartclasse che gestisce gli articoli del carrello e calcola il prezzo totale. I metodi addIteme removeItemnella ShoppingCartclasse sono comandi che modificano gli articoli del carrello, mentre il getTotalPricemetodo è una query che restituisce il prezzo totale degli articoli nel carrello.

Prima di applicare il principio di separazione del comando e della query, i metodi addIteme removeItempotrebbero anche aggiornare direttamente la proprietà del prezzo totale. Tuttavia, questo può rendere il codice più difficile da comprendere e mantenere.

Ecco un esempio di un componente React che gestisce un carrello della spesa:

import { useState, useMemo } from 'react';

function ShoppingCart() {
  const [cart, setCart] = useState([]);

  // Command: Add item to cart
  function addItemToCart(item) {
    setCart([...cart, item]);
  }

  // Command: Remove item from cart
  function removeItemFromCart(id) {
    setCart(cart.filter((item) => item.id !== id));
  }

  // Query: Calculate total price of items in cart
  const totalPrice = useMemo(() => {
    return cart.reduce((total, item) => total + item.price, 0);
  }, [cart]);

  return (
    <div>
      <h2>Shopping Cart</h2>
      <ul>
        {cart.map((item) => (
          <li key={item.id}>
            {item.name} - {item.price}
            <button onClick={() => removeItemFromCart(item.id)}>Remove</button>
          </li>
        ))}
      </ul>
      <p>Total Price: {totalPrice}</p>
    </div>
  );
}

Vantaggi della separazione comando-query

La separazione di comandi e query nel codice può fornire i seguenti vantaggi:

  • Chiarezza: separando comandi e query, il codice diventa più facile da leggere e comprendere, poiché è più facile determinare cosa fa ogni metodo e cosa restituisce.
  • Modularità: la separazione di comandi e query può rendere il codice più modulare e riutilizzabile, poiché i comandi e le query possono essere utilizzati in modo indipendente.
  • Testabilità: la separazione di comandi e query può semplificare il test del codice, poiché l'isolamento e il test dei singoli metodi è più semplice.
  • Complessità ridotta: la separazione di comandi e query può aiutare a ridurre la complessità del codice, in quanto incoraggia a suddividere operazioni complesse in parti più piccole e più gestibili.

D'altra parte, il principio introduce un'ulteriore astrazione extra nel codice che può rendere un po' più difficile il debug. L'utilizzo di un meccanismo pub-sub o listener di eventi per separare comandi e query può rendere più difficile eseguire il debug e ragionare sul comportamento del codice, specialmente quando il sito chiamante è lontano dai dati che modifica.

L'utilizzo di un'architettura ben definita e di uno stile di codifica coerente è essenziale per mantenere il codice organizzato e gestibile. È anche importante utilizzare nomi chiari e descrittivi per funzioni e variabili per rendere più comprensibile il loro scopo e comportamento.

Un altro approccio per separare comandi e query in un'applicazione React consiste nell'utilizzare una libreria di gestione dello stato come Redux o MobX. Queste librerie forniscono un archivio centralizzato per lo stato dell'applicazione e offrono un modo chiaro e strutturato per modificare e accedere allo stato da diverse parti dell'applicazione.

In Redux, ad esempio, possiamo definire le azioni come comandi che descrivono una modifica allo stato e i riduttori come query che gestiscono queste azioni e restituiscono un nuovo stato:

// Define actions
const addToCart = (item) => ({
  type: 'ADD_TO_CART',
  payload: item,
});

const removeItemFromCart = (id) => ({
  type: 'REMOVE_FROM_CART',
  payload: id,
});

// Define reducer
const cartReducer = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TO_CART':
      return [...state, action.payload];
    case 'REMOVE_FROM_CART':
      return state.filter((item) => item.id !== action.payload);
    default:
      return state;
  }
};

Conclusione

Il principio di separazione tra comando e query è un principio importante da considerare durante la progettazione del software. La separazione di comandi e query rende il tuo codice più modulare, testabile e più facile da capire. Aderendo a questo principio, è possibile scrivere codice più facile da mantenere e meno soggetto a errori e bug.

In sintesi, separando il codice che modifica lo stato dal codice che restituisce informazioni sullo stato, è possibile creare un codice più modulare e verificabile, più facile da comprendere e gestire. Con una netta separazione tra comandi e query, è possibile ottenere un'architettura software più gestibile e scalabile.

Se ti piace la lettura, per favore Iscriviti alla mia mailing list . Condivido settimanalmente tecniche di Clean Code e Refactoring tramite blog , libri e video .