Uvod u objektno orijentirano programiranje u JavaScript-u

Ovaj je članak namijenjen studentima JavaScript-a koji nemaju predznanje iz objektno-orijentiranog programiranja (OOP). Usredotočujem se na dijelove OOP-a koji su relevantni samo za JavaScript, a ne OOP općenito. Preskačem polimorfizam jer se bolje uklapa u jezik statičkog tipa.

Zašto to trebate znati?

Jeste li odabrali JavaScript za svoj prvi programski jezik? Želite li biti popularni programer koji radi na divovskim sustavima poduzeća koji obuhvaćaju stotine tisuća linija koda ili više?

Ako ne naučite u potpunosti prihvatiti objektno orijentirano programiranje, bit ćete dobro i istinski izgubljeni.

Različiti mentalni sklopovi

U nogometu možete igrati iz sigurne obrane, možete igrati visoke lopte sa strane ili možete napadati kao da sutra nema. Sve ove strategije imaju isti cilj: Pobijediti u igri.

Isto vrijedi i za paradigme programiranja. Postoje različiti načini pristupanja problemu i dizajniranja rješenja.

Objektno orijentirano programiranje ili OOP paradigma je za moderni razvoj aplikacija. Podržavaju ga glavni jezici poput Java, C # ili JavaScript.

Objektno orijentirana paradigma

Iz perspektive OOP-a, aplikacija je zbirka "predmeta" koji međusobno komuniciraju. Te predmete zasnivamo na stvarima u stvarnom svijetu, poput proizvoda u inventaru ili evidenciji zaposlenika. Objekti sadrže podatke i izvode neku logiku na temelju svojih podataka. Kao rezultat toga, OOP kod je vrlo lako razumljiv. Ono što nije tako lako je odlučiti kako uopće razbiti aplikaciju na te male predmete.

Ako ste bili poput mene kad sam to prvi put čuo, nemate pojma što ovo zapravo znači - sve zvuči vrlo apstraktno. Tako se osjećati apsolutno je u redu. Važnije je da ste ideju čuli, zapamtili je i pokušali primijeniti OOP u svom kodu. S vremenom ćete steći iskustvo i uskladiti više svog koda s ovim teorijskim konceptom.

Lekcija : OOP zasnovan na stvarnim objektima omogućuje bilo kome da pročita vaš kôd i shvati što se događa.

Objekt kao središnji dio

Jednostavan primjer pomoći će vam da vidite kako JavaScript provodi temeljna načela OOP-a. Razmotrite slučaj korištenja kupovine u koji stavljate proizvode u svoju košaru, a zatim izračunajte ukupnu cijenu koju morate platiti. Ako uzmete svoje znanje o JavaScript-u i kodirate slučaj upotrebe bez OOP-a, to bi izgledalo ovako:

const bread = {name: 'Bread', price: 1};const water = {name: 'Water', price: 0.25};
const basket = [];basket.push(bread);basket.push(bread);basket.push(water);basket.push(water);basket.push(water);
const total = basket .map(product => product.price) .reduce((a, b) => a + b, 0);
console.log('one has to pay in total: ' + total);

OOP perspektiva olakšava pisanje boljeg koda jer o objektima razmišljamo onako kako bismo ih susreli u stvarnom svijetu. Kako naš slučaj upotrebe sadrži košaru proizvoda, već imamo dvije vrste predmeta - predmet košarice i predmete proizvoda.

OOP verzija slučaja korištenja za kupovinu mogla bi se napisati ovako:

const bread = new Product('bread', 1);const water = new Product('water', .25)const basket = new Basket();basket.addProduct(2, bread);basket.addProduct(3, water);basket.printShoppingInfo();

Kao što možete vidjeti u prvom retku, stvaramo novi objekt pomoću ključne riječi newnakon koje slijedi naziv onoga što se naziva klasa (opisano u nastavku). Ovo vraća objekt koji pohranjujemo u varijablu bread. Ponavljamo to za varijabilnu vodu i idemo sličnim putem za stvaranje promjenjive košarice. Nakon što dodate ove proizvode u košaricu, konačno ispisujete ukupan iznos koji morate platiti.

Razlika između dva isječka koda je očita. Verzija OOP-a gotovo se čita kao prave engleske rečenice i lako možete reći što se događa.

Lekcija : Objekt po uzoru na stvari iz stvarnog svijeta sastoji se od podataka i funkcija.

Predavanje kao predložak

Klase u OOP-u koristimo kao predloške za stvaranje objekata. Objekt je "instanca klase", a "instancija" je stvaranje objekta na temelju klase. Kôd je definiran u klasi, ali se ne može izvršiti ako nije u aktivnom objektu.

Satove možete gledati poput nacrta automobila. Oni definiraju svojstva automobila poput okretnog momenta i konjskih snaga, unutarnje funkcije poput omjera zrak-gorivo i javno dostupne metode poput paljenja. Međutim, tek kad tvornica pokrene automobil, možete okrenuti ključ i voziti.

U našem slučaju korištenja koristimo klasu Product za instanciranje dva predmeta, kruha i vode. Naravno, tim objektima je potreban kod koji morate navesti u klasama. To ide ovako:

function Product(_name, _price) { const name = _name; const price = _price;
this.getName = function() { return name; };
this.getPrice = function() { return price; };}
function Basket() { const products = [];
this.addProduct = function(amount, product) { products.push(...Array(amount).fill(product)); };
this.calcTotal = function() { return products .map(product => product.getPrice()) .reduce((a, b) => a + b, 0); };
this.printShoppingInfo = function() { console.log('one has to pay in total: ' + this.calcTotal()); };}

Predaja u JavaScript-u izgleda kao funkcija, ali je koristite drugačije. Ime funkcije je ime klase i napisano je velikim slovima. Budući da ne vraća ništa, ne pozivamo funkciju na uobičajeni način kao const basket = Product('bread', 1);. Umjesto toga dodajemo ključnu riječ novi like const basket = new Product('bread', 1);.

Kôd unutar funkcije je konstruktor. Ovaj se kôd izvršava svaki put kada se objekt instancira. Proizvod ima parametre _namei _price. Svaki novi objekt u sebi pohranjuje te vrijednosti.

Nadalje, možemo definirati funkcije koje će objekt pružiti. Te funkcije definiramo dodavanjem ove ključne riječi što ih čini dostupnima izvana (vidi Enkapsulacija). Primijetite da funkcije imaju puni pristup svojstvima.

Class Basket ne zahtijeva nikakve argumente za stvaranje novog objekta. Instanciranje novog objekta košarice jednostavno generira prazan popis proizvoda koje program može naknadno napuniti.

Lekcija : Klasa je predložak za generiranje objekata tijekom izvođenja.

Kapsulacija

Možda ćete naići na drugu verziju kako prijaviti klasu:

function Product(name, price) { this.name = name; this.price = price;}

Pazite na dodjeljivanje svojstava varijabli this. Na prvi pogled čini se da je to bolja verzija jer više ne zahtijeva metode getter (getName & getPrice) i stoga je kraća.

Nažalost, sada ste omogućili potpuni pristup svojstvima izvana. Dakle, svi su mogli pristupiti i izmijeniti ga:

const bread = new Product('bread', 1);bread.price = -10;

This is something you don’t want as it makes the application more difficult to maintain. What would happen if you added validation code to prevent, for example, prices less than zero? Any code that accesses the price property directly would bypass the validation. This could introduce errors that would be difficult to trace. Code that uses the object’s getter methods, on the other hand, is guaranteed to go through the object’s price validation.

Objects should have exclusive control over their data. In other words, the objects “encapsulate” their data and prevent other objects from accessing the data directly. The only way to access the data is indirect via the functions written into the objects.

Data and processing (aka logic) belong together. This is especially true when it comes to larger applications where it is very important that processing data is restricted to specifically-defined places.

Done right, OOP produces modularity by design, the holy grail in software development. It keeps away the feared spaghetti-code where everything is tightly coupled and you don’t know what happens when you change a small piece of code.

In our case, objects of class Product don’t let you change the price or the name after their initialization. The instances of Product are read-only.

Lesson: Encapsulation prevents access to data except through the object’s functions.

Inheritance

Inheritance lets you create a new class by extending an existing class with additional properties and functions. The new class “inherits” all of the features of its parent, avoiding the creation of new code from scratch. Furthermore, any changes made to the parent class will automatically be available to the child class. This makes updates much easier.

Let’s say we have a new class called Book that has a name, a price and an author. With inheritance, you can say that a Book is the same as a Product but with the additional author property. We say that Product is the superclass of Book and Book is a subclass of Product:

function Book(_name, _price, _author) { Product.call(this, _name, _price); const author = _author; this.getAuthor = function() { return author; }}

Note the additional Product.call along the this as the first argument. Please be aware: Although book provides the getter methods, it still doesn’t have direct access to the properties name and price. Book must call that data from the Product class.

You can now add a book object to the basket without any issues:

const faust = new Book('faust', 12.5, 'Goethe');basket.addProduct(1, faust);

Basket expects an object of type Product. Since book inherits from Product through Book, it is also a Product.

Lesson: Subclasses can inherit properties and functions from superclasses while adding properties and functions of their own.

JavaScript and OOP

You will find three different programming paradigms used to create JavaScript applications. They are Prototype-Based Programming, Object-Oriented Programming and Functional-Oriented Programming.

The reason for this lies in JavaScript’s history. Originally, it was prototype-based. JavaScript was not intended as a language for large applications.

Against the plan of its founders, developers increasingly used JavaScript for bigger applications. OOP was grafted on top of the original prototype-based technique.

The prototype-based approach is shown below. It is seen as the “classical and default way” to construct classes. Unfortunately it does not support encapsulation.

Even though JavaScript’s support for OOP is not at the same level as other languages like Java, it is still evolving. The release of version ES6 added a dedicated class keyword we could use. Internally, it serves the same purpose as the prototype property, but it reduces the size of the code. However, ES6 classes still lack private properties, which is why I stuck to the “old way”.

For the sake of completeness, this is how we would write the Product, Basket and Book with ES6 classes and also with the prototype (classical and default) approach. Please note that these versions don’t provide encapsulation:

// ES6 version
class Product { constructor(name, price) { this.name = name; this.price = price; }}
class Book extends Product { constructor(name, price, author) { super(name, price); this.author = author; }}
class Basket { constructor() { this.products = []; }
 addProduct(amount, product) { this.products.push(…Array(amount).fill(product)); }
 calcTotal() { return this.products .map(product => product.price) .reduce((a, b) => a + b, 0); }
 printShoppingInfo() { console.log('one has to pay in total: ' + this.calcTotal()); }}
const bread = new Product('bread', 1);const water = new Product('water', 0.25);const faust = new Book('faust', 12.5, 'Goethe');
const basket = new Basket();basket.addProduct(2, bread);basket.addProduct(3, water);basket.addProduct(1, faust);basket.printShoppingInfo();
//Prototype versionfunction Product(name, price) { this.name = name; this.price = price;}function Book(name, price, author) { Product.call(this, name, price); this.author = author;}Book.prototype = Object.create(Product.prototype);Book.prototype.constructor = Book;function Basket() { this.products = [];}Basket.prototype.addProduct = function(amount, product) { this.products.push(...Array(amount).fill(product));};Basket.prototype.calcTotal = function() { return this.products .map(product => product.price) .reduce((a, b) => a + b, 0);};Basket.prototype.printShoppingInfo = function() { console.log('one has to pay in total: ' + this.calcTotal());};

Lesson: OOP was added to JavaScript later in its development.

Summary

As a new programmer learning JavaScript, it will take time to appreciate Object-Oriented Programming fully. The important things to understand at this early stage are the principles the OOP paradigm is based on and the benefits they provide:

  • Objects modeled on real-world things are the centerpiece of any OOP-based application.
  • Encapsulation protects data from uncontrolled access.
  • Objects have functions that operate on the data the objects contain.
  • Classes are the templates used to instantiate objects.
  • Inheritance is a powerful tool for avoiding redundancy.
  • OOP is more verbose but easier to read than other coding paradigms.
  • Since OOP came later in JavaScript’s development, you may come across older code that uses prototype or functional programming techniques.

Further reading

  • //developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object-oriented_JS
  • //voidcanvas.com/es6-private-variables/
  • //medium.com/@rajaraodv/is-class-in-es6-the-new-bad-part-6c4e6fe1ee65
  • //developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Inheritance

    * //en.wikipedia.org/wiki/Object-oriented_programming

  • //en.wikipedia.org/wiki/Object-oriented_programming