La possibilità di riutilizzare il codice ed evitare le duplicazioni è uno degli obiettivi che, come buoni sviluppatori, dovremmo sempre cercare di perseguire. Uno dei tanti modi in cui è possibile raggiungere questo risultato è quello di utilizzare l'ereditarietà.
Molti linguaggi di programmazione consentono l’ereditarietà multipla ma come sviluppatori front-end sappiamo bene, però, che JavaScript non è un linguaggio che la supporta. Inoltre, è bene tenere a mente che quando una classe eredita da un'altra deve mantenere una relazione "Is a" (es: A Student is a Person).
Per risolvere il problema comune della multi-ereditarietà, molti linguaggi hanno introdotto un meccanismo che permette di "mixare" le funzionalità (metodi e variabili) per favorire il riutilizzo e ridurre la complessità del codice. Questi prendono il nome di Mixin o Traits (eh sì, mi piace PHP) e sono usati con l'intenzione di raggruppare delle funzionalità a grana-fine e di combinarli insieme con altri gruppi per poi utilizzarli all'interno delle classi, così da semplificare il codice e favorire il suo riutilizzo.
Utilizzando JavaScript con TypeScript, ho scoperto che questo concetto è presente, ma, a differenza di altri linguaggi di programmazione, non è presente un costrutto definito e quindi esistono diversi modi per implementare i Mixin.
Al seguente link potete trovare la guida ufficiale messa a disposizione da TypeScript per poter creare dei Mixin. Se il vostro scopo è quello di applicare dei mixin all'interno di un Component, probabilmente vi troverete ben presto a dover affrontare il caso in cui un mixin faccia utilizzo di un Service:
1@Injectable({
2 providedIn: 'root'
3})
4class MyService {
5 doSomething(): void {
6 console.log('Operation...');
7 }
8}
9
10@Directive()
11class ValidationMixin {
12 myService = inject(MyService);
13
14 validate(): void { ... }
15}
16
17@Directive()
18class ActionsMixin {
19 onCreate(): void { ... }
20
21 onEdit(): void { ... }
22}
23
24class FormView {}
25
26interface FormView extends ActionsMixin, ValidationMixin {}
27
28applyMixins(FormView, [ActionsMixin, ValidationMixin]);
29
30class AppComponent extends FormView implements OnInit {
31 ngOnInit(): void {
32 this.myService.doSomething();
33 }
34}
Nel codice precedente sono stati:
Se si esegue il codice sopra riportato, si riscontrerà un errore in console in prossimità della proprietà myService:
Cannot read properties of undefined (reading 'doSomething')
L'errore è causato dalla funzione applyMixins, che non riesce a "mescolare" correttamente la dipendenza al servizio MyService. Per evitare ciò, possiamo riscrivere i mixin nel modo seguente:
services/my-service.service.ts
1@Injectable({
2 providedIn: 'root'
3})
4class MyService {
5 doSomething(): void {
6 console.log('Operation...');
7 }
8}
models/mixin.model.ts
1export type Constructor<T = object> = new (...args: any[]) => T;
2
3export class Base {}
mixins/validation.mixin.ts
1export interface Validation {
2 myService: MyService;
3 validate(): void;
4}
5
6export function validationMixin<T extends Constructor>(Parent: T): T {
7 @Directive()
8 class ValidationImpl extends Parent implements Validation {
9 myService = inject(MyService);
10
11 validate(): void { ... }
12 }
13
14 return ValidationImpl;
15}
mixins/actions.mixin.ts
1export interface Actions {
2 onCreate(): void;
3 onEdit(): void;
4}
5
6export function actionsMixin<T extends Constructor>(Parent: T): T {
7 @Directive()
8 class ActionsImpl extends Parent implements Actions {
9 onCreate(): void { ... }
10
11 onEdit(): void { ... }
12 }
13
14 return ActionsImpl;
15}
mixins/index.ts
1import { Base } from '../models/mixin.model';
2import { Actions, actionsMixin } from './actions.mixin';
3import { Validation, validationMixin } from './validation.mixin';
4
5export interface FormView extends Actions, Validation {}
6
7export class FormView extends validationMixin(actionsMixin(Base)) {}
app.component.ts
1@Component({
2 selector: 'app-root',
3 standalone: true,
4 templateUrl: './app.component.html',
5 styleUrl: './app.component.css'
6})
7export class AppComponent extends FormView implements OnInit {
8 ngOnInit(): void {
9 this.myService.doSomething();
10 }
11}
Questa seconda soluzione risolve l'errore precedente adottando un approccio ai mixin a "Matrioska", vediamo quali sono i passaggi chiave:
T
. La funzione restituisce una classe che estende il tipo Parent ricevuto in ingresso e implementa una interfaccia che definisce tutte le proprietà e metodi pubblici che devono essere visibili e utilizzabili all'interno del componente che farà utilizzo del mixin. Come nel codice precedente, l'interfaccia serve ai fini dell'inferenza dei tipi.
Cosa succede se abbiamo due mixin che necessitano di avere un medesimo metodo?
1export function validationMixin<T extends Constructor>(Parent: T): T {
2 @Directive()
3 class ValidationImpl extends Parent implements Validation {
4 myService = inject(MyService);
5
6 validate(): void {
7 this.myCommonLogic();
8 }
9
10 myCommonLogic(): void {
11 console.log('Some same logic');
12 }
13 }
14
15 return ValidationImpl;
16}
17
18export function actionsMixin<T extends Constructor>(Parent: T): T {
19 @Directive()
20 class ActionsImpl extends Parent implements Actions {
21 onCreate(): void {
22 this.myCommonLogic();
23 }
24
25 onEdit(): void {
26 this.myCommonLogic();
27 }
28
29 myCommonLogic(): void {
30 console.log('Some same logic');
31 }
32 }
33
34 return ActionsImpl;
35}
Nel codice sopra, il metodo myCommonLogic contiene la stessa logica per entrambi i mixin, per evitare ciò possiamo creare un terzo mixin e lasciare ad esso l'implementazione del metodo:
mixins/validation.mixin.ts
1export function validationMixin<T extends Constructor>(Parent: T): T {
2 @Directive()
3 class ValidationImpl extends Parent implements Validation {
4 myService = inject(MyService);
5
6 validate(): void {
7 this.myCommonLogic();
8 }
9
10 myCommonLogic(): void {
11 throw new Error('Not implemented');
12 }
13 }
14
15 return ValidationImpl;
16}
mixins/actions.mixin.ts
1export function actionsMixin<T extends Constructor>(Parent: T): T {
2 @Directive()
3 class ActionsImpl extends Parent implements Actions {
4 onCreate(): void {
5 this.myCommonLogic();
6 }
7
8 onEdit(): void {
9 this.myCommonLogic();
10 }
11
12 myCommonLogic(): void {
13 throw new Error('Not implemented');
14 }
15 }
16
17 return ActionsImpl;
18}
mixins/common.mixin.ts
1export interface Common {
2 myCommonLogic(): void;
3}
4
5export function commonMixin<T extends Constructor>(Parent: T): T {
6 @Directive()
7 class CommonImpl extends Parent implements Common {
8 myCommonLogic(): void {
9 console.log('Some same logic');
10 }
11 }
12
13 return CommonImpl;
14}
mixins/index.ts
1export interface FormView extends Actions, Validation, Common {}
2
3export class FormView extends commonMixin(validationMixin(actionsMixin(Base))) {}
Abbiamo visto insieme come funzionano i mixin e come possono essere utilizzati nel contesto di un’applicazione Angular. Se hai bisogno di consolidare e/o combinare logica riutilizzabile che possa anche agganciarsi ai metodi del ciclo di vita di un componente Angular, i mixin possono essere uno strumento potente. Ma attenzione, potrebbero creare conflitti nel caso in cui andassero a sovrascrivere metodi già esistenti nella classe. Quindi usalo con cautela e scegli con cura i nomi dei metodi 😎
Developer @Devmy, PUG Catania Organizer
Da sempre considera programmare più come una passione che come un lavoro. D'altra parte, questa è la ragione principale che spinge i programmatori a imparare nuovi linguaggi, nuove tecnologie e metodologie ogni giorno. Senza questa "fame" di sapere non è possibile raggiungere grandi conquiste in questa professione.
La sua passione lo ha portato allo sviluppo di applicazioni web usando tecnologie quali PHP, MySQL, Symfony, JavaScript, Node, Angular, tuttavia sperimenta volentieri anche con altri linguaggi e tecnologie come Vue, VB e Flutter. Grande sostenitore della metodologia Agile e dei principi presenti all'interno del manifesto che lo hanno fortemente ispirato nella creazione di una serie di libri che ben esprimono i vantaggi di seguire questi principi.
Precedentemente docente presso PED Academy.
Hai un progetto in mente?
Anche se - semplicemente - vuoi prendere un caffè con noi o vedere la nostra collezione di Action Figures scrivici tramite questo form.