@@ -7,6 +7,7 @@ The Rikta DI container with full autowiring support.
77- ** Automatic resolution** via TypeScript metadata
88- ** Single decorator** : ` @Autowired() ` for everything
99- ** Token-based injection** for interfaces
10+ - ** Abstract class-based injection** for contracts (Strategy pattern)
1011- ** Zero configuration** - just decorate!
1112
1213## @Autowired () - The Universal Decorator
@@ -222,3 +223,255 @@ class RequestLogger { }
222223| ` container.registerProvider(provider) ` | Register custom provider |
223224| ` container.resolve(token) ` | Get instance |
224225| ` container.resolveOptional(token) ` | Get instance or undefined |
226+
227+ ## Abstract Class-Based Injection
228+
229+ For complex contracts and the Strategy pattern, use abstract classes instead of tokens.
230+
231+ ### Why Abstract Classes?
232+
233+ | Aspect | InjectionToken | Abstract Class |
234+ | --------| ----------------| ----------------|
235+ | ** Type Safety** | ⚠️ Requires explicit type | ✅ Inferred automatically |
236+ | ** Boilerplate** | ⚠️ Create token separately | ✅ Just the abstract class |
237+ | ** Refactoring** | ⚠️ Update token everywhere | ✅ Rename works automatically |
238+ | ** Shared Methods** | ❌ Not possible | ✅ Add utility methods |
239+
240+ ### Define the Contract
241+
242+ ``` typescript
243+ // contracts/notification.strategy.ts
244+ export abstract class NotificationStrategy {
245+ abstract send(recipient : string , message : string ): Promise <boolean >;
246+ abstract isAvailable(): boolean ;
247+
248+ // Shared utility method available to all implementations
249+ protected log(message : string ): void {
250+ console .log (` [${this .constructor .name }] ${message } ` );
251+ }
252+ }
253+ ```
254+
255+ ### Implement with ` @Implements `
256+
257+ ``` typescript
258+ // strategies/email.strategy.ts
259+ import { Injectable , Implements , Primary } from ' @riktajs/core' ;
260+ import { NotificationStrategy } from ' ../contracts/notification.strategy' ;
261+
262+ @Injectable ()
263+ @Implements (NotificationStrategy )
264+ @Primary () // This is the default implementation
265+ export class EmailStrategy extends NotificationStrategy {
266+ async send(recipient : string , message : string ): Promise <boolean > {
267+ this .log (` Sending email to ${recipient } ` );
268+ // ... email logic
269+ return true ;
270+ }
271+
272+ isAvailable(): boolean {
273+ return true ;
274+ }
275+ }
276+ ```
277+
278+ ``` typescript
279+ // strategies/sms.strategy.ts
280+ @Injectable ()
281+ @Implements (NotificationStrategy )
282+ export class SmsStrategy extends NotificationStrategy {
283+ async send(recipient : string , message : string ): Promise <boolean > {
284+ this .log (` Sending SMS to ${recipient } ` );
285+ // ... SMS logic
286+ return true ;
287+ }
288+
289+ isAvailable(): boolean {
290+ return process .env .TWILIO_ENABLED === ' true' ;
291+ }
292+ }
293+ ```
294+
295+ ### Inject the Abstract Class
296+
297+ ``` typescript
298+ @Controller (' /notifications' )
299+ export class NotificationController {
300+ // Automatically resolved to EmailStrategy (the @Primary)
301+ @Autowired ()
302+ private strategy! : NotificationStrategy ;
303+
304+ @Post (' /send' )
305+ async send(@Body () data : { to: string ; message: string }) {
306+ if (! this .strategy .isAvailable ()) {
307+ throw new Error (' Notification strategy not available' );
308+ }
309+ return this .strategy .send (data .to , data .message );
310+ }
311+ }
312+ ```
313+
314+ ### Strategy Pattern with Factory
315+
316+ For runtime strategy selection, combine with a Factory pattern:
317+
318+ ``` typescript
319+ // factory/notification.factory.ts
320+ @Injectable ()
321+ export class NotificationFactory {
322+ @Autowired ()
323+ private emailStrategy! : EmailStrategy ;
324+
325+ @Autowired ()
326+ private smsStrategy! : SmsStrategy ;
327+
328+ @Autowired ()
329+ private pushStrategy! : PushStrategy ;
330+
331+ getStrategy(channel : ' email' | ' sms' | ' push' ): NotificationStrategy {
332+ switch (channel ) {
333+ case ' email' : return this .emailStrategy ;
334+ case ' sms' : return this .smsStrategy ;
335+ case ' push' : return this .pushStrategy ;
336+ }
337+ }
338+
339+ getAvailableStrategies(): NotificationStrategy [] {
340+ return [this .emailStrategy , this .smsStrategy , this .pushStrategy ]
341+ .filter (s => s .isAvailable ());
342+ }
343+ }
344+
345+ // services/notification.service.ts
346+ @Injectable ()
347+ export class NotificationService {
348+ @Autowired ()
349+ private factory! : NotificationFactory ;
350+
351+ @Autowired ()
352+ private defaultStrategy! : NotificationStrategy ; // Gets @Primary
353+
354+ async notify(
355+ recipient : string ,
356+ message : string ,
357+ channel ? : ' email' | ' sms' | ' push'
358+ ): Promise <boolean > {
359+ const strategy = channel
360+ ? this .factory .getStrategy (channel )
361+ : this .defaultStrategy ;
362+
363+ return strategy .send (recipient , message );
364+ }
365+
366+ async notifyAll(recipient : string , message : string ): Promise <void > {
367+ const strategies = this .factory .getAvailableStrategies ();
368+ await Promise .all (strategies .map (s => s .send (recipient , message )));
369+ }
370+ }
371+ ```
372+
373+ ### Multiple Implementations Rules
374+
375+ 1 . ** Single implementation** : Automatically used, no ` @Primary ` needed
376+ 2 . ** Multiple implementations with ` @Primary ` ** : The ` @Primary ` is the default
377+ 3 . ** Multiple implementations without ` @Primary ` ** : Error at resolution time
378+ 4 . ** Multiple implementations with ` @Named ` ** : Use qualified injection by name
379+
380+ ``` typescript
381+ // ✅ Single implementation - works
382+ @Injectable ()
383+ @Implements (CacheStrategy )
384+ export class RedisCache extends CacheStrategy { }
385+
386+ // ✅ Multiple with @Primary - works
387+ @Injectable ()
388+ @Implements (PaymentGateway )
389+ @Primary ()
390+ export class StripeGateway extends PaymentGateway { }
391+
392+ @Injectable ()
393+ @Implements (PaymentGateway )
394+ export class PayPalGateway extends PaymentGateway { }
395+
396+ // ❌ Multiple without @Primary - throws error
397+ @Injectable ()
398+ @Implements (Logger )
399+ export class FileLogger extends Logger { }
400+
401+ @Injectable ()
402+ @Implements (Logger )
403+ export class ConsoleLogger extends Logger { }
404+ // Error: Multiple implementations found. Use @Primary() to mark one as default.
405+ ```
406+
407+ ### Named Implementations with @Named
408+
409+ When you have multiple implementations and want to inject specific ones by name:
410+
411+ ``` typescript
412+ import { Injectable , Implements , Named , Primary , Autowired } from ' @riktajs/core' ;
413+
414+ // Abstract contract
415+ abstract class Mailer {
416+ abstract send(to : string , body : string ): Promise <void >;
417+ }
418+
419+ // Named implementations
420+ @Injectable ()
421+ @Implements (Mailer )
422+ @Named (' smtp' )
423+ @Primary () // Also the default
424+ export class SmtpMailer extends Mailer {
425+ async send(to : string , body : string ): Promise <void > {
426+ console .log (' [SMTP]' , to , body );
427+ }
428+ }
429+
430+ @Injectable ()
431+ @Implements (Mailer )
432+ @Named (' sendgrid' )
433+ export class SendGridMailer extends Mailer {
434+ async send(to : string , body : string ): Promise <void > {
435+ console .log (' [SendGrid]' , to , body );
436+ }
437+ }
438+
439+ // Inject by name
440+ @Injectable ()
441+ export class MailService {
442+ @Autowired (Mailer )
443+ private defaultMailer! : Mailer ; // Gets SmtpMailer (Primary)
444+
445+ @Autowired (Mailer , ' smtp' )
446+ private smtpMailer! : Mailer ;
447+
448+ @Autowired (Mailer , ' sendgrid' )
449+ private sendgridMailer! : Mailer ;
450+ }
451+
452+ // Also works with constructor injection
453+ @Injectable ()
454+ export class CampaignService {
455+ constructor (
456+ @Autowired (Mailer , ' sendgrid' ) private bulkMailer : Mailer ,
457+ @Autowired (Mailer , ' smtp' ) private transactionalMailer : Mailer
458+ ) {}
459+ }
460+ ```
461+
462+ ### Explicit Registration (Override)
463+
464+ You can also register implementations explicitly, overriding ` @Implements ` :
465+
466+ ``` typescript
467+ // main.ts
468+ import { container } from ' @riktajs/core' ;
469+
470+ // Override based on environment
471+ if (process .env .NODE_ENV === ' test' ) {
472+ container .registerProvider ({
473+ provide: NotificationStrategy ,
474+ useClass: MockNotificationStrategy ,
475+ });
476+ }
477+ ```
0 commit comments