JavaRush /Java Blog /Random-IT /Pausa caffè #90. I 4 pilastri della programmazione orient...

Pausa caffè #90. I 4 pilastri della programmazione orientata agli oggetti

Pubblicato nel gruppo Random-IT
Fonte: The Geek Asian Diamo un'occhiata ai quattro fondamenti della programmazione orientata agli oggetti e proviamo a capire come funzionano. La programmazione orientata agli oggetti (OOP) è uno dei principali paradigmi di programmazione. Può essere facile e semplice o, al contrario, molto complesso. Tutto dipende da come decidi di sviluppare la tua applicazione. Pausa caffè #90.  I 4 pilastri della programmazione orientata agli oggetti - 1I pilastri dell’OOP sono 4:
  1. Incapsulamento.
  2. Eredità.
  3. Astrazione.
  4. Polimorfismo.
Discuteremo ora ciascuno di essi con una breve spiegazione e un esempio di codice reale.

1. Incapsulamento

Abbiamo tutti studiato l'incapsulamento come nascondere elementi di dati e consentire agli utenti di accedere ai dati utilizzando metodi pubblici. Chiamiamo questi getter e setter. Ora dimentichiamolo e troviamo una definizione più semplice. L'incapsulamento è un metodo per impedire all'utente di modificare direttamente i membri dei dati o le variabili della classe per mantenere l'integrità dei dati. Come facciamo questo? Limitiamo l'accesso alle variabili impostando il modificatore di accesso su privato ed esponendo metodi pubblici che possono essere utilizzati per accedere ai dati. Diamo un'occhiata agli esempi specifici di seguito. Questo ci aiuterà a capire come possiamo utilizzare l'incapsulamento per mantenere l'integrità dei dati. Senza incapsulamento:
/**
 * @author thegeekyasian.com
 */
public class Account {

  public double balance;

  public static void main(String[] args) {

  	Account theGeekyAsianAccount = new Account();

  	theGeekyAsianAccount.balance = -54;
  }
}
Nello snippet di codice riportato sopra, il metodo main() accede direttamente alla variabile balance . Ciò consente all'utente di impostare qualsiasi valore doppio nella variabile saldo della classe Conto . Possiamo perdere l'integrità dei dati consentendo a chiunque di impostare il saldo su qualsiasi numero non valido, come -54 in questo caso. Con incapsulamento:
/**
 * @author thegeekyasian.com
 */
public class Account {

  private double balance;

  public void setBalance(double balance) {

    if(balance >= 0) { // Validating input data in order to maintain data integrity
	  this.balance = balance;
    }

    throw new IllegalArgumentException("Balance cannot be less than zero (0)");
  }

  public static void main(String[] args) {

  	Account theGeekyAsianAccount = new Account();

  	theGeekyAsianAccount.setBalance(1); // Valid input - Allowed
  	theGeekyAsianAccount.setBalance(-55); // Stops user and throws exception
  }
}
In questo codice abbiamo limitato l'accesso alla variabile balance e aggiunto un metodo setBalance() che consente agli utenti di impostare il valore del saldo per Account . Il setter controlla il valore fornito prima di assegnarlo alla variabile. Se il valore è inferiore a zero, viene generata un'eccezione. Ciò garantisce che l'integrità dei dati non venga compromessa. Dopo aver spiegato gli esempi precedenti, spero che il valore dell'incapsulamento come uno dei quattro pilastri dell'OOP sia chiaro.

2. Eredità

L'ereditarietà è un metodo per ottenere proprietà di un'altra classe che condivide caratteristiche comuni. Ciò ci consente di aumentare la riusabilità e ridurre la duplicazione del codice. Il metodo segue anche il principio dell'interazione figlio-genitore, quando un elemento figlio eredita le proprietà del suo genitore. Analizziamo due rapidi esempi e vediamo come l'ereditarietà rende il codice più semplice e più riutilizzabile. Senza eredità:
/**
 * @author thegeekyasian
 */
public class Rectangle {

  private int width;
  private int height;

  public Rectangle(int width, int height) {
	this.width = width;
	this.height = height;
  }

  public int getArea() {
	return width * height;
  }
}

public class Square {

  private int width; // Duplicate property, also used in class Rectangle

  public Square(int width) {
	this.width = width;
  }

  public int getArea() { // Duplicate method, similar to the class Rectangle
	return this.width * this.width;
  }
}
Le due classi simili condividono le proprietà width e il metodo getArea() . Possiamo aumentare il riutilizzo del codice eseguendo un piccolo refactoring in cui la classe Square finisce per ereditare dalla classe Rectangle . Con eredità:
/**
 * @author thegeekyasian
 */
public class Rectangle {

  private int width;
  private int height;

  public Rectangle(int width, int height) {
	this.width = width;
	this.height = height;
  }

  public int getArea() {
	return width * height;
  }
}

public class Square extends Rectangle {

  public Square(int width) {
	super(width, width); // A rectangle with the same height as width is a square
  }
}
Estendendo semplicemente la classe Rectangle , otteniamo la classe Square come tipo Rectangle . Ciò significa che eredita tutte le proprietà comuni a Square e Rectangle . Negli esempi precedenti vediamo come l'ereditarietà gioca un ruolo importante nel rendere il codice riutilizzabile. Consente inoltre a una classe di ereditare il comportamento della sua classe genitore.

3. Astrazione

L'astrazione è una tecnica che presenta all'utente solo i dettagli essenziali nascondendo i dettagli non necessari o irrilevanti di un oggetto. Aiuta a ridurre la complessità operativa lato utente. L'astrazione ci consente di fornire un'interfaccia semplice all'utente senza richiedere dettagli complessi per eseguire un'azione. In poche parole, dà all'utente la possibilità di guidare un'auto senza dover capire esattamente come funziona il motore. Diamo prima un'occhiata a un esempio e poi discutiamo di come l'astrazione ci aiuta.
/**
* @author thegeekyasian.com
*/
public class Car {

  public void lock() {}
  public void unlock() {}

  public void startCar() {

	checkFuel();
	checkBattery();
	whatHappensWhenTheCarStarts();
  }

  private void checkFuel() {
	// Check fuel level
  }

  private void checkBattery() {
	// Check car battery
  }

  private void whatHappensWhenTheCarStarts() {
	// Magic happens here
  }
}
Nel codice precedente, i metodi lock() , sblocca() e startCar() sono pubblici e il resto è privato per la classe. Abbiamo reso più semplice per l’utente “guidare l’auto”. Naturalmente, potrebbe controllare manualmente checkFuel() e checkBattery() prima di avviare l'auto con startCar() , ma ciò complicherebbe solo il processo. Con il codice sopra, tutto ciò che l'utente deve fare è utilizzare startCar() e la classe si occuperà del resto. Questo è ciò che chiamiamo astrazione.

4. Polimorfismo

L'ultimo e il più importante dei quattro pilastri dell'OOP è il polimorfismo. Polimorfismo significa “molte forme”. Come suggerisce il nome, si tratta di una funzione che consente di eseguire un'azione in più o diversi modi. Quando parliamo di polimorfismo, non c'è molto da discutere se non parliamo dei suoi tipi. Esistono due tipi di polimorfismo:
  1. Overloading del metodo - polimorfismo statico (Static Binding).
  2. Metodo overriding: polimorfismo dinamico (Dynamic Binding).
Discutiamo ciascuno di questi tipi e vediamo qual è la differenza tra loro.

Sovraccarico del metodo - polimorfismo statico:

L'overload del metodo o il polimorfismo statico, noto anche come associazione statica o associazione in fase di compilazione, è un tipo in cui le chiamate al metodo vengono determinate in fase di compilazione. L'overload dei metodi ci consente di avere più metodi con lo stesso nome, con tipi di dati di parametri diversi o numeri diversi di parametri o entrambi. Ma la domanda è: perché è utile l'overload del metodo (o polimorfismo statico)? Diamo un'occhiata agli esempi seguenti per comprendere meglio l'overload del metodo. Senza sovraccarico del metodo:
/**
* @author thegeekyasian.com
*/
public class Number {

  public void sumInt(int a, int b) {
	System.out.println("Sum: " + (a + b));
  }

  public void sumDouble(double a, double b) {
	System.out.println("Sum: " + (a + b));
  }

  public static void main(String[] args) {

	Number number = new Number();

	number.sumInt(1, 2);
	number.sumDouble(1.8, 2.5);
  }
}
Nell'esempio sopra abbiamo creato due metodi con nomi diversi, solo per aggiungere due diversi tipi di numeri. Se continuiamo con un'implementazione simile, avremo più metodi con nomi diversi. Ciò ridurrà la qualità e la disponibilità del codice. Per migliorare ciò, possiamo utilizzare l'overloading dei metodi utilizzando lo stesso nome per metodi diversi. Ciò consentirà all'utente di avere un'opzione come punto di ingresso per sommare diversi tipi di numeri. L'overload dei metodi funziona quando due o più metodi hanno lo stesso nome ma parametri diversi. Il tipo di reso può essere lo stesso o diverso. Ma se due metodi hanno lo stesso nome, gli stessi parametri, ma tipi di ritorno diversi, ciò causerà un sovraccarico e un errore di compilazione! Con l'overload del metodo:
/**
* @author thegeekyasian.com
*/
public class Number {

  public void sum(int a, int b) {
	System.out.println("Sum: " + (a + b));
  }

  public void sum(double a, double b) {
	System.out.println("Sum: " + (a + b));
  }

  public static void main(String[] args) {

	Number number = new Number();

	number.sum(1, 2);
	number.sum(1.8, 2.5);
  }
}
Nello stesso codice, con alcune piccole modifiche, siamo riusciti a sovraccaricare entrambi i metodi, rendendo i nomi uguali per entrambi. L'utente può ora specificare i propri tipi di dati specifici come parametri del metodo. Eseguirà quindi un'azione in base al tipo di dati fornito. Questa associazione del metodo viene eseguita in fase di compilazione perché il compilatore sa quale metodo verrà chiamato con il tipo di parametro specificato. Ecco perché lo chiamiamo associazione in fase di compilazione.

Metodo override - polimorfismo dinamico:

A differenza dell'overloading dei metodi, l'override dei metodi consente di avere esattamente la stessa firma di più metodi, ma devono trovarsi in più classi diverse. La domanda è: cosa c'è di così speciale? Queste classi hanno una relazione IS-A, ovvero devono ereditare l'una dall'altra. In altre parole, nell'override del metodo o nel polimorfismo dinamico, i metodi vengono elaborati dinamicamente in fase di esecuzione quando viene chiamato il metodo. Questo viene fatto in base al riferimento all'oggetto con cui viene inizializzato. Ecco un piccolo esempio di override del metodo:
/**
* @author thegeekyasian.com
*/
public class Animal {

  public void walk() {
	System.out.println("Animal walks");
  }
}

public class Cat extends Animal {

  @Override
  public void walk() {
	System.out.println("Cat walks");
  }
}

public class Dog extends Animal {

  @Override
  public void walk() {
	System.out.println("Dog walks");
  }
}

public class Main {

  public static void main(String[] args) {

	Animal animal = new Animal();
	animal.walk(); // Animal walks

	Cat cat = new Cat();
	cat.walk(); // Cat walks

	Dog dog = new Dog();
	dog.walk(); // Dog walks

	Animal animalCat = new Cat(); // Dynamic Polymorphism
	animalCat.walk(); // Cat walks

	Animal animalDog = new Dog(); // Dynamic Polymorphism
	animalDog.walk(); //Dog walks
  }
}
In questo esempio di sostituzione, abbiamo assegnato dinamicamente gli oggetti di tipo "Cane" e "Gatto" al tipo "Animale". Questo ci consente di chiamare dinamicamente il metodo walk() sulle istanze a cui si fa riferimento in fase di runtime. Possiamo farlo utilizzando il metodo overriding (o polimorfismo dinamico). Questo conclude la nostra breve discussione sui quattro pilastri dell'OOP e spero che la troverai utile.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION