We use cookies to enhance your experience on the site
CodeWorlds

Services and Providers - specialists in the Empire

Salve once again, experienced Roman programmer! Now we will delve into the heart of our Empire - we will learn about services and providers. They do the real work, while controllers merely pass on orders!

Services - legion specialists

Services are classes containing the business logic of the application. They are like experienced specialists in a Roman legion - each has their field and knows how to carry out the work entrusted to them.

Basic legion service

1import { Injectable, NotFoundException } from '@nestjs/common';
2
3@Injectable()
4export class LegionService {
5 private legionaries: Legionary[] = [
6 { id: 1, name: 'Marcus Aurelius', rank: 'Centurion', experience: 10 },
7 { id: 2, name: 'Julia Domna', rank: 'Engineer', experience: 8 },
8 { id: 3, name: 'Titus Flavius', rank: 'Ballista-Operator', experience: 6 },
9 ];
10
11 // Find all legionaries
12 findAll(): Legionary[] {
13 return this.legionaries;
14 }
15
16 // Find a legionary by ID
17 findById(id: number): Legionary {
18 const legionary = this.legionaries.find(l => l.id === id);
19 if (!legionary) {
20 throw new NotFoundException(\`Legionary with ID \${id} not found in legion!\`);
21 }
22 return legionary;
23 }
24
25 // Recruit a new legionary
26 recruit(legionaryData: CreateLegionaryDto): Legionary {
27 const newLegionary = {
28 id: this.getNextId(),
29 ...legionaryData,
30 joinDate: new Date(),
31 };
32
33 this.legionaries.push(newLegionary);
34 return newLegionary;
35 }
36
37 // Promote a legionary
38 promote(id: number, newRank: string): Legionary {
39 const legionary = this.findById(id);
40 legionary.rank = newRank;
41 legionary.experience += 1;
42 return legionary;
43 }
44
45 // Remove a legionary from the legion
46 dismiss(id: number): boolean {
47 const index = this.legionaries.findIndex(l => l.id === id);
48 if (index === -1) {
49 throw new NotFoundException(\`Cannot dismiss - legionary with ID \${id} not found!\`);
50 }
51
52 this.legionaries.splice(index, 1);
53 return true;
54 }
55
56 private getNextId(): number {
57 return Math.max(...this.legionaries.map(l => l.id), 0) + 1;
58 }
59}

Dependency Injection - specialists collaborate

Services often need help from other specialists. Here's how we organize collaboration:

Tributes service collaborating with the legion

1@Injectable()
2export class TributesService {
3 constructor(
4 private readonly legionService: LegionService,
5 private readonly mapService: MapService,
6 private readonly weatherService: WeatherService,
7 ) {}
8
9 async organizeTributeCollection(provinceId: string): Promise<TributeCollectionResult> {
10 // Check legion availability
11 const availableLegionaries = this.legionService.getAvailableLegionaries();
12 if (availableLegionaries.length < 3) {
13 throw new BadRequestException('Need at least 3 legionaries for tribute collection!');
14 }
15
16 // Get province map
17 const provinceMap = await this.mapService.getProvinceLocation(provinceId);
18
19 // Check weather
20 const weather = await this.weatherService.getWeatherReport(provinceMap.coordinates);
21 if (weather.condition === 'storm') {
22 throw new BadRequestException('Too dangerous! Wait for better weather.');
23 }
24
25 // Organize tribute collection
26 const campaign = {
27 tribute: provinceMap,
28 legion: this.selectBestLegion(availableLegionaries, provinceMap.difficulty),
29 estimatedDuration: this.calculateDuration(provinceMap),
30 startTime: new Date(),
31 };
32
33 return this.launchCampaign(campaign);
34 }
35
36 private selectBestLegion(legionaries: Legionary[], difficulty: number): Legionary[] {
37 return legionaries
38 .filter(l => l.experience >= difficulty - 2)
39 .sort((a, b) => b.experience - a.experience)
40 .slice(0, Math.min(5, legionaries.length));
41 }
42}

Advanced service patterns

Repository Pattern - data warehouse

1@Injectable()
2export class LegionaryRepository {
3 constructor(@Inject('DATABASE_CONNECTION') private db: Database) {}
4
5 async findAll(): Promise<Legionary[]> {
6 return this.db.query('SELECT * FROM legionaries ORDER BY experience DESC');
7 }
8
9 async findByRank(rank: string): Promise<Legionary[]> {
10 return this.db.query('SELECT * FROM legionaries WHERE rank = ?', [rank]);
11 }
12
13 async save(legionary: Legionary): Promise<Legionary> {
14 if (legionary.id) {
15 await this.db.query(
16 'UPDATE legionaries SET name = ?, rank = ?, experience = ? WHERE id = ?',
17 [legionary.name, legionary.rank, legionary.experience, legionary.id]
18 );
19 } else {
20 const result = await this.db.query(
21 'INSERT INTO legionaries (name, rank, experience) VALUES (?, ?, ?)',
22 [legionary.name, legionary.rank, legionary.experience]
23 );
24 legionary.id = result.insertId;
25 }
26 return legionary;
27 }
28
29 async delete(id: number): Promise<boolean> {
30 const result = await this.db.query('DELETE FROM legionaries WHERE id = ?', [id]);
31 return result.affectedRows > 0;
32 }
33}
34
35// Service using the repository
36@Injectable()
37export class LegionManagementService {
38 constructor(private readonly legionaryRepository: LegionaryRepository) {}
39
40 async getAllLegionaries(): Promise<Legionary[]> {
41 return this.legionaryRepository.findAll();
42 }
43
44 async getCenturions(): Promise<Legionary[]> {
45 return this.legionaryRepository.findByRank('Centurion');
46 }
47
48 async promoteLegionary(id: number, newRank: string): Promise<Legionary> {
49 const legionary = await this.legionaryRepository.findById(id);
50 legionary.rank = newRank;
51 legionary.experience += 1;
52 return this.legionaryRepository.save(legionary);
53 }
54}

Factory Pattern - workshops in the application

1// Interface for different weapon types
2interface Weapon {
3 name: string;
4 damage: number;
5 range: number;
6 reload(): void;
7 fire(): AttackResult;
8}
9
10// Implementations of different weapons
11class Ballista implements Weapon {
12 constructor(
13 public name: string,
14 public damage: number,
15 public range: number,
16 private ammunitionCount: number,
17 ) {}
18
19 reload(): void {
20 console.log(\`Reloading \${this.name}...\`);
21 this.ammunitionCount = Math.min(this.ammunitionCount + 1, 10);
22 }
23
24 fire(): AttackResult {
25 if (this.ammunitionCount === 0) {
26 return { hit: false, message: 'No ammunition!' };
27 }
28
29 this.ammunitionCount--;
30 const hit = Math.random() > 0.3; // 70% chance to hit
31
32 return {
33 hit,
34 damage: hit ? this.damage : 0,
35 message: hit ? 'Direct hit!' : 'Missed target!',
36 };
37 }
38}
39
40class Pilum implements Weapon {
41 constructor(
42 public name: string,
43 public damage: number,
44 public range: number,
45 private loaded: boolean = false,
46 ) {}
47
48 reload(): void {
49 this.loaded = true;
50 }
51
52 fire(): AttackResult {
53 if (!this.loaded) {
54 return { hit: false, message: 'Pilum not loaded!' };
55 }
56
57 this.loaded = false;
58 const hit = Math.random() > 0.2; // 80% chance to hit (more accurate)
59
60 return {
61 hit,
62 damage: hit ? this.damage : 0,
63 message: hit ? 'Precise shot!' : 'Shot went wide!',
64 };
65 }
66}
67
68// Factory service
69@Injectable()
70export class ArmoryFactory {
71 private weaponConfigs = {
72 ballista: { damage: 50, range: 500 },
73 pilum: { damage: 25, range: 200 },
74 scorpio: { damage: 35, range: 300 },
75 };
76
77 createWeapon(type: string, name: string): Weapon {
78 const config = this.weaponConfigs[type];
79 if (!config) {
80 throw new Error(\`Unknown weapon type: \${type}\`);
81 }
82
83 switch (type) {
84 case 'ballista':
85 return new Ballista(name, config.damage, config.range, 5);
86 case 'pilum':
87 return new Pilum(name, config.damage, config.range);
88 case 'scorpio':
89 return new Ballista(name, config.damage, config.range, 8); // Different ammo count
90 default:
91 throw new Error(\`Cannot create weapon of type: \${type}\`);
92 }
93 }
94
95 createWeaponSet(unitType: string): Weapon[] {
96 const weaponSets = {
97 legion: [
98 { type: 'ballista', name: 'Left Ballista 1' },
99 { type: 'ballista', name: 'Left Ballista 2' },
100 { type: 'ballista', name: 'Right Ballista 1' },
101 { type: 'ballista', name: 'Right Ballista 2' },
102 { type: 'scorpio', name: 'Front Scorpio' },
103 ],
104 cohort: [
105 { type: 'ballista', name: 'Main Ballista' },
106 { type: 'pilum', name: 'Legionary Pilum 1' },
107 { type: 'pilum', name: 'Legionary Pilum 2' },
108 ],
109 };
110
111 const weaponList = weaponSets[unitType] || weaponSets.cohort;
112 return weaponList.map(w => this.createWeapon(w.type, w.name));
113 }
114}

Custom Providers - special solutions

Value Provider - constant configurations

1// Legion configuration
2const LEGION_CONFIG = {
3 name: 'Legio X Equestris',
4 maxSpeed: 15,
5 legionaryCapacity: 50,
6 supplyCapacity: 1000,
7 ballistae: 32,
8};
9
10@Module({
11 providers: [
12 {
13 provide: 'LEGION_CONFIG',
14 useValue: LEGION_CONFIG,
15 },
16 LegionService,
17 ],
18})
19export class LegionModule {}
20
21// Usage in the service
22@Injectable()
23export class LegionService {
24 constructor(@Inject('LEGION_CONFIG') private legionConfig: any) {}
25
26 getLegionSpecs() {
27 return {
28 ...this.legionConfig,
29 currentStatus: this.getCurrentStatus(),
30 };
31 }
32}

Factory Provider - dynamic creation

1const databaseConnectionFactory = {
2 provide: 'DATABASE_CONNECTION',
3 useFactory: async (configService: ConfigService): Promise<Database> => {
4 const config = {
5 host: configService.get('DB_HOST'),
6 port: configService.get('DB_PORT'),
7 username: configService.get('DB_USERNAME'),
8 password: configService.get('DB_PASSWORD'),
9 database: configService.get('DB_NAME'),
10 };
11
12 const connection = new Database(config);
13 await connection.connect();
14
15 // Initialize tables if they don't exist
16 await connection.query(\`
17 CREATE TABLE IF NOT EXISTS legionaries (
18 id INT AUTO_INCREMENT PRIMARY KEY,
19 name VARCHAR(255) NOT NULL,
20 rank VARCHAR(100) NOT NULL,
21 experience INT DEFAULT 1
22 )
23 \`);
24
25 return connection;
26 },
27 inject: [ConfigService],
28};
29
30@Module({
31 providers: [databaseConnectionFactory],
32 exports: ['DATABASE_CONNECTION'],
33})
34export class DatabaseModule {}

Class Provider - alternative implementations

1// Interface for the logging system
2interface Logger {
3 log(message: string): void;
4 error(message: string): void;
5 warn(message: string): void;
6}
7
8// Implementation for development
9@Injectable()
10class ConsoleLogger implements Logger {
11 log(message: string): void {
12 console.log(\` [LOG]: \${message}\`);
13 }
14
15 error(message: string): void {
16 console.error(\`[ERROR]: \${message}\`);
17 }
18
19 warn(message: string): void {
20 console.warn(\` [WARN]: \${message}\`);
21 }
22}
23
24// Implementation for production
25@Injectable()
26class FileLogger implements Logger {
27 log(message: string): void {
28 this.writeToFile('log', message);
29 }
30
31 error(message: string): void {
32 this.writeToFile('error', message);
33 }
34
35 warn(message: string): void {
36 this.writeToFile('warn', message);
37 }
38
39 private writeToFile(level: string, message: string): void {
40 // File writing implementation
41 }
42}
43
44// Dynamic implementation selection
45@Module({
46 providers: [
47 {
48 provide: 'Logger',
49 useClass: process.env.NODE_ENV === 'production' ? FileLogger : ConsoleLogger,
50 },
51 ],
52})
53export class LoggingModule {}

Asynchronous services - long campaigns

1@Injectable()
2export class CampaignService {
3 async planCampaign(destination: string): Promise<CampaignPlan> {
4 // Parallel information retrieval
5 const [weatherData, mapData, suppliesNeeded] = await Promise.all([
6 this.weatherService.getForecast(destination),
7 this.mapService.getRouteInfo(destination),
8 this.suppliesService.calculateNeeds(destination),
9 ]);
10
11 // Risk analysis
12 const riskAssessment = await this.analyzeRisks({
13 weather: weatherData,
14 route: mapData,
15 supplies: suppliesNeeded,
16 });
17
18 return {
19 destination,
20 estimatedDuration: mapData.travelTime,
21 weatherForecast: weatherData,
22 requiredSupplies: suppliesNeeded,
23 riskLevel: riskAssessment.level,
24 recommendations: riskAssessment.recommendations,
25 };
26 }
27
28 async executeCampaign(plan: CampaignPlan): Promise<CampaignResult> {
29 try {
30 // Preparation
31 await this.prepareLegion(plan.requiredSupplies);
32 await this.briefLegionaries(plan);
33
34 // Campaign execution (simulation)
35 const progress = await this.simulateMarch(plan);
36
37 return {
38 success: true,
39 tributesCollected: progress.discoveries,
40 legionStatus: progress.legionCondition,
41 timeElapsed: progress.duration,
42 };
43
44 } catch (error) {
45 return {
46 success: false,
47 error: error.message,
48 partialResults: error.partialData,
49 };
50 }
51 }
52
53 private async simulateMarch(plan: CampaignPlan): Promise<MarchProgress> {
54 const stages = ['departure', 'march', 'conquest', 'return'];
55 const progress = { discoveries: [], legionCondition: 'good', duration: 0 };
56
57 for (const stage of stages) {
58 await this.delay(1000); // Time simulation
59 const stageResult = await this.executeStage(stage, plan);
60
61 progress.discoveries.push(...stageResult.discoveries);
62 progress.duration += stageResult.timeSpent;
63
64 if (stageResult.legionCondition !== 'good') {
65 progress.legionCondition = stageResult.legionCondition;
66 }
67 }
68
69 return progress;
70 }
71
72 private delay(ms: number): Promise<void> {
73 return new Promise(resolve => setTimeout(resolve, ms));
74 }
75}

Best practices for services

  1. Single Responsibility - one service, one task
  2. Dependency Injection - all dependencies through the constructor
  3. Interface Segregation - small, specialized interfaces
  4. Error Handling - error handling at the service level
  5. Async/Await - for asynchronous operations
  6. Testing - each service should be testable
  7. Documentation - clear descriptions of public methods

Services are the true strength of our Roman Empire - they do all the work while controllers only issue orders. Well-designed services are the foundation of a scalable and maintainable application!

In the next module we will learn about Guards and Interceptors - the security and monitoring systems of our forum!

Go to CodeWorlds