Principiile programarii OOP

In acest articol o sa abordam un subiect despre care s-a mai vorbit in trecut pe acest blog, cu mentiunea ca de aceasta data vom aborda subiectul folosindu-ne si de exemple in cod. Cele patru principii despre care vom vorbi sunt: MOSTENIRE, ABSTRACTIZARE, INCAPSULARE si POLIMORFISM. Varianta pe care am ales-o pentru explicarea acestor principii este aceea de a scrie o singura clasa si a o adapta in functie de principiul explicat cu exemple concrete. Limbajul de programare ales pentru exemplificare, este TypeScript (un superset al limbajului de programare JavaScript). Clasa de baza este una destul de simpla:

class Car {
    id: number;
    fuel: string;

    constructor(id: number, fuel: string){
        this.id = id;
        this.fuel = fuel;
    }

    start(): void {
        console.log('Car started.');
    }

    stop(): void {
        console.log('Car stoped.');
    }

    getId(): number {
        return this.id;
    }
}

Mostenire

Mostenirea reprezinta capacitatea de a scrie o clasa care extinde o alta clasa, adaugand functionalitati noi la clasa-parinte sau clasa de baza (se mai gaseste si sub denumirea de super-clasa). Continuand exemplul de mai sus, vom scrie o clasa derivata, denumita Taxi, care va extinde clasa Car. Aceasta va mosteni o parte din functionalitatile clasei de baza, pentru a se folosi de ele si a adauga noi functionalitati. Clasa derivata spre deosebire de clasa de baza, va contine doua metode pentru inceperea respectiv terminarea unei curse a unui taxi. In implementarea celor doua metode, vom avea nevoie de metodele de start respectiv stop, din clasa de baza. De asemenea, vom avea nevoie si de o a treia metoda prin care clasa derivata foloseste id-ul masinii din clasa de baza.

class Taxi extends Car {
    company: string;

    constructor(id: number, fuel: string, company: string){
        super(id, fuel); // call the parent class constructor
        this.company = company;
    }

    startRide(): void {
        console.log(this.company, 'taxi:', super.getId(), 'is busy.');
        super.start();
    }

    finishRide(): void {
        super.stop();
        console.log(this.company, 'taxi:', super.getId(), 'is free.');
    }
}

Astfel, daca rulam urmatorul cod:

const taxi = new Taxi(456, 'diesel', 'GoCab');
taxi.startRide();
console.log('Ride in progress...');
taxi.finishRide();

Vom obtine in consola:

GoCab taxi: 456 is busy.
Car started.
Ride in progress...
Car stoped.
GoCab taxi: 456 is free.

Astfel cu ajutorul mostenirii, putem extinde o clasa de baza si sa adaugam functionalitati pentru cazuri mai specifice, fara a o altera.

Abstractizare

Reprezinta procesul prin care se “ascund” anumite functionalitati pe care nu e nevoie sa le cunoastem in detaliu, acest lucru ducand la o mai buna modularizare a aplicatiei. Luand in calcul exemplul de mai sus, putem considera ca in clasa derivata Taxi, nu ne intereseaza cu exactitate functionalitatea din spatele metodei start. Tot ce ne intereseaza este ca ea sa functioneze optim pentru a o putea folosi in metoda startRide a clasei derivate. Similar se poate spune si despre celelalte doua metode folosite din clasa de baza: stop respectiv getId, tot ce vrem este sa le folosim cu success in clasa derivata. Un nivel ridicat de abstractizare a aplicatiei poate ajuta la mentenanta si scalabilitatea ei pe termen lung daca este folosita optim. In caz contrar, daca se abuzeaza de abstractizare aplicatia deveine greu de urmarit, codul devine “spaghetti code”.

Incapsulare

Incapsularea reprezinta principiul prin care facem inaccesibil accesul din exterior asupra unor proprietati sau metode dintr-o clasa. Incapsularea este folosita in general pentru procesele interne dintr-o clasa, atunci cand vrem sa fim siguri ca nimeni nu va altera respectivele procese din exterior. Incapsularea se face cu ajutorul modificatorilor de acces: public, private si protected.


Public: oricine poate accesa proprietatea sau metoda (in unele limbaje nu trebuie specificat, o metoda sau proprietate are public by default, daca nu e specificat alt modificator de acces).
Protected: metodele sau proprietatile se pot accesa doar din clasa derivata.
Private: metodele sau proprietatile nu pot fi accesate din exterior.

Daca o proprietate incapsulata dintr-o clasa trebuie cu orice pret modificata sau accesata, pentru a nu altera nivelul de accesibilitate al proprietatii respective, putem sa definim un getter pentru a accesa valoarea, respectiv un setter pentru a o modifica.


Spre exemplu in clasa Car putem avea un process intern legat de starea motorului, denumit engineStart, process ce ar trebui sa tina strict de clasa Car, deci il incapsulam. Metodele de start si stop, sunt publice, pentru a putea fi accesate de catre clasa derivata.

class Car {
    id: number;
    fuel: string;
    private engineStart: boolean = false;

    constructor(id: number, fuel: string){ ... }

    public get engineStatus(): boolean {
        return this.engineStart;
    }

    public set engineStatus(status: boolean) {
        this.engineStart = status;
    }

    start(): void {
        this.engineStart = true;
        console.log('Car started.');
    }

    stop(): void {
        this.engineStart = false;
        console.log('Car stoped.');
    }
}

Polimorfism

Este procesul prin care putem sa creem un numar nelimitat de obiecte cu ajutorul aceleasi clase, bazandu-ne pe inputurile pe care i le furnizam. Continuand pe exemplul cu clasa Car, o sa modificam clasa si o sa-i atribuim mai multe proprietati pentru a intelege polimorfismul cat mai bine.

class Car{
    id: number;
    fuel: string;
    ownerName: string;
    transmitionType: string;
    color: string;

    constructor(id: number, fuel: string, ownerName: string, transmitionType: string, color: string){
        this.id = id;
        this.fuel = fuel;
        this.ownerName = ownerName;
        this.transmitionType = transmitionType;
        this.color = color;
    }

    details(): void {
        console.log(this.id, this.fuel, this.ownerName, this.transmitionType, this.color);
    }
}

Vom instantia un array cu mai multe masini diferite, ulterior vom apela pentru fiecare obiect metoda de details in felul urmator:

const cars = [
    new Car(123, 'hybrid', 'John Doe', 'automatic', 'red'),
    new Car(456, 'electric', 'Janet Doe', 'automatic', 'blue'),
    new Car(879, 'electric', 'Max Doe', 'automatic', 'green'),
    new Car(101, 'hybrid', 'Michelle Doe', 'automatic', 'red')
];

for(const car of cars) {
    car.details();
}

Astfel vom afisa in consola:

123 hybrid John Doe automatic red
456 electric Janet Doe automatic blue
879 electric Max Doe automatic green
101 hybrid Michelle Doe automatic red

Cu cat o clasa respecta mai mult acest principiu, se va evita scrierea de cod duplicat la nivel de aplicatie.

Avantajul principiilor programarii orientate pe obiect este acela ca faciliteaza dezvoltarea aplicatiilor mult mai rapid, evitand codul duplicat si pastrand linia foarte fina dintre descifrabilitatea cat mai buna a unui cod si eficienta lui. De asemenea o aplicare cat mai buna a acestor principii ajuta la mentenanta aplicatiei pe termen lung.

Edward Vlad

Related post