Oggi parliamo di due principi chiave di una buona programmazione ad oggetti: Coupling e Cohesion. Affrontare nel modo giusto questi due concetti ci aiuta a scrivere codice professionale e robusto.
Cosa si intende per Coupling?
Il coupling, ovvero "accoppiamento", indica la dipendenza di una classe da altre. Piu che dipendenza, si potrebbe parlare di "conoscenza". Ecco un esempio pratico:
class RisultatoService
{
public function process($data)
{
$db = new MySQLConnection();
$mailer = new SmtpMailer();
$logger = new FileLogger();
$db->save($data);
$mailer->send('Ecco il tabellino della partita');
$logger->log('Processato dato della partita');
}
}
Questa classe "conosce" fin troppo delle altre. Se un domani il database anzichè MySql diventasse un PostgreSQL? Se domani la classe "SmtpMailer" venisse deprecata e non più aggiornata e volessimo rimpiazzarla? Non ci potremmo limitare a sostituire queste classi, ma dovremmo ricercare nel nostro codice tutte le altre che la utilizzano.
Come risolviamo il problema del Coupling?
Riscriviamo la classe precedente in questo modo:
class RisultatoService
{
public function __construct
(
private RisultatoRepository $repository,
private MailerInterface $mailer,
private LoggerInterface $logger
) {}
public function process(array $data): void
{
$this->repository->save($data);
$this->mailer->send("Ecco il tabellino della partita");
$this->logger->log("Processato dato della partita");
}
}
Uno scenario decisamente diverso. L'utlizzo di interfaccie ci consente di sostituire facilmente le dipendenze ed evita di portar dietro delle classi concrete. Oltre alla flessibilità diventa più semplice anche il lavoro di test.
Cosa è invece la Cohesion?
Traduzione facilissima, la coesione indica la specializzazione di una classe, ovvero la sua capacità di far fronte ad un compito chiaro e coerente.
Prendiamo un esempio dal mondo del fantacalcio:
class UserManager
{
public function createUser() {}
public function sendEmail() {}
public function setTeam() {}
public function setPoints() {}
public function printBadge() {}
}
Una classe 'tuttofare' che esegue calcoli, invia email, genera pdf. Proviamo a separare i compiti:
class UserService
{
public function createUser(array $data): User {}
}
class MailService
{
public function sendEmail(User $user): void {}
}
class PdfGenerator
{
public function printBadge(User $user): void {}
}
Separare i compiti aumenta la coesione delle classi. Ogni oggetto si occupa di una sola responsabilità e diventa più semplice da comprendere, testare e modificare. Separare le responsabilità aumenta la coesione della singola classe. Inoltre, centralizzando l'invio delle email in un unico servizio, riduciamo l'accoppiamento tra il resto dell'applicazione e la libreria utilizzata per la spedizione dei messaggi.
Basso coupling, alta cohesion: oggi il software funziona cosi?
A questo proposito va citato l'esempio di Magento 2, dove si è intrapresa una strada interessante, quella di favorire controller molto piccoli e specializzati, spesso dedicati a una singola operazione.
Per cui è facile imbattersi in componenti con tanti controller, tutti molto leggeri, basati su un costruttore e un metodo execute(). Un controller che si limita a intercettare la richiesta http, validare i dati essenziali della richiesta, delegare la logica applicativa a servizi dedicati e restituire una risposta, magari attraverso un service Json.
Sono passati i tempi in cui trovavamo funzioni come questa:
public function execute()
{
$data = $this->getRequest()->getPostValue();
$customer = new Customer();
$customer->setName($data['name']);
$this->connection->insert(...);
$this->mailer->send(...);
$this->logger->log(...);
// altre 200 righe
}
oggi Magento, tanto per citarne uno, segue strade come questa:
public function execute()
{
$this->customerRegistrationService->register(
$this->getRequest()->getPostValue()
);
}
dove la logica è tutta altrove.
Come mi accorgo di usare codice sbagliato?
Posto che codice con alto coupling e bassa coesione può comunque funzionare benissimo, specie se si tratta di un applicativo che nel corso del tempo non è soggetto a modifiche, ci possiamo accorgere facilmente di aver intrapreso strade sbagliate grazie ad alcuni sintomi:
- Dentro le classi troviamo frequenti istruzioni new per creare dipendenze applicative (repository, mailer, gateway, logger): significa che stiamo istanziando altre classi, con un alto coupling
- Eccesso di dipendenze interne
- Presenza di classi chiamate "manager", "service", "Helper" che anzichè fungere da servizi specializzati, contengono svariate ed eterogenee funzioni con compiti diversi
- File/classi grandi sono di solito sintomo di bassa coesione