Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Serwisy i Providers - specjaliści w Imperium

Salve ponownie, doświadczony rzymski programisto! Teraz zagłębimy się w serce naszego Imperium - poznamy serwisy i dostawców (providers). To oni wykonują prawdziwą pracę, podczas gdy kontrolery jedynie przekazują rozkazy!

Serwisy - specjaliści legionu

Serwisy to klasy zawierające logikę biznesową aplikacji. To jak doświadczeni specjaliści w rzymskim legionie - każdy ma swoją dziedzinę i wie, jak wykonać powierzoną mu pracę.

Podstawowy serwis legionu

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 // Znajdź wszystkich legionistów
12 findAll(): Legionary[] {
13 return this.legionaries;
14 }
15
16 // Znajdź legionistę po 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 // Rekrutuj nowego legionistę
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 // Awansuj legionistę
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 // Usuń legionistę z legionu
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 - specjaliści współpracują

Serwisy często potrzebują pomocy innych specjalistów. Oto jak organizujemy współpracę:

Serwis trybutów współpracujący z legionem

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 // Sprawdź dostępność legionu
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 // Pobierz mapę prowincji
17 const provinceMap = await this.mapService.getProvinceLocation(provinceId);
18
19 // Sprawdź pogodę
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 // Organizuj zbieranie trybutu
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}

Zaawansowane wzorce serwisów

Repository Pattern - magazyn danych

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// Serwis używający repozytorium
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 - warsztaty w aplikacji

1// Interface dla różnych typów broni
2interface Weapon {
3 name: string;
4 damage: number;
5 range: number;
6 reload(): void;
7 fire(): AttackResult;
8}
9
10// Implementacje różnych broni
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 - specjalne rozwiązania

Value Provider - stałe konfiguracje

1// Konfiguracja legionu
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// Użycie w serwisie
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 - dynamiczne tworzenie

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 // Inicjalizacja tabel jeśli nie istnieją
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 - alternatywne implementacje

1// Interface dla systemu logowania
2interface Logger {
3 log(message: string): void;
4 error(message: string): void;
5 warn(message: string): void;
6}
7
8// Implementacja dla 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// Implementacja dla 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 // Implementacja zapisu do pliku
41 }
42}
43
44// Dynamiczny wybór implementacji
45@Module({
46 providers: [
47 {
48 provide: 'Logger',
49 useClass: process.env.NODE_ENV === 'production' ? FileLogger : ConsoleLogger,
50 },
51 ],
52})
53export class LoggingModule {}

Asynchroniczne serwisy - długie kampanie

1@Injectable()
2export class CampaignService {
3 async planCampaign(destination: string): Promise<CampaignPlan> {
4 // Równoległe pobieranie informacji
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 // Analiza ryzyka
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 // Przygotowanie
31 await this.prepareLegion(plan.requiredSupplies);
32 await this.briefLegionaries(plan);
33
34 // Wykonanie kampanii (symulacja)
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); // Symulacja czasu
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}

Najlepsze praktyki dla serwisów

  1. Single Responsibility - jeden serwis, jedno zadanie
  2. Dependency Injection - wszystkie zależności przez konstruktor
  3. Interface Segregation - małe, wyspecjalizowane interfejsy
  4. Error Handling - obsługa błędów na poziomie serwisu
  5. Async/Await - dla operacji asynchronicznych
  6. Testing - każdy serwis powinien być testowalny
  7. Documentation - jasne opisy publicznych metod

Serwisy to prawdziwa siła naszego rzymskiego imperium - to oni wykonują całą pracę, podczas gdy kontrolery tylko wydają rozkazy. Dobrze zaprojektowane serwisy to podstawa skalowalnej i maintainable aplikacji!

W następnym module poznamy Guards i Interceptors - systemy bezpieczeństwa i monitorowania naszego forum!

Vai a CodeWorlds