Εισαγωγή στον αντικειμενοστρεφή προγραμματισμό σε JavaScript

Αυτό το άρθρο απευθύνεται σε μαθητές JavaScript που δεν έχουν καμία προηγούμενη γνώση σε αντικειμενοστρεφή προγραμματισμό (OOP). Εστιάζω στα μέρη του OOP που σχετίζονται μόνο με το JavaScript και όχι το OOP γενικά. Παραλείπω τον πολυμορφισμό επειδή ταιριάζει καλύτερα με μια στατική-τυποποιημένη γλώσσα.

Γιατί πρέπει να το γνωρίζετε αυτό;

Έχετε επιλέξει το JavaScript για να είναι η πρώτη σας γλώσσα προγραμματισμού; Θέλετε να είστε ένας προγραμματιστής που εργάζεται σε γιγαντιαία εταιρικά συστήματα που εκτείνονται σε εκατοντάδες χιλιάδες γραμμές κώδικα ή περισσότερα;

Εάν δεν μάθετε να αγκαλιάζετε πλήρως τον αντικειμενοστρεφή προγραμματισμό, θα είστε καλά και πραγματικά χαμένοι.

Διαφορετικές νοοτροπίες

Στο ποδόσφαιρο, μπορείτε να παίξετε από μια ασφαλή άμυνα, μπορείτε να παίξετε με ψηλές μπάλες από τις πλευρές ή μπορείτε να επιτεθείτε όπως δεν υπάρχει αύριο. Όλες αυτές οι στρατηγικές έχουν τον ίδιο στόχο: να κερδίσουν το παιχνίδι.

Το ίδιο ισχύει και για τον προγραμματισμό παραδειγμάτων. Υπάρχουν διαφορετικοί τρόποι προσέγγισης ενός προβλήματος και σχεδιασμός μιας λύσης.

Ο αντικειμενοστραφής προγραμματισμός, ή OOP, είναι το πρότυπο για τη σύγχρονη ανάπτυξη εφαρμογών. Υποστηρίζεται από μεγάλες γλώσσες όπως Java, C # ή JavaScript.

Το Αντικειμενοστρεφές Παράδειγμα

Από την οπτική γωνία OOP, μια εφαρμογή είναι μια συλλογή «αντικειμένων» που επικοινωνούν μεταξύ τους. Βασίζουμε αυτά τα αντικείμενα σε πράγματα στον πραγματικό κόσμο, όπως προϊόντα σε απογραφή ή αρχεία υπαλλήλων. Τα αντικείμενα περιέχουν δεδομένα και εκτελούν κάποια λογική με βάση τα δεδομένα τους. Ως αποτέλεσμα, ο κωδικός OOP είναι πολύ κατανοητός. Αυτό που δεν είναι τόσο εύκολο είναι να αποφασίσουμε πώς να σπάσουμε μια εφαρμογή σε αυτά τα μικρά αντικείμενα.

Αν ήμουν σαν εμένα όταν το άκουσα για πρώτη φορά, δεν έχετε ιδέα τι σημαίνει αυτό - όλα ακούγονται πολύ αφηρημένα. Το να νιώθεις έτσι είναι εντάξει. Είναι πιο σημαντικό να έχετε ακούσει την ιδέα, να τη θυμάστε και να προσπαθήσετε να εφαρμόσετε το OOP στον κωδικό σας. Με την πάροδο του χρόνου, θα αποκτήσετε εμπειρία και θα ευθυγραμμίσετε περισσότερο τον κώδικά σας με αυτήν τη θεωρητική ιδέα.

Μάθημα : Το OOP που βασίζεται σε αντικείμενα πραγματικού κόσμου επιτρέπει σε οποιονδήποτε να διαβάσει τον κωδικό σας και να καταλάβει τι συμβαίνει.

Αντικείμενο ως κεντρικό τεμάχιο

Ένα απλό παράδειγμα θα σας βοηθήσει να δείτε πώς η JavaScript εφαρμόζει τις θεμελιώδεις αρχές του OOP. Εξετάστε μια περίπτωση χρήσης αγορών στην οποία βάζετε προϊόντα στο καλάθι σας και, στη συνέχεια, υπολογίστε τη συνολική τιμή που πρέπει να πληρώσετε. Εάν λάβετε τις γνώσεις σας στο JavaScript και κωδικοποιήσετε τη θήκη χρήσης χωρίς OOP, θα μοιάζει με αυτό:

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 καθιστά ευκολότερη τη σύνταξη καλύτερου κώδικα επειδή σκεφτόμαστε αντικείμενα όπως θα τα συναντούσαμε στον πραγματικό κόσμο. Καθώς η θήκη χρήσης μας περιέχει ένα καλάθι προϊόντων, έχουμε ήδη δύο είδη αντικειμένων - το αντικείμενο καλαθιού και τα αντικείμενα του προϊόντος.

Η έκδοση OOP της υπόθεσης χρήσης αγορών θα μπορούσε να γραφτεί όπως:

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();

Όπως μπορείτε να δείτε στην πρώτη γραμμή, δημιουργούμε ένα νέο αντικείμενο χρησιμοποιώντας τη λέξη-κλειδί newακολουθούμενη από το όνομα αυτού που ονομάζεται τάξη (περιγράφεται παρακάτω). Αυτό επιστρέφει ένα αντικείμενο που αποθηκεύουμε στο μεταβλητό ψωμί. Το επαναλαμβάνουμε αυτό για το μεταβλητό νερό και ακολουθούμε παρόμοιο μονοπάτι για να δημιουργήσουμε ένα μεταβλητό καλάθι. Αφού προσθέσετε αυτά τα προϊόντα στο καλάθι σας, τελικά εκτυπώνετε το συνολικό ποσό που πρέπει να πληρώσετε.

Η διαφορά μεταξύ των δύο αποσπασμάτων κώδικα είναι προφανής. Η έκδοση OOP σχεδόν διαβάζει σαν πραγματικές αγγλικές προτάσεις και μπορείτε εύκολα να καταλάβετε τι συμβαίνει.

Μάθημα : Ένα αντικείμενο μοντελοποιημένο σε πραγματικό κόσμο αποτελείται από δεδομένα και λειτουργίες.

Κατηγορία ως πρότυπο

Χρησιμοποιούμε τάξεις στο OOP ως πρότυπα για τη δημιουργία αντικειμένων. Ένα αντικείμενο είναι μια «παρουσία μιας τάξης» και η «instantiation» είναι η δημιουργία ενός αντικειμένου που βασίζεται σε μια τάξη. Ο κώδικας ορίζεται στην κλάση αλλά δεν μπορεί να εκτελεστεί εκτός εάν βρίσκεται σε ζωντανό αντικείμενο.

Μπορείτε να δείτε μαθήματα όπως τα σχεδιαγράμματα ενός αυτοκινήτου. Καθορίζουν τις ιδιότητες του αυτοκινήτου όπως ροπή και ιπποδύναμη, εσωτερικές λειτουργίες όπως αναλογίες αέρα προς καύσιμο και μεθόδους προσβάσιμες από το κοινό, όπως η ανάφλεξη. Μόνο όταν ένα εργοστάσιο ενεργοποιεί το αυτοκίνητο, ωστόσο, μπορείτε να γυρίσετε το κλειδί και να οδηγήσετε.

Στην περίπτωσή μας, χρησιμοποιούμε την κατηγορία προϊόντων για να δημιουργήσουμε δύο αντικείμενα, το ψωμί και το νερό. Φυσικά, αυτά τα αντικείμενα χρειάζονται κωδικό που πρέπει να παρέχετε στις τάξεις. Πάει κάπως έτσι:

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()); };}

Μια τάξη σε JavaScript μοιάζει με μια συνάρτηση, αλλά τη χρησιμοποιείτε διαφορετικά. Το όνομα της συνάρτησης είναι το όνομα της τάξης και είναι κεφαλαία. Δεδομένου ότι δεν επιστρέφει τίποτα, δεν καλούμε τη συνάρτηση με τον συνηθισμένο τρόπο όπως const basket = Product('bread', 1);. Αντ 'αυτού, προσθέτουμε τη λέξη-κλειδί νέα like const basket = new Product('bread', 1);.

Ο κωδικός μέσα στη συνάρτηση είναι ο κατασκευαστής. Αυτός ο κωδικός εκτελείται κάθε φορά που ένα αντικείμενο δημιουργείται. Το προϊόν έχει τις παραμέτρους _nameκαι _price. Κάθε νέο αντικείμενο αποθηκεύει αυτές τις τιμές μέσα σε αυτό.

Επιπλέον, μπορούμε να ορίσουμε συναρτήσεις που θα παρέχει το αντικείμενο. Προσδιορίζουμε αυτές τις λειτουργίες προπαρασκευάζοντας αυτήν τη λέξη-κλειδί που τις καθιστά προσβάσιμες από το εξωτερικό (βλ. Encapsulation) Σημειώστε ότι οι συναρτήσεις έχουν πλήρη πρόσβαση στις ιδιότητες.

Class Basket doesn’t require any arguments to create a new object. Instantiating a new Basket object simply generates an empty list of products that the program can fill afterwards.

Lesson: A class is a template for generating objects during runtime.

Encapsulation

You may encounter another version of how to declare a class:

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

Mind the assignment of the properties to the variable this. At first sight, it seems to be a better version because it doesn’t require the getter (getName & getPrice) methods anymore and is therefore shorter.

Unfortunately, you have now given full access to the properties from the outside. So everybody could access and modify it:

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_προγραμματισμός