Clasa vs Interfata

Ca tot se vorbea despre JS zilele astea, am si eu o curiozitate: ce folositi pentru a defini obiectele … clasa sau interfata?

Lucrand cu angular, pana acum aproape un an, obisnuiam sa folosesc clase cu constructor. Eh, dupa ce-am facut un curs la o firma de outsourcing, am observat ca se evita folosirea lor, in schimb sunt utilizate interfete. Dupa ceva cautari pe net, am descoperit ca, interfetele sunt incarcate doar la runtime, in timp ce clasele cresc sizeul aplicatiei (?!?!?! gresesc?). Zic bun, am inceput sa folosesc si eu interfete :)) problema cu ele este ca nu pot fi instantiate. Daca vreau sa creez un obiect cu campuri default, trebuie sa enumar tot carnatul de parametrii.

When not to use interfaces : When you want to have default values, implementations, constructors, or functions (not just signatures).
When not to use classes : When you have a simple data interface, do not need to instantiate it, when you want to have it implemented by other objects
sursa: Class vs interface

avand o aplicatie cu domeniile Product, Order, Price, Document, folosind clase pentru a mapa fiecare obiect din backend, ii puteam da foarte usor valori default. Altfel, folosind interfata, pentru obiectul Product, sunt nevoie sa enumar 30 fielduri in componenta:

export interface Product {
   id: number;
   name: string;
   ...
}

vs

export class Product {
   id: number;
   name: string;
   ...
   constructor(obj: any) {
      this.id = obj && obj.id || -1;
      this.name = obj && obj.name || '';
     ..
  }
}

folosirea in componenta:

const instanceObj: Product = {
    id: -1,
    name: '',
    ...
   }

vs

const classObj = new Product({id: 73}); // dau valori anumitor fielduri, restul sunt cele default din constructor

Ma gandesc ca pentru acest tip de date, oricine foloseste varianta A sau B, nu ambele, in functie de cate fielduri are fiecare obiect, altfel s-ar pierde din consistenta.

Ce nu-mi place … initierea unei interfete cu valori default in componenta pare ca duce la cresterea codului in nr de linii inutil.

Voi cum faceti? :unamused:

Nu cred că ai o comparație foarte bună între clase vs interfețe.

Interfețele sunt un contract. Ceva ce-ți garantează că o clasă implementează anumite metode într-un anumit fel. Nu contează cum implementează, cât timp acceptă anumiți parametri și întoarce un anumit tip.

Mergând mai departe pe exemplul tău, dacă vrei să ai o clasă Product și mai ai încă o clasă Subscription, vei observa că vei avea două seturi de proprietăți foarte similare. Cum te asiguri că ambele au același comportament pentru obj.getName() de exemplu?

O interfață nu poate fi inițializată[1] și nici nu ai nevoie de asta, din moment ce nu (ar trebui) să conțină date.

Uită-te peste principiile SOLID, în special segregarea interfețelor.


  1. în PHP cel puțin, nu știu cum e TS ↩︎

1 Like

Din ce stiu, ce spui tu aici este dependency injection, cel mai des folosit in backend, unde definesti interfata cu metodele, iar clasa le implementeaza. exemplu:

 public interface IProductsService {
    public Product GetById(id: Int)
 }
 public class ProductsService extends IProductsService {
    public Product GetById(id: Int) {
      repository.GetById(id)
   }
 }
 class Product(id: Int, name: String) { // nici o logica aici, e doar un DTO }

fix de partea boldata eram curios. In angular, poti extinde interfete (de tip serviciu ca cea de mai sus), insa pot fi folosite si ca obiecte simple (probabil posibil si in celelalte limbaje), ce contin doar fielduri. exemplu: rezultatul unui http request ajuns in frontend este mapat intr-o interfata (in loc de clasa).

Eu nu am acel obj.getName() pentru ca nu am logica in obiectul respectiv, ci doar campurile lui, name-ul il pot obtine prin obj.name sau daca am ceva mai complex de facut, il las in serviciul entitatii respective (ProductsService)

recunosc, nu stapanesc foarte bine patternurile :woozy_face: :rofl:


  1. în PHP cel puțin, nu știu cum e TS ↩︎

Interfața e pentru type definition, cu interfete poți face overloading la clase, poți extinde interfetele cu alte interfețe.

Cauta type vs interface.

Poți utiliza un obiect ca și type cu typeof, dar nu poți utiliza o Interfața ca și un obiect. (În TS Interfața are rolul de type definition, fiindcă poate pui any/unknown si nu mai are rol de contract, oricum nu are rol de contract la rulare)

Eu vad o Interfața aproape egala cu type. Type-urile le poti extinde cu Union, interfețele cu extends. La type-uri poti folosi de exemplu [key in Keys]: string;
iar Keys e un Union type de string-uri (computed type). Type-urile sunt mai puternice cu computed type fiindcă poți face magie neagră cu type literals si să generezi n type-uri. La interfețe nu ai computed type fiindcă din OOP vine că o clasă implementeaza o Interfața. Ar insemna sa ti se schimbe si implementarea dinamic dacă se schimbă Interfața.
Cum poti avea o Interfața cu același nume dar proprietati cu type-uri diferite îți permite să faci clase cu același nume, dar pe type-uri separate. (Generice)

Amândouă sunt deferred si se rulează înainte de rulare.

Clasa e un obiect în JS, e syntactic sugar pentru prototipuri. Dacă ai folosit Java/C++ acolo ai interfețe adevărate, în JS nu ai type-uri și nici interfețe, TS ti le da dar singurele avantaje sunt autocomplete, documentatie și static type checking înainte de rulare. Nu o să ruleze în timpul rulării. In cel mai bun caz folosești interfețele/type-urile sa setezi un validator de json/interface checker/form validator la runtime dar mănâncă resurse.

Angular are un DI specific care nu e standard in TS sau JS. În JS ai module și poți evita DI din exports sau faci simplu fatade la tot ce ai vrea sa înlocuiești.

2 Likes

Strict pentru Typescript si Angular: interfetele merg foarte bine cu ideea de “immutable data” unde trebuie tot timpul sa recreezi obiectele in loc sa le mutezi. E mult mai “cheap” si mai performant sa recreezi obiecte care contin doar date decat sa reinstantiezi mereu obiecte care au behavior.

La fel si pentru cazul in care folosesti Redux (sau NgRx in Angular) si serializezi state-ul.
Una e sa serializezi si deserializezi obiecte care contin doar date si cu totul altceva sa serializezi obiecte cu behavior (constructor si metode).

Astfel eu incerc sa separ complet datele de behavior.
Pentru date folosesc doar interfete, iar pentru behavior folosesc “pure functions” si clase (uneori clase statice pentru a grupa mai multe pure functions intr-un namespace).

Pentru servicii nu folosesc niciodata interfete pentru ca Angular DI nu accepta interfete ca providers, ci doar clase.
Asa ca folosesc clase abstracte pentru repositories sau pentru servicii la care ma astept sa am mocks pentru unit testing.

Pe exemplul tau, Product la mine ar fi interfata, iar daca am nevoie de behavior sau alte metode ajutatoare pentru Product, atunci mai creez o clasa ProductModel unde, de regula, pun doar functii statice:

export interface Product {
  id: number;
  name: string;
}

export class ProductModel {
  static create(id: number, string: string): Product {
    return {
      id: number || -1,
      name: name || ''
    };
  }
}

Astfel ma asigur ca state-ul aplicatiei va contine intotdeauna doar date si poate fi mutat si serializat/deserializat foarte usor, iar behavior-ul este in alta parte.

Chiar daca folosesti clase pentru model, cu constructor si poate cateva metode ajutatoare, este o iluzie sa crezi ca poti cuprinde toata logica aplicatiei in niste clase de tip model (cum e Product in exemplul tau).
In aplicatii reale ai mult mai mult “business logic” pe care daca nu il izolezi in model sau domain sau application core (se folosesc mai multe denumire), folosind OOP si diverse design patterns (application core-ul trebuie sa fie framework agnostic pe cat de mult posibil), atunci o sa ai business logic peste tot prin aplicatie (de regula prin componente, deci in presentation), ceea ce e foarte rau.

2 Likes

multumesc pentru raspunsuri.

de aici confuzia mea. Nu stiu cum am nimerit, dar in mare parte din proiecte, logica n-o scriu/n-am vazut-o scrisa in model (modelul e doar un dto si atat), ci in serviciu. Poate doar ceva validatoare simple pentru campuri, dar cam atat. Asta pe .Net core. Singura data unde am apucat sa vad logica in model a fost pe o aplicatie desktop C# pe MVVM.

Astfel eu incerc sa separ complet datele de behavior. :+1: :+1: :+1:

mi-ai explicat tu mai multe octav, dar din lipsa de timp/chef/oboseala/vreme nasoala, tot pierd timp pe chestii inutile :rofl: :man_facepalming: