Encapsulation

L'encapsulation consiste nel:

  • conservare nello stesso oggetto dati e comportamenti correlati
  • impedire al codice esterno di modificare liberamente lo stato interno
  • esporre soltanto le operazioni pubbliche necessarie

In PHP si realizza principalmente con la visibilità di proprietà e metodi: public, private e protected.

Esempio senza encapsulation:

<?php

class BankAccount
{
    public float $balance = 0;
}

$account = new BankAccount();

// il problema è che chiunque può modificare la proprietà della classe
$account->balance = -10000;
?>

Esempio con encapsulation:

<?php

class BankAccount
{
    public function __construct(
        private float $balance = 0,
    ) {
        if ($balance < 0) {
            throw new InvalidArgumentException(
                'Il saldo iniziale non può essere negativo.',
            );
        }
    }

    public function deposit(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException(
                'L’importo deve essere maggiore di zero.',
            );
        }

        $this->balance += $amount;
    }

    public function withdraw(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException(
                'L’importo deve essere maggiore di zero.',
            );
        }

        if ($amount > $this->balance) {
            throw new RuntimeException('Saldo insufficiente.');
        }

        $this->balance -= $amount;
    }

    public function getBalance(): float
    {
        return $this->balance;
    }
}

//uso in un altro file
$account = new BankAccount(100);

$account->deposit(50);
$account->withdraw(30);

echo $account->getBalance(); // 120

//questo non è possibile farlo perchè $balance nella classe è private
$account->balance = -500;

?>

Abstraction

L'abstraction consiste nel rappresentare soltanto le caratteristiche essenziali di un concetto, nascondendo i dettagli implementativi non necessari a chi lo utilizza. Per esempio, chi utilizza un sistema di pagamento deve sapere che può eseguire un metodo pay(100); ma non dovrebbe necessariamente sapere cosa avviene all'interno del metodo.

In PHP l'abstraction si esprime attraverso interfacce, classi astratte e metodi pubblici che nascondono implementazioni private.

Interfacce

L'interfaccia prescrive soltanto che qualunque gateway di pagamento deve avere un metodo pay() che riceve un importo e restituisce un PaymentResult ma non stabilisce come debba essere effettuato il pagamento.

I metodi delle interfacce devono essere pubblici.

<?php

interface PaymentGatewayInterface
{
    public function pay(float $amount): PaymentResult;
}

?>

Quindi le classi come ad esempio StripePaymentGateway e PayPalPaymentGateway implementano l'interfaccia PaymentGatewayInterface e devono esporre il metodo pay() che ritorna un PaymentResult, ma le altre classi non devono sapere i dettagli interni dei due metodi.

Classi Astratte

Una calasse astratta può offrire una parte comune dell'implementazione e poi le classi concrete che estendono la classe astratta definiscono i dettagli che possono essere differenti per ogni sottoclasse.

Non è possibile creare direttamente un'istanza di una classe astratta, inoltre una sottoclasse concreta deve implementare i metodi astratti ereditati rispettandone la firma.

I metodi astratti non hanno un'implementazione, sono solo definiti nella classe astratta ma poi le classi concrete che estendono la classe astratta devono decidere loro come implementare i metodi.

Quando usare un'interfaccia e quando usare una classe astratta?

Una stessa interfaccia può essere usata da classi non correlate tra loro, inoltre una classe può implementare più interfacce.

Inheritance

L'ereditarietà permette a una classe figlia di ricevere proprietà, metodi e costanti public e protected da una classe padre (i private no, quelli rimangono solo nella classe in cui sono definiti). Ogni classe figlia può estendere una sola classe padre (ma può implementare più interfacce).

La classe figlia può:

  • utilizzare il comportamento ereditato
  • aggiungere nuovi comportamenti
  • ridefinire, cioè fare override, di alcuni metodi

Per riferirsi alla classe padre si usa la parola chiave parent.

Nella classe figlia si può anche non dichiarare un costruttore, verrà utilizzato quello della classe padre. Se si dichiara il costruttore nella classe figlia quello della classe padre non verrà eseguito, se si vuole farlo bisogna usare la sintassi parent::__construct(arguments).

Traits

Uno svantaggio dell'ereditarietà è che le classi sono strettamente dipendenti l'una dall'altra, quindi da PHP 5.4 sono stati introdotti i Traits, un costrutto simile alla classe ma che serve solo per raggruppare metodi.

Per usare un Trait in una classe si importa usando la parla chiave use, tutti i metodi del Trait sono disponibili nella classe in cui è stato importato come fossero metodi dell'oggetto (quindi $this->metodoDelTrait();).

Dato che una classe può usare più Traits possono verificarsi conflitti di nomenclatura se due Trait hanno un metodo con lo stesso nome, quindi quando si importano con la parola chiave use si può assegnare un alias.

<?php

class Logger
{
	use FileLogger, DatabaseLogger{
		DatabaseLogger::log as logToDatabase;
		FileLogger::log insteadof DatabaseLogger;
	}
}

$logger = new Logger();
$logger->log('this is a test message #1');
$logger->logToDatabase('this is a test message #2');

?>

@TODO fare approfondimento su Composition

@TODO fare approfondimento su Liskov Substitution Principle

Override

Una classe figlia può definire la propria versione di un metodo della classe padre definendo un metodo con lo stesso nome, parametri e return type. Per richiamare dalla classe figlia il metodo definito nella lasse padre si usa lo scope resolution operator : parent::nomeMetodo().

Se il metodo della classe padre è stato definito con la parola chiave final la classe figlia non può fare l'override.

L'overloading non esiste nativamente.

Late static binding

Se si definisce un metodo in una classe padre e lo si richiama in una sottoclasse, il metodo non fa riferimento alla sottoclasse. Per risolvere questo problema nella classe padre non usare self ma static.

Esempio senza late static binding:

<?php

class Model
{
	protected static $tableName = 'Model';

	public static function getTableName()
	{
		return self::$tableName;
	}
}

class User extends Model
{
	protected static $tableName = 'User';
}

//qui viene stampato Model e non User, perchè nel metodo getTableName() la parola chiave self si riferisce alla classe
//in cui è definito il metodo
echo User::getTableName(); 

Esempio con late static binding:

<?php

class Model
{
	protected static $tableName = 'Model';

	public static function getTableName()
	{
		return static::$tableName; //qui invece di self mettiamo static
	}
}

class User extends Model
{
	protected static $tableName = 'User';
}

//qui verrà stampato User perchè nel metodo la parola chiave static indica la classe a run-time
echo User::getTableName(); // User

?>

Polymorphism

Il polimorfismo permette a uno stesso codice di lavorare con implementazioni differenti attraverso un contratto comune. Le interfacce permettono proprio di usare oggetti appartenenti a classi differenti in modo intercambiabile, purché implementino lo stesso contratto.

<?php

//definisco una classe astratta con il metodo greet() (ma si può fare anche con un'interfaccia)
abstract class Person
{
	abstract public function greet();
}

//ogni classe figlia ha la propria implementazione del metodo greet()
class English extends Person
{
	public function greet()
	{
		return 'Hello!';
	}
}

class German extends Person
{
	public function greet()
	{
		return 'Hallo!';
	}
}

class French extends Person
{
	public function greet()
	{
		return 'Bonjour!';
	}
}

//la funzione greeting() accetta un array
function greeting($people)
{
	foreach ($people as $person) {
		echo $person->greet() . '<br>';
	}
}

//creo un array di persone, sono 3 oggetti di classi differenti ma hanno tutte il metodo greet()
$people = [
	new English(),
	new German(),
	new French()
];

//la funzione greeting funziona su oggetti di classi diverse, basta che abbiano il metodo greet()
greeting($people);

?>