Angular Mixins

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:

  • Registrati i mixins come Directive: affinché un servizio possa essere iniettato usando la DI è necessario che l'iniezione della dipendenza avvenga all'interno di un injection context.
  • Creati i tipi per la classe FormView: questa è la classe composta dai due mixin che poi verrà estesa dal componente. L'interfaccia serve ai fini dell'inferenza dei tipi, senza la quale TypeScript non riuscirà a suggerire correttamente le proprietà e i metodi definiti all'interno dei mixin.
  • Mescolati i mixins all'interno di FormView: la funzione applyMixins si occupa di "mescolare" proprietà e metodi da più classi base (o mixin) nel prototipo di una classe derivata.

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:

  • Definizione dei modelli: sono stati definiti due tipi Constructor e Base. Il primo serve per identificare una classe generica dotata di costruttore. Esso servirà a "mescolare" le classi mixin. La classe Base invece verrà utilizzata come punto di partenza per agganciare tra di loro i mixin.
  • Creazione dei mixin: i due mixin vengono definiti come funzioni che accettano come parametro di ingresso un Parent di tipo 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.
  • Creazione della classe FormView: questa classe è data dalla composizione dei due mixin sopra dichiarati validationMixin e actionsMixin che come una "Matrioska" ingloberà le proprietà e i metodi definiti in entrambi (attenzione: nel caso di due mixin che presentano metodi che hanno lo stesso nome, l'ordine in cui vengono composti è importante)

 

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))) {}

 

Conclusioni

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 😎

Contattaci.

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.

Questo sito è protetto da reCAPTCHA e si applicano le Norme sulla privacy e i Termini di servizio di Google.

Ultimi Articoli