Il potere degli extension method in Dart
Funzionalità avanzate del linguaggio per aumentare la leggibilità del codice
Le extension
in Dart sono una caratteristica del linguaggio che consente di aggiungere nuovi metodi getter/setter a classi esistenti senza dover sottoporre il codice sorgente della classe originale a modifiche. Sono dichiarate utilizzando la parola chiave extension
seguita dal nome dell'estensione ed al loro interno si possono definire nuovi metodi che operano sull’istanza della classe stessa.
Perché sono necessarie?
Quando bisogna implementare un nuovo metodo per una classe solitamente si aggiunge ad esso una funzione statica, ma se non si ha accesso a tale classe poiché parte di un package importato si può utilizzare un’extension
. Dalla documentazione Flutter:
When you’re using someone else’s API or when you implement a library that’s widely used, it’s often impractical or impossible to change the API. But you might still want to add some functionality.
Questo flusso permette di mantenere genuina la classe originale ed al contempo aggiungere funzionalità ad essa, importando i nuovi metodi solo ove sono necessari.
Per evitare classi wrapper ed utils
Un altro sistema molto diffuso per espandere una classe a cui non si ha diretto accesso è creare un’ulteriore classe wrapper o utils. Per esempio:
//Imported class, unmodifiable
class Car {
final String name;
const Car({
required this.name,
});
}
//Wrapper class
class CarList {
final List<Car> cars;
const CarList({
required this.cars,
});
List<String> names() {
return cars.map((car) => car.name).toList();
}
}
//Utils class
class CarUtils {
static List<String> names(List<Car> cars) {
return cars.map((car) => car.name).toList();
}
}
//Usage example
void main() {
//With a wrapper class
CarList carList = CarList(
cars: [
Car(name: 'Toyota Corolla'),
Car(name: 'Honda Civic'),
],
);
print('With a wrapper class: ${carList.names()}');
//Output: With a wrapper class: [Toyota Corolla, Honda Civic]
//With an utils class
List<Car> cars = [
Car(name: 'Toyota Corolla'),
Car(name: 'Honda Civic'),
];
print('With an utils class ${CarUtils.names(cars)}');
//Output: With an utils class: [Toyota Corolla, Honda Civic]
}
Questo sistema tuttavia porta ad aggiungere un livello di astrazione, incapsulando il tipo originario List
e lasciando così come unico indizio di tale tipo la dicitura scelta dallo sviluppatore CarList
e CarUtils
.
Utilizzando un’extension
il codice rimane esplicativo senza alcuna scelta di stile dello sviluppatore. Riscrivendo l’esempio:
//Imported class, unmodifiable
class Car {
final String name;
const Car({
required this.name,
});
}
//Class extension
extension CarList on List<Car> {
List<String> names() {
return map((car) => car.name).toList();
}
}
//Usage example
void main() {
List<Car> carList = [
Car(name: 'Toyota Corolla'),
Car(name: 'Honda Civic'),
];
print('With a class extension: ${carList.names()}');
//Output: With a class extension: [Toyota Corolla, Honda Civic]
}
Un esempio di utilizzo pratico delle extension
si può trovare nel package collection, largamente diffuso in molti progetti Flutter, creato principalmente per estendere le funzionalità di classi primarie e velocizzare così la scrittura di codice null-safe:
extension IterableExtension<T> on Iterable<T> {
...
/// The first element satisfying [test], or `null` if there are none.
T? firstWhereOrNull(bool Function(T element) test) {
for (var element in this) {
if (test(element)) return element;
}
return null;
}
...
}
La funzione capitalize()
La libreria dart:core di Flutter implementa molti metodi di manipolazione a partire da una String
. In particolar modo presenta di default due funzioni, toLowerCase
e toUpperCase
, per presentare un testo in minuscolo o maiuscolo. Tuttavia non è stata ancora aggiunta la possibilità di rendere maiuscola solo la prima lettera con una funzione capitalize
. Data la banale quanto ricorrente natura di tale funzione, proprio in questo caso può entrare in gioco l’utilizzo di un’extension
, che si integra nel modo più trasparente possibile con il tipo String
iniziale:
//String extension
extension StringExtension on String {
String capitalize() {
if(isEmpty) return '';
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
}
}
//Usage example
void main() {
String foo = 'test string';
print(foo.capitalize());
//Output: Test string
String bar = 'TEST STRING';
print(bar.capitalize());
//Output: Test string
}
Conclusioni
In conclusione, le extension
rappresentano un’importante risorsa per i programmatori Dart, poiché offrono un modo per implementare funzionalità avanzate e migliorare la leggibilità e manutenibilità del codice. È importante sottolineare, tuttavia, che non si dovrebbero sostituire alle funzioni statiche per le classi modificabili in modo diretto. In tali situazioni, l’utilizzo delle extension potrebbe suddividere le funzionalità senza una reale necessità, rendendo più opportuno mantenere i metodi all’interno della classe originale.