Skip to content

Commit 9678a67

Browse files
committed
feat(core): implement Abstract Class-Based DI with @primary and @nAmed decorators
Add comprehensive dependency injection pattern using abstract classes as contracts: Features: - @implements decorator: register implementations for abstract classes - @primary decorator: select default implementation when multiple exist - @nAmed decorator: qualified injection for specific named implementations - Full registry tracking and container resolution - Support for both property and constructor injection - Combine @primary with @nAmed for default + specific access Changes: - Add 16 comprehensive tests - Update documentation with examples
1 parent c07b65b commit 9678a67

File tree

23 files changed

+2419
-24
lines changed

23 files changed

+2419
-24
lines changed

docs/guide/dependency-injection.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
```

examples/example/README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@ example/
1616
│ └── logger.provider.ts # @Provider for LOGGER (with dependencies!)
1717
├── controllers/
1818
│ ├── app.controller.ts # Root & health endpoints
19+
│ ├── notification.controller.ts # Strategy Pattern demo
1920
│ └── user.controller.ts # User CRUD endpoints
21+
├── strategies/
22+
│ ├── notification.strategy.ts # Abstract strategy contract
23+
│ ├── email.strategy.ts # @Primary email implementation
24+
│ ├── sms.strategy.ts # SMS implementation
25+
│ ├── push.strategy.ts # Push notification implementation
26+
│ └── notification.factory.ts # Factory for runtime selection
2027
└── services/
2128
├── database.service.ts # In-memory database (priority: 100)
2229
├── health.service.ts # Health check + OnApplicationListen
@@ -43,6 +50,10 @@ Server starts at `http://localhost:3000`
4350
| POST | `/users` | Create user |
4451
| PUT | `/users/:id` | Update user |
4552
| DELETE | `/users/:id` | Delete user |
53+
| GET | `/notifications/channels` | List notification channels |
54+
| GET | `/notifications/status` | Notification system status |
55+
| POST | `/notifications/send` | Send notification |
56+
| POST | `/notifications/broadcast` | Broadcast to all channels |
4657

4758
## 🔄 Lifecycle Demonstrations
4859

@@ -136,6 +147,55 @@ export class DatabaseService {
136147
}
137148
```
138149

150+
### Strategy Pattern with Abstract Class DI
151+
152+
```typescript
153+
// Abstract contract
154+
abstract class NotificationStrategy {
155+
abstract send(recipient: string, message: string): Promise<boolean>;
156+
abstract isAvailable(): boolean;
157+
}
158+
159+
// Primary implementation (default)
160+
@Injectable()
161+
@Primary()
162+
@Implements(NotificationStrategy)
163+
export class EmailStrategy extends NotificationStrategy {
164+
async send(recipient: string, message: string): Promise<boolean> {
165+
// Email logic...
166+
return true;
167+
}
168+
169+
isAvailable(): boolean {
170+
return true;
171+
}
172+
}
173+
174+
// Factory for runtime selection
175+
@Injectable()
176+
export class NotificationFactory {
177+
@Autowired()
178+
private emailStrategy!: EmailStrategy;
179+
180+
@Autowired()
181+
private smsStrategy!: SmsStrategy;
182+
183+
getStrategy(channel: 'email' | 'sms'): NotificationStrategy {
184+
return channel === 'email' ? this.emailStrategy : this.smsStrategy;
185+
}
186+
}
187+
188+
// Inject abstract class - auto-resolved to @Primary
189+
@Injectable()
190+
export class NotificationService {
191+
@Autowired()
192+
private strategy!: NotificationStrategy; // Gets EmailStrategy
193+
194+
@Autowired()
195+
private factory!: NotificationFactory;
196+
}
197+
```
198+
139199
## 🧪 Test with cURL
140200

141201
```bash
@@ -152,6 +212,24 @@ curl -X POST http://localhost:3000/users \
152212

153213
# List users
154214
curl http://localhost:3000/users
215+
216+
# Notification channels
217+
curl http://localhost:3000/notifications/channels
218+
219+
# Send notification via default channel (email)
220+
curl -X POST http://localhost:3000/notifications/send \
221+
-H "Content-Type: application/json" \
222+
-d '{"recipient": "user@example.com", "message": "Hello!"}'
223+
224+
# Send notification via specific channel
225+
curl -X POST http://localhost:3000/notifications/send \
226+
-H "Content-Type: application/json" \
227+
-d '{"recipient": "+1234567890", "message": "Your OTP is 123456", "channel": "sms"}'
228+
229+
# Broadcast to all available channels
230+
curl -X POST http://localhost:3000/notifications/broadcast \
231+
-H "Content-Type: application/json" \
232+
-d '{"recipient": "user123", "message": "Important announcement!"}'
155233
```
156234

157235
## 🔑 Key Features Demonstrated
@@ -165,6 +243,11 @@ curl http://localhost:3000/users
165243
| `OnApplicationListen` hook | `services/health.service.ts` |
166244
| `@On()` decorator | `services/monitoring.service.ts` |
167245
| Auto-discovery | `main.ts` (autowired: ['./src']) |
246+
| Strategy Pattern | `strategies/*.ts` |
247+
| Abstract Class DI | `strategies/notification.strategy.ts` |
248+
| `@Implements` decorator | `strategies/email.strategy.ts` |
249+
| `@Primary` decorator | `strategies/email.strategy.ts` |
250+
| Factory Pattern | `strategies/notification.factory.ts` |
168251

169252
## 📦 Path Resolution
170253

0 commit comments

Comments
 (0)