C#
OOP in C#: gli esempi che finalmente hanno fatto click
Dog, Animal, e Bark() non sono mai rimasti. Encapsulation, abstraction, inheritance, e polymorphism hanno finalmente fatto click quando li ho visti in una bank account, in un notification service, e in un payment processor invece.
Ogni tutorial di OOP con cui ho iniziato usava gli stessi esempi da libro di testo: un Dog che eredita da Animal e chiama Bark(), una Car che eredita da Vehicle e chiama Drive(). Niente di tutto ciò è rimasto, perché io non costruisco canili né concessionarie d'auto. Costruisco flussi di pagamento e integrazioni di API. I quattro pilastri hanno davvero fatto click solo quando li ho visti in codice che assomigliava a quello che scrivo davvero.
Encapsulation: BankAccount
La versione che non ha fatto click era un campo pubblico che chiunque poteva impostare direttamente:
public class BankAccount
{
public decimal Balance;
}
Niente impedisce a qualsiasi codice nella solution di impostare Balance a un numero negativo. Ciò che ha fatto scattare l'encapsulation non è stato "nascondi il campo", è stato rendermi conto che l'object dovrebbe essere l'unica cosa autorizzata a cambiare il proprio stato, e solo tramite regole che impone da sé:
public class BankAccount
{
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive.");
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount > Balance)
throw new InvalidOperationException("Insufficient funds.");
Balance -= amount;
}
}
Abstraction: un notification service
L'abstraction ha fatto click quando ho smesso di pensarla come "fai un'interfaccia" e ho iniziato a pensarla come nascondere il casino di cui chi chiama non dovrebbe doversi preoccupare:
public interface INotificationService
{
Task SendSmsAsync(string phoneNumber, string message);
}
public class TwilioNotificationService : INotificationService
{
private readonly HttpClient _httpClient;
public async Task SendSmsAsync(string phoneNumber, string message)
{
// formattazione del payload HTTP, firma dell'auth header, gestione errori
}
}
Qualunque cosa chiami SendSmsAsync non ha bisogno di sapere nulla sugli status code HTTP o sulla firma degli header. Quel divario tra il contratto semplice e l'implementazione disordinata dietro di esso è ciò che l'abstraction ti dà davvero.
Inheritance: un template di payment processor
Le catene profonde di inheritance non mi sono mai sembrate sensate finché non ho visto l'inheritance usata per un workflow condiviso invece che per una tassonomia condivisa:
public abstract class PaymentProcessor
{
public async Task ProcessTransactionAsync(decimal amount)
{
ValidateAmount(amount);
await ExecutePaymentAsync(amount);
LogReceipt();
}
private void ValidateAmount(decimal amount) { /* logica condivisa */ }
private void LogReceipt() { /* logica condivisa */ }
protected abstract Task ExecutePaymentAsync(decimal amount);
}
public class StripePaymentProcessor : PaymentProcessor
{
protected override async Task ExecutePaymentAsync(decimal amount)
{
// logica di integrazione con l'API di Stripe
}
}
La base class possiede il processo. Ogni subclass sovrascrive solo l'unico passo che è davvero diverso. È un impegno molto più piccolo delle catene Dog -> Mammal -> Animal che continuavo a vedere nei tutorial.
Polymorphism: sostituire payment provider
L'esempio che ha fatto scattare il polymorphism è stato sostituire una catena crescente di if/else con un'unica abstraction:
public class CheckoutService
{
private readonly PaymentProcessor _paymentProcessor;
public CheckoutService(PaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public async Task CompleteOrderAsync(Order order)
{
await _paymentProcessor.ProcessTransactionAsync(order.TotalAmount);
}
}
CheckoutService non sa e non gli importa quale subclass concreta di PaymentProcessor abbia ricevuto, StripePaymentProcessor o qualsiasi cosa aggiunta dopo. Aggiungere un nuovo metodo di pagamento significa scrivere una nuova subclass, non modificare uno switch statement sepolto da qualche altra parte nel codebase.
Cosa è cambiato davvero
Ciò che ha fatto attaccare questi quattro concetti è che sono tutte cose che scriverei davvero. Il concetto ha finalmente avuto un posto reale a cui aggrapparsi, invece di stare sopra una classe Dog che non avrei mai più usato.