strategia: wyliczanie zniżki

omówię:

- jak dodać strategię wyliczającą zniżkę w zależności od rodzaju klienta

Stworzyliśmy formularz generowany dynamicznie w zależności od wybranego rodzaju klienta. W tej lekcji rozszerzymy funkcjonalność formularza o wyliczenie indywidualnej zniżki.

Na poziomie folderu app stwórz katalog strategy oraz dodaj w nim interfejs offer-strategy.interface.ts

import {Client} from '../interfaces/client.interface';

export interface OfferStrategy {
getDiscount(client: Client): number;
}

Stworzymy dwie strategie biznesową oraz prywatną, które będą rozszerzać ten interfejs. 

Aby korzystać ze strategii potrzebujemy danych klienta. Stwórz interfejs client.interface.ts. Interfejs klienta także podzielimy na dwa rodzaje. Dodamy interfejs bazowy client.interface.ts oraz business-client.interface.ts i private-client.interface.ts. W folderze interfaces stwórz trzy interfejsy klienta.

bazowy client.interface.ts

import { ClientType } from '../enums/client-type.enum';

export interface Client {
clientType: ClientType;
}

business-client.interface.ts

import { Client } from './client.interface';

export interface BusinessClient extends Client {
companyName: string;
nip: string;
businessInMonths: number;
}

private-client.interface.ts

import { Client } from './client.interface';

export interface PrivateClient extends Client {
clientName: string;
clientLastName: string;
isUnder25: boolean;
licenseInMonths: number;
}

Przejdźmy do stworzenia dwóch strategii. 

W katalogu strategy dodaj plik business-offer-strategy.ts

import {OfferStrategy} from './offer-strategy.interface';
import {Client} from '../interfaces/client.interface';
import {BusinessClient} from '../interfaces/business-client.interface';

export class BusinessOfferStrategy implements OfferStrategy {

getDiscount(client: Client): number {
const businessInMonths = (client as BusinessClient).businessInMonths;

if (businessInMonths < 6) {
return 0;
}

if (businessInMonths >= 6 && businessInMonths < 12) {
return 4;
}

if (businessInMonths > 60) {
return 30;
}
return businessInMonths * 0.4;
}
}

oraz private-offer-strategy.ts

import {OfferStrategy} from './offer-strategy.interface';
import {PrivateClient} from '../interfaces/private-client.interface';
import {Client} from '../interfaces/client.interface';

export class PrivateOfferStrategy implements OfferStrategy {
getDiscount(client: Client): number {
const privateClient = (client as PrivateClient);
const licenseInMonths = privateClient.licenseInMonths;

console.log(licenseInMonths < 24);
if (licenseInMonths <= 24 || (licenseInMonths <= 36 && privateClient.isUnder25)) {
return 0;
}

if (licenseInMonths > 60) {
return 15;
}
return licenseInMonths * 0.2;
}
}

Nowością która pojawiła się w kodzie jest rzutowanie.

const privateClient = (client as PrivateClient);

Działa to tak samo jak w Javie. Jeżeli przekazujemy w metodzie parametr typu bazowego to jeżeli chcemy działać na konkretnym typie, który rozszerza typ bazowy, musimy dokonać rzutowania.

Mamy już dwie strategie które wyliczają zniżkę w odpowiedni sposób dla danego rodzaju klienta. Musimy stworzyć serwis który będzie wybierał odpowiednią styrategię. 

Stwórz serwis discount.service.ts w folderze strategy

import {Injectable} from '@angular/core';
import {OfferStrategy} from './offer-strategy.interface';
import {BusinessOfferStrategy} from './business-offer-strategy';
import {PrivateOfferStrategy} from './private-offer-strategy';
import {Client} from '../interfaces/client.interface';
import {ClientType} from '../enums/client-type.enum';

@Injectable(
{
providedIn: 'root'
}
)
export class DiscountService {
public getDiscount(client: Client): number {
return this.resolveStrategy(client).getDiscount(client);
}

private resolveStrategy(client: Client): OfferStrategy {
return client.clientType === ClientType.BUSINESS ?
new BusinessOfferStrategy() : new PrivateOfferStrategy();
}
}

Zanim użyjemy discount.service.ts musimy dostosować form.servis.ts. Musimy dodać możliwość zebrania danych z formularza i przekształcenia ich na dane klienta, które przekażemy dalej do serwisu wyliczającego zniżkę.

form.service.ts

import {Injectable} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms';
import {ClientType} from '../enums/client-type.enum';
import {Client} from '../interfaces/client.interface';
import {PrivateClient} from '../interfaces/private-client.interface';
import {BusinessClient} from '../interfaces/business-client.interface';

@Injectable({
providedIn: 'root'
})
export class FormService {
constructor(private formBuilder: FormBuilder) {
}

public createPrivateClientFormGroup(): FormGroup { // <------- tworzenie formularza dla klienta prywatnego
return this.formBuilder.group({
clientName: ['', Validators.required],
clientLastName: ['', Validators.required],
isUnder25: [''],
licenseInMonths: ['', [Validators.required, Validators.pattern('^[0-9]*$')]],
});
}

public createBusinessClientFormGroup(): FormGroup { // <------- tworzenie formularza dla klienta biznesowego
return this.formBuilder.group({
companyName: ['', Validators.required],
nip: ['', [Validators.required, Validators.pattern('^[0-9]*$')]],
businessInMonths: ['', [Validators.required, Validators.pattern('^[0-9]*$')]],
});
}

// <----- przygotwanie danych klienta z danych wprowadzonych do formularza
public prepareClientDataByForm(form: FormGroup, clientType: ClientType): Client {
if (!form || !clientType) {
return;
}
return clientType === ClientType.PRIVATE ?
this.preparePrivateClient(form, clientType) : this.prepareBusinessClient(form, clientType);
}

private preparePrivateClient(form: FormGroup, clientType: ClientType): PrivateClient {
const clientNameControl: FormControl = form.get('clientName') as FormControl;
const clientLastNameControl: FormControl = form.get('clientLastName') as FormControl;
const isUnder25Control: FormControl = form.get('isUnder25') as FormControl;
const licenseInMonthsControl: FormControl = form.get('licenseInMonths') as FormControl;
return {
clientName: clientNameControl.value,
clientLastName: clientLastNameControl.value,
isUnder25: isUnder25Control.value,
licenseInMonths: licenseInMonthsControl.value,
clientType
};
}

private prepareBusinessClient(form: FormGroup, clientType: ClientType): BusinessClient {
const companyNameControl: FormControl = form.get('companyName') as FormControl;
const nipControl: FormControl = form.get('nip') as FormControl;
const businessInMonthsControl: FormControl = form.get('businessInMonths') as FormControl;
return {
companyName: companyNameControl.value,
nip: nipControl.value,
businessInMonths: businessInMonthsControl.value,
clientType
};
}
}

Serwis discount.service.ts będzie wykorzystywany w dwóch komponentach: business-client-form.component.ts oraz private-client-form.component.ts 

business-client-form.component.ts

import {Component, EventEmitter, OnInit, Output} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {FormService} from '../form.service';
import {DiscountService} from '../../strategy/discount.service';
import {ClientType} from '../../enums/client-type.enum';

@Component({
selector: 'app-business-client-form',
templateUrl: './business-client-form.component.html',
styleUrls: ['./business-client-form.component.scss']
})
export class BusinessClientFormComponent implements OnInit {
public businessClientForm: FormGroup;
@Output()
public discountCounted = new EventEmitter<number>(); // <--- dodanie komunikacji o zdarzeniu wyliczenia zniżki

constructor(private formService: FormService, private discountService: DiscountService) {
}

ngOnInit(): void {
this.businessClientForm = this.formService.createBusinessClientFormGroup();
}

public countDiscount() { // <---- metoda wyliczająca zniżkę
const clientData = this.formService.prepareClientDataByForm(this.businessClientForm, ClientType.BUSINESS);
const discount = this.discountService.getDiscount(clientData);
this.discountCounted.emit(discount);
}
}

oraz private-client-form.component.ts 

import {Component, EventEmitter, OnInit, Output} from '@angular/core';
import {FormGroup} from '@angular/forms';
import {FormService} from '../form.service';
import {ClientType} from '../../enums/client-type.enum';
import {DiscountService} from '../../strategy/discount.service';

@Component({
selector: 'app-private-client-form',
templateUrl: './private-client-form.component.html',
styleUrls: ['./private-client-form.component.scss']
})
export class PrivateClientFormComponent implements OnInit {

public privateClientForm: FormGroup;
@Output()
public discountCounted = new EventEmitter<number>(); // <--- dodanie komunikacji o zdarzeniu wyliczenia zniżki

constructor(private formService: FormService, private discountService: DiscountService) {
}

ngOnInit(): void {
this.privateClientForm = this.formService.createPrivateClientFormGroup();
}

public countDiscount() { // <---- metoda wyliczająca zniżkę
const clientFromForm = this.formService.prepareClientDataByForm(this.privateClientForm, ClientType.PRIVATE);
const discount = this.discountService.getDiscount(clientFromForm);
this.discountCounted.emit(discount);
}
}

Z tak przygotowaną strategią możemy przejść do głównego komponentu app.componet.ts w celu sprawdzenia czy danemu klientowi przysługuje zniżka. Wyliczymy takżę cenę samochodu po uwzględnieniu zniżki.

Przejdź do app.component.ts

import {Component, OnInit} from '@angular/core';
import {CarService} from './car/car.service';
import {OfferType} from './interfaces/offer-type.interface';
import {Car} from './car/car.interface';
import {ClientType} from './enums/client-type.enum';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
selectedCar: Car;
cars: Car[] = [];
clientType: ClientType;
public ClientType = ClientType;
public carPriceAfterDiscount: number; // <----- cena samochodu po zniżce
public discountAmount: number; // <--- wartość zniżki

public ngOnInit(): void {
this.carService.fetchAvailableCars().subscribe(cars => {
this.cars = cars;
});
}

constructor(private carService: CarService) {
}

public carSelected(offerType: OfferType): void {
this.selectedCar = this.cars
.find(car => car.vin === offerType.carVin);
this.clientType = offerType.clientType;
}

public resetCarSection() {
this.selectedCar = null;
this.discountAmount = null;
this.carPriceAfterDiscount = null;
}

public discountCounted(discount: number): void { // <------- dodanie metody która przechwytuje zdarzenie wyliczenie zniżki
const discountAmount = this.selectedCar.price * (discount / 100);
this.carPriceAfterDiscount = this.selectedCar.price - discountAmount;
this.discountAmount = discountAmount;
}
}

Nie pozostaje nic innego jak dostosowanie szablonu app.component.html

<div class="main-container">
<ng-container *ngIf="!selectedCar; else carInfo">
<app-card *ngFor="let car of cars" class="card"
[identifier]="car.vin"
[title]="car.brand"
[subtitle]="car.model"
[photoSource]="car.photoSource"
[description]="'CENA: ' + car.price + ' ZŁ. VIN: ' + car.vin"
(carSelected)="carSelected($event)">
</app-card>
</ng-container>
<ng-template #carInfo>
<app-card class="info-card"
[identifier]="selectedCar.vin"
[title]="selectedCar.brand"
[subtitle]="selectedCar.model"
[photoSource]="selectedCar.photoSource"
[description]="'CENA: ' + selectedCar.price + ' ZŁ. VIN: ' + selectedCar.vin"
[isCarSelected]="!!selectedCar"
(returnToMainWindow)="resetCarSection()">
</app-card>
<mat-card class="form-card">
<mat-card-header>
<mat-card-title>
Klient
<ng-container *ngIf="clientType === ClientType.PRIVATE">prywatny</ng-container>
<ng-container *ngIf="clientType === ClientType.BUSINESS">biznesowy</ng-container>
wprowadź dane
</mat-card-title>
</mat-card-header>
<mat-card-content class="card-content">
<app-private-client-form *ngIf="clientType === ClientType.PRIVATE"
(discountCounted)="discountCounted($event)"> <!-- dodanie przypisania do output-->
</app-private-client-form>
<app-business-client-form *ngIf="clientType === ClientType.BUSINESS"
(discountCounted)="discountCounted($event)"> <!-- dodanie przypisania do output-->
</app-business-client-form>

<!-- wyświetlenie odpowiedniego komunikatu w zależnosci czy przysługuje zniżka-->
<div class="discount"
*ngIf="discountAmount && discountAmount > 0; else noDiscountAvailable">
<p>Przysługuje zniżka:
<span class="discount-available">{{discountAmount}}</span>
<span> </span>
</p>
<ng-container *ngIf="carPriceAfterDiscount">
<p>Cena samochodu po zniżce:
<span class="discount-available">{{carPriceAfterDiscount}}</span>
<span> </span>
</p>
</ng-container>
</div>
<ng-template #noDiscountAvailable>
<div *ngIf="discountAmount === 0" class="discount">
<span class="no-discount">Niestety nie przysługuje zniżka.</span>
</div>
</ng-template>
</mat-card-content>
</mat-card>
</ng-template>
</div>

dostosuj jeszcze plik ze stylami app.component.scss

.main-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 60px;
}

.card {
margin: 10px;
}

.info-card {
display: flex;
width: 33%;
}

.form-card {
display: flex;
width: 66%;
flex-direction: column;
align-items: center;

.card-content {
width: 100%;

.form-container {
display: flex;
flex-direction: column;
align-items: center;

.form-input {
width: 50%;
}
}
}
}

// <--------- początek
.no-discount {
color: red;
}

.discount-available {
color: green;
}

.discount {
font-size: 24px;
margin-top: 20px;
text-align: center;
}
// ---------> koniec

Przejdź do aplikacji localhost:4200 i przetestuj rozwiązanie.

Powinieneś zobaczyć następujące wyniki