Skip to the content.

2.8 Καλές τεχνικές προγραμματισμού

© Γιάννης Κωστάρας


<- Δ

Η συγγραφή κώδικα είναι τόσο επιστήμη όσο και τέχνη. Ένας καλός προγραμματιστής πρέπει να έχει υπόψιν του πολλές, πολλές φορές αντιδιαμετρικές, συνισταμένες ώστε να μπορέσει να γράψει ποιοτικό κώδικα.

Με τον όρο ποιοτικό κώδικα εννοούμε κώδικα που είναι:

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

Παρακάτω παρουσιάζουμε μερικές χρήσιμες συμβουλές που θα σας βοηθήσουν να γράφετε συντηρήσιμο και αποτελεσματικό κώδικα. Ο ποιοτικός κώδικας πρέπει να είναι: σωστός (bug free), επεκτάσιμος (extendable), συντηρήσιμος (maintenant), να μπορεί να τεστάρεται (testable), κλιμακωτός (scalable), αποδοτικός (performant) και ασφαλής (secure).

Αμετάβλητες (Immutable) κλάσεις

Κλάσεις που μπορούν ν’ αλλάξουν τις τιμές των γνωρισμάτων τους μετά τη δημιουργία τους λέγονται μεταβαλλόμενες (mutable) κλάσεις. Αυτές συνήθως διαθέτουν μεθόδους setXXX() που επιτρέπουν ν’ αλλάξουν την τιμή ενός γνωρίσματος. Μεταβάλλοντας την κατάσταση ενός αντικειμένου κατά τη διάρκεια εκτέλεσης ενός προγράμματος μερικές φορές είναι επικίνδυνο, ιδιαίτερα αν οι τιμές των γνωρισμάτων αλλάζουν από διαφορετικές διαδικασίες (processes) ή νήματα (threads) με μη ελεγχόμενα αποτελέσματα. Οι immutable classes είναι ευκολότερες να σχεδιαστούν και να υλοποιηθούν απ’ ότι οι mutable classes, πιο ασφαλής και είναι λιγότερο ευάλωτες σε λάθη.

Οι καλές τεχνικές προγραμματισμού προωθούν τη χρήση αμετάβλητων (immutable) κλάσεων. Οι τιμές των γνωρισμάτων (δηλ. η κατάσταση) ενός στιγμιοτύπου μιας immutable class δεν μπορούν (μπορεί) να αλλάξουν (αλλάξει) μετά τη δημιουργία του. Ας δούμε ένα παράδειγμα μιας αμετάβλητης κλάσης:

public final class Point {
	private final int x, y;
	
	public Point() {
		x=0;
		y=0;
	}
	
	public Point(int x, int y) {
		this.x = x;
		this.y = y;
	}
	
	public int getX() {
		return x;
	}
	
	public int getY() {
		return y;
	}
}

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

Ακολουθήστε τους παρακάτω πέντε κανόντες για να δημιουργήσετε αμετάβλητες κλάσεις:

  1. μην παρέχετε mutators (δηλ. μεθόδους που αλλάζουν τις τιμές των γνωρισμάτων της κλάσης)
  2. μην επιτρέψετε στην κλάση να κληρονομηθεί (ορίστε τη ως final)
  3. όλα τα γνωρίσματα της κλάσης πρέπει να είναι σταθερές
  4. όλα τα γνωρίσματα της κλάσης πρέπει να είναι private
  5. αν η κλάση σας διαθέτει μεταβλητά γνωρίσματα τότε αν δεν τα εκθέτετε σε άλλες κλάσεις η κλάση σας μπορεί να χαρακτηριστεί ως immutable

Η Java διαθέτει πολλά παραδείγματα αμετάβλητων κλάσεων όπως String, Double, Integer, Color, BigInteger και BigDecimal (αν και οι δυο τελευταίες δεν πληρούν όλα τα κριτήρια όπως θα δούμε σε επόμενο μάθημα) κλπ.

Είναι επίσης πολύ σημαντικό να καταλάβουμε τι σημαίνει η δήλωση π.χ. final Car car = new Car(...);. Η δήλωση αυτή δηλώνει ότι η μεταβλητή car είναι ένας δείκτης σε μια διεύθυνση της κύριας μνήμης η οποία αποθηκεύει ένα αντικείμενο τύπου Car. Η δήλωση final σημαίνει ότι ο δείκτης δεν μπορεί ν’ αλλάξει, δηλ. να λάβει μια νέα τιμή. Δε σημαίνει όμως ότι και το αντικείμενο στο οποίο δείχνει δεν μπορεί ν’ αλλάξει, εκτός κι αν το αντικείμενο αυτό είναι αμετάβλητο (immutable). Η κλάση Car όμως μπορεί να μεταβληθεί (mutable), όπως είδαμε στα προηγούμενα μαθήματα, με αποτέλεσμα κώδικας που χρησιμοποιεί την μεταβλητή car μπορεί να τροποποιήσει τα γνωρίσματα του αντικειμένου στο οποίο δείχνει η car παρόλο που δηλώνουμε ότι είναι final (γιατί το final αναφέρεται μόνο στον δείκτη της μνήμης κι όχι στο αντικείμενο αυτό καθαυτό που δείχνει ο δείκτης αυτός).

Εγγραφές (records)

Από την έκδοση 14 και μετά εισήχθηκαν στη γλώσσα οι εγγραφές (records). Όταν αναπαριστούμε ως κλάσεις αντικείμενα του πραγματικού κόσμου, συνήθως γράφουμε μια κλάση που περιέχει απλά τα γνωρίσματα (attributes) και μεθόδους προσπέλασης αυτών των αντικειμένων (δηλ. getXXX() και setXXX()). Π.χ.

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 37 * hash + this.x;
        hash = 37 * hash + this.y;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Point other = (Point) obj;
        if (this.x != other.x) {
            return false;
        }
        if (this.y != other.y) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Point{" + "x=" + x + ", y=" + y + '}';
    }  
  
}

Πολύς κώδικας για να περιγράψετε ένα σημείο στο δισδιάστατο χώρο, έτσι δεν είναι; Από την έκδοση 14 και μετά εισήχθηκε στη γλώσσα η εγγραφή. Ο παρακάτω κώδικας είναι ισοδύναμος με τον παραπάνω!

public record Point(int x, int y) { 
}

και μπορούμε δημιουργήσουμε αντικείμενα της Point κατά τα γνωστά:

Point p = new Point (0, 0);
int x = p.x();

Παρατηρήστε ότι οι μέθοδοι ανάγνωσης των τιμών των γνωρισμάτων δεν ονομάζονται πλέον getXXX() αλλά XXX(). Επίσης, ένα record είναι αμετάβλητο (immutable), δεν επιτρέπεται να κληρονομήσει άλλες κλάσεις (extends) αλλά επιτρέπεται να υλοποιήσει (implements) διεπαφές. Οι εγγραφές είναι ως επί των πλείστων πλειάδες (tuples) αλλά με περισσότερες δυνατότητες.

Φυσικά, μπορείτε να ορίσετε και τις δικές σας μεθόδους σε μια εγγραφή, να ορίσετε compact constructors, όπως:

public record Point(int x, int y) { 
	public Point {
		if (x < 0 || y < 0) 
			throw new IllegalArgumentException(
			      "x and y must be positive");
	}
}

ή ακόμα και εναλλακτικές μεθόδους κατασκευής (alternative constructors), π.χ.

public record Point(int x, int y) { 
	public static Point of(int x, int y) {
		this.x = x;
		this.y = y;
	}
}

Αναδιοργάνωση Κώδικα (Refactoring)

Πολλές φορές συμβαίνει να θέλετε να προσθέσετε ένα νέο χαρακτηριστικό σε ένα πρόγραμμα, αλλά η δομή του προγράμματος είναι τέτοια που δεν είναι εύκολο ή εφικτό να προστεθεί αυτό το νέο χαρακτηριστικό. Σ’ αυτήν την περίπτωση, θα πρέπει πρώτα να αναδιοργανώσετε τον κώδικα ώστε να είναι ευκολότερο να εισάγετε νέα χαρακτηριστικά.

Ορισμός: Με τον όρο Αναδιοργάνωση Κώδικα (Refactoring) εννούμε μια αλλαγή στην δομή ενός προγράμματος ώστε να κάνουμε τον κώδικα πιο κατανοητό και ευκολότερο στην αλλαγή χωρίς όμως να αλλάξουμε τη συμπεριφορά του. Συνήθως, η αναδιοργάνωση κώδικα βελτιώνει την ποιότητα του κώδικα, τον καθιστά πιο κατανοητό ή/και ευκολότερο ν’ αλλάξει/επεκταθεί.

Παραδείγματα: Εξαγωγή κώδικα σε νέα μέθοδο (Extract Method), Μετακίνηση Μεθόδου σε Άλλη Κλάση (Move Method),

Όλα τα μοντέρνα ΟΠΕ (Integrated Development Environments - IDEs) προσφέρουν δυνατότητες refactoring. Π.χ. το NetBeans διαθέτει το μενού Refactor.

Εικόνα 2.8.1 Μενού Refactor του NetBeans

Η Rename είναι η πιο συνήθης εντολή αναδιοργάνωσης καθώς σας επιτρέπει ν’ αλλάξετε το όνομα μια μεταβλητής, μεθόδου, κλάσης κλπ. με ασφάλεια χωρίς να “σπάτε” τον κώδικα.

Δείτε εδώ για το μενού Inspect and Transform.

Πηγές

  1. Evans B. (2020), Records Come to Java, Java Magazine.
  2. Fowler M. (2019), Refactoring, Improving the Design of Existing Code, 2nd Ed., Addison-Wesley.
  3. Visser J. (2016), Building Maintainable Software, Java Edition, O’Reilly.

<- Δ