Object Oriented come programmare coesione e disaccoppiamento

codice

Sei esperto nella progettazione di software Object Oriented?

Sei bravo a programmare con il buon vecchio Object Oriented? E con i componenti come stai messo? Sei sicuro di saper progettare componenti software e di saper individuare perfettamente (o quasi) classi, proprietà, ecc.? Le classi che hai definito sono tutte in forte coesione? In altre parole programming object oriented?

Come dici? Sei stufo di leggere sempre questi argomenti perchè sono cose vecchie che tutti sanno e tutti applicano? Nei sei proprio certo? Benissimo, allora non proseguire nella lettura, non perdere tempo….

Bene, sei ancora qui, allora sento che qualche dubbio ti sta venendo in mente, perfetto, è proprio ciò che voglio da te. Mi interessa che tu ti ponga domande e che legga questo post che ho scritto apposta per te.

In questo articolo ti insegnerò come scrivere classi fortemente coese e disaccoppiate tra loro.

Ora mettiti comodo e inizia a leggere, confronta le tue idee con le mie e se vuoi commenta. Buona lettura!

 

L’arte della Coesione in Object Oriented

Il titolo non è una provocazione piuttosto la dura realtà dei fatti, scrivere (e programmare) software object oriented coeso e disaccoppiato è un’arte e una professione, insomma è una cosa seria.

Il termine coesione deriva dal latino cohaerere e significa essere congiunto, attaccato, stare unito; se lo riferisco al software posso dire che in una classe dovrò inserire tutto ciò che, visto nell’insieme, la renderà unita, coesa e adatta ad offrire un servizio.

La cosa più complessa da fare è proprio saper scrivere classi e, per essere più preciso, sapere cosa mettere in una classe; in realtà la coesione cambia secondo il punto di vista, se dico che un software è coeso significa che le classi da cui è composto sono coese le une con le altre e se dico che una classe è coesa, significa che i suoi metodi e le sue proprietà sono unite tra loro.

Provo a semplificare la questione: se tra le classi Aeroporto, Aereo e Pilota identifico delle responsabilità (metodi) e caratteristiche (proprietà) comuni molto probabilmente significa che devo creare una nuova classe AereoSchedule che le contenga.

 

Metti il codice dove ti aspetti di trovarlo con Object Oriented

In questo modo ho messo in pratica il principio “Metti il codice dove ti aspetti di trovarlo” che trovo eccezionalmente efficace al pari della sua semplice filosofia, quanti di noi hanno aperto il codice scritto da qualcun altro e non ci hanno capito un tubo pur sapendone di codice e quanti, al contrario, ne hanno aperto un altro benedicendo e lodando l’autore perchè hanno trovato “al volo” le parti di codice nei punti in cui si aspettavano di trovarlo? Non c’è nulla di peggiore che trovarsi tra le mani un software funzionante ma con un codice organizzato in maniera illeggibile (poveramente coeso) da manutenere.

Se vi riconoscete in questi due casi significa che il vostro codice object oriented è poco coeso e fortemente accoppiato (di quest’ultimo ne parlerò più avanti):

  • avete una singola classe che dovete modificare in diversi modi per diverse ragioni; è il classico esempio di bassa coesione dove occorre effettuare refactoring, identificare meglio le responsabilità, estrarle ed inserirle in una nuova classe;
  • avete un metodo della classe A che contiene troppa conoscenza dei field della classe B; è il classico esempio di forte accoppiamento delle due classi, occorre spostare le funzionalità della classe A che “conoscono” la classe B.

Lasciare il codice inalterato nelle suddette condizioni vi condurrà, con alta probabilità, ad incrementare la difficoltà: alla comprensione delle classi e dei componenti, alla manutenzione del sistema e, soprattutto, al riuso delle classi in altri contesti e applicazioni.

Ti consiglio di utilizzare, se ti è possibile, due pattern di tipo GRASP, il Controller e il Creator, poichè aiutano a garantire un’alta coesione tra classi.

 

Il Controller

In un’ottica di architettura a n-layer il Controller è un componente situato tra il livello Presentation e il Business e può rappresentare un caso d’uso oppure il sistema stesso.

Generalmente un Controller non fa grandi cose, gestisce messaggi e decide a chi reindirizzarli; quando questi ultimi sono numerosi, non relazionati ad un oggetto di destinazione e provengono dalla user interface, il controller è di tipo use case, altrimenti è un facade.

Questo pattern aiuta ad implementare l’alta coesione indirizzando specifici messaggi a specifici metodi; in tal modo i messaggi sono ben relazionati e focalizzati e hanno, come destinatario, un metodo ben preciso e coeso in maniera corretta.

Il Creator

Il pattern Creator risolve il problema di chi dovrebbe essere responsabile della creazione dell’istanza di una classe; anch’esso, quindi, favorisce l’alta coesione, e nello stesso tempo, rende povero l’accoppiamento tra le classi dato che ciascuna, essendo pienamente responsabile di creare determinate istanze, non necessita delle altre.

L’accoppiamento è la scrittura di classi in cui metodi dell’una hanno conoscenza dei metodi dell’altra; la conseguenza maggiormente catastrofica di tale pratica avviene in caso di modifica e di comprensione del design delle classi, a seguire ecco qualche suggerimento.

Una delle prime attività da compiere è la eliminazione della conoscenza che un metodo di una classe ha nei confronti di un’altra; ecco un breve esempio di come NON programmare coesione nel codice:


public void GetSomething()
{
   string connString = getConnString();
   SqlConnection conn = new SqlConnection(connString);
   DataServer1 srv = new DataServer1(conn);
   int recNumber = 10;

   using (SqlDataReader reader = srv.GetWorkItemData(recNumber))
   {
      while (reader.Read())
      {
         string surname = reader.GetString(0);
         string address = reader.GetString(1);
         DoSomething(surname, address);
      }
   }
}

a prima vista sembrerebbe un ottimo esempio di implementazione di un metodo, osserva ora la seconda versione ripulita:

public void GetSomething()
{ 
   DataServer2 srv = new DataServer2();
   foreach (DataItem item in srv.GetWorkItemData(10))
   { 
      DoSomething(item);
   } 
}

nella seconda versione sono stati rimossi i riferimenti che il metodo possedeva relativamente a come accedere al db, ciò riduce la complessità, mette maggiore ordine e disaccoppia il metodo da quelli di ADO.NET.

Ecco un ottimo esempio di come programmare coesione e disaccoppiamento! in object oriented programming

Chi vuole accedere al GetSomething (prima versione) deve creare istanze degli oggetti per la connessione a SQL e capire la struttura del DataReader restituito.

Viceversa chi vuole accedere alla seconda versione non deve fare altro che invocare un metodo che restituisce un array di oggetti tipizzati.

Infine, nella prima versione l’eventuale modifica al modo di lavorare di DataServer1 potrebbe impattare sul metodo, nella seconda versione DataServer2 potrebbe riferirsi a un tipo diverso di RDBMS senza intaccare minimamente il metodo GetSomething.

Bene, proseguiamo.

 

Non parlare con gli estranei ma solo con chi conosci (object oriented programming)

Esiste un modo per capire in tempo utile se il codice object oriented che scriviamo in un metodo può portare potenziali problemi, si chiama “Non parlare con gli estranei ma solo con chi conosci“, a seguire l’esempio:

public interface DataService
{
   Orders[] FindOrders(Customer customer);
}

public class Repository
{
   public DataService InnerService { get; set; }
}

public class UseOrders
{
   private Repository _repository;

   public UseOrders(Repository repository) 
   {
      _repository = repository;
   }

   public void DoOrders(Customer customer) 
   {
      // La seguente riga viola la legge di Demetra
      Orders[] orders = _repository.InnerService.FindOrders(customer);
   }
}

Nel suddetto codice la classe DoOrders deve avere informazioni su Orders e ha un riferimento alla classe Repository la quale possiede un oggetto DataService; DoOrders attraversa Repository, mediante DataService, per invocare Repository.FindOrders e farsi restituire i dati desiderati.

In tal modo DoOrders invoca un metodo di un’altra classe tramite una sua proprietà, ciò vìola la legge di Demetra e, più in generale, non favorisce il disaccoppiamento poichè chi consuma Repository è fortemente legato alla sua implementazione; a seguire, invece, l’esempio “buono”:

public class Repository
{
   private DataService _service;

   public Repository(DataService service) 
   {
      _service = service;
   }
   public Orders[] FindOrders(Customer customer)
   {    
      return _service.FindOrders(customer);
   }
}

public class UseOrders
{
   private Repository _repository;

   public UseOrders(Repository repository)
   {
      _repository = repository;
   }

   public void DoSomething(Customer customer) 
   {
      // Ora siamo nei confini della legge...
      Orders[] orders = _repository.FindOrders(customer);
   }
}

Questa volta si invoca direttamente FindOrders grazie alla ridotta complessità di Repository; ed è possibile effettuare modifiche a questa classe senza preoccuparsi della classe consumer poichè debolmente accoppiata.

 

Dillo, non chiedere (Object Oriented Programming)

Un altro modo per rendere fortemente disaccoppiate due classi è di renderle autonome a compiere azioni con messaggi che ricevono o con le proprie informazioni ma non con metodi di altre classi; sto parlando del principio del “Dillo, non chiedere“, a seguire un breve esempio:

public class Purchase
{
   public double SubTotal { get; set; }
   public double Discount { get; set; }
   public double Total { get; set; }
}

public class Account 
{
   public double Balance { get; set;}
}

public class UseEntities 
{
   public void MakePurchase(Purchase purchase, Account account) 
   {
      purchase.Discount = purchase.SubTotal > 5000 ? .20 : 0;
      purchase.Total = purchase.SubTotal*(1 - purchase.Discount);

      if (purchase.Total < account.Balance) 
      {
         account.Balance -= purchase.Total;
      }
      else
      {
         rejectPurchase(purchase, "Sorry. Non hai denaro sufficiente.");
      }
   }
} 

Le classi Purchase e Account, come noterete, non hanno business logic che è invece innaturalmente posta nella classe UseEntities, ora il “buon” esempio:

public class Purchase
{
   private readonly double _subTotal;

   public Purchase(double subTotal) 
   {
      _subTotal = subTotal;
   }

   public double Total 
   {
      get 
      {
         double discount = _subTotal > 5000 ? .20 : 0;
         return _subTotal*(1 - discount);
      }
   }
}

public class Account 
{
   private double _balance;

   public void Deduct(Purchase purchase, PurchaseMessenger messenger) 
   {
      if (purchase.Total < _balance) 
      {
         _balance -= purchase.Total;
      }
      else
      {
         messenger.RejectPurchase(purchase, this);
      }
   }
}

public class UseEntities 
{
   public void MakePurchase(Purchase purchase, Account account) 
   {
      PurchaseMessenger messenger = new PurchaseMessenger();
      account.Deduct(purchase, messenger);
   }
}

Stavolta il codice della business rule è giustamente posizionato nelle classi Purchase e Account le quali sanno esattamente cosa fare, come fare e non devono chiedere nulla alle altre.

Inoltre si riduce drasticamente la complessità, l’accoppiamento e la potenziale duplicazione del codice grazie al fatto che la logica, essendo interna alle due classi, può essere riutilizzatata facilmente attraverso il sistema.

 

Conclusioni programming object oriented

Concludo questo articolo su un aspetto dell’object oriented programming suggerendo alcune domande da porti. In tal modo comprenderai meglio se il design che stai  effettuando sulle classi è ad un livello non ancora sufficientemente alto per ritenerti soddisfatto:

  • Il nome che hai dato alla classe è generico? Se non implementa un solo ruolo significa che gli stai facendo fare troppe cose;
  • Il nome che hai dato alla classe è semplice? Se il nome è complicato significa che la classe è troppo complessa;
  • La tua classe è piena zeppa di metodi? Significa che sta facendo cose diverse oltre quella principale, cerca di suddividerla in più classi;
  • Il tuo metodo accetta una marea di parametri di input? Significa che hai perso poco tempo a progettarlo, e probabilmente, al suo interno vi è una struttura condizionale ad albero. Molto meglio suddividerlo in più metodi.

Benissimo, ora dovresti sapere tutto su come programmare coesione e disaccoppiamento, se ti e’ piaciuto questo articolo ti invito caldamente a condividerlo con i tuoi amici e a commentarlo. Inoltre, se vuoi approfondire maggiormente l’argomento dai una occhiata a questa recensione e a questo link.

Infine, se desideri, acquista il seguente libro che ti consiglio vivamente di leggere; basta che clicchi sulla sua immagine o sul link qui sotto. Grazie per la tua attenzione!

 

Acquista il libro Design patterns

 

🔥375 volte è stato letto questo articolo
Se ti è piaciuto questo articolo, lascia le tue stelle!

1 Comment

  • CARLO DE GIORGI

    28 Agosto 2018 - 10:22

    Veramente interessante!
    sono per la filosofia:
    “Metti il codice dove ti aspetti di trovarlo”

    Ottima lettura. grazie interessante

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *