Πολλοί άνθρωποι μαθαίνουν Ruby ξεκινώντας με το πλαίσιο Rails και, δυστυχώς, αυτός είναι ο χειρότερος δυνατός τρόπος εκμάθησης αυτής της γλώσσας. Μην με παρεξηγήσετε: Το Rails είναι σπουδαίο, σας βοηθά να δημιουργείτε διαδικτυακές εφαρμογές γρήγορα και αποτελεσματικά χωρίς να χρειάζεται να μπείτε σε πολλές τεχνικές λεπτομέρειες.
Χάρηκα για τη γνωριμία!
Πολλοί άνθρωποι μαθαίνουν Ruby ξεκινώντας με το πλαίσιο Rails και, δυστυχώς, αυτός είναι ο χειρότερος δυνατός τρόπος εκμάθησης αυτής της γλώσσας. Μην με παρεξηγήσετε: Το Rails είναι σπουδαίο, σας βοηθά να δημιουργείτε διαδικτυακές εφαρμογές γρήγορα και αποτελεσματικά χωρίς να χρειάζεται να μπείτε σε πολλές τεχνικές λεπτομέρειες. Παρέχουν πολλή "μαγεία του Rails" που κάνει τα πράγματα απλά να λειτουργούν. Και για έναν αρχάριο προγραμματιστή αυτό είναι πραγματικά σπουδαίο, γιατί η πιο ευχάριστη στιγμή της διαδικασίας είναι όταν μπορείτε να πείτε "είναι ζωντανό!", και να δείτε ότι όλα τα μέρη ταιριάζουν μεταξύ τους και οι άνθρωποι χρησιμοποιούν την εφαρμογή σας. Μας αρέσει να είμαστε "δημιουργοί" 🙂 Υπάρχει όμως ένα πράγμα που ξεχωρίζει τους καλούς προγραμματιστές από τον μέσο όρο: οι καλοί καταλαβαίνουν πώς λειτουργούν τα εργαλεία που χρησιμοποιούν. Και με τον όρο "κατανοούν τα εργαλεία τους" δεν εννοώ να γνωρίζουν όλες τις μεθόδους και τα modules που παρέχει ένα framework, αλλά να καταλαβαίνουν πώς λειτουργεί, να κατανοούν πώς συμβαίνει η "μαγεία του Rails". Μόνο τότε μπορείτε να νιώσετε άνετα με τη χρήση αντικειμένων και τον προγραμματισμό με το Rails. Το θεμέλιο του αντικειμενοστραφούς προγραμματισμού, και το μυστικό όπλο που κάνει την περίπλοκη εφαρμογή Rails πιο εύκολη, είναι το ήδη αναφερόμενο στον τίτλο PORO, δηλαδή το Plain Old Ruby Object
Τι πραγματικά κρύβεται πίσω από αυτό το όνομα; Ποιο είναι αυτό το μεγάλο μυστικό όπλο; Είναι μια απλή κλάση Ruby που δεν κληρονομεί από τίποτα. Ναι, ακριβώς αυτό και άλλα τόσα.
κλάση AwesomePoro
end
Πώς μπορώ να σας βοηθήσω;
Αναπτύσσετε συνεχώς την εφαρμογή σας και προσθέτετε νέες λειτουργίες, καθώς ο αριθμός των χρηστών και οι προσδοκίες τους αυξάνονται. Φτάνετε στο σημείο όπου συναντάτε όλο και περισσότερα σκοτεινά σημεία εξαιρετικά διεστραμμένης λογικής, τα σημεία τα οποία αποφεύγουν σαν την πανούκλα ακόμη και οι πιο γενναίοι προγραμματιστές. Όσο περισσότερα είναι αυτά τα μέρη, τόσο πιο δύσκολη είναι η διαχείριση και η ανάπτυξη της εφαρμογής. Ένα τυπικό παράδειγμα είναι η ενέργεια της εγγραφής ενός νέου χρήστη, η οποία ενεργοποιεί μια ολόκληρη ομάδα άλλων ενεργειών που σχετίζονται με αυτό το γεγονός:
- έλεγχος της διεύθυνσης IP σε μια βάση δεδομένων spam,
- στέλνοντας ένα email στον νέο χρήστη,
- προσθέτοντας ένα μπόνους σε έναν λογαριασμό ενός χρήστη που συνιστά,
- δημιουργία λογαριασμών σε συναφείς υπηρεσίες,
- και πολλά άλλα...
Ένα δείγμα κωδικός υπεύθυνη για την εγγραφή του χρήστη μπορεί να μοιάζει ως εξής:
class RegistrationController < ApplicationController
def create
user = User.new(registration_params)
if user.valid? && ip_valid?(registration_ip)
user.save!
user.add_bonuses
user.synchronize_related_accounts
user.send_email
end
end
end
Εντάξει, το έχετε κωδικοποιήσει, όλα λειτουργούν, αλλά... είναι όλος αυτός ο κώδικας πραγματικά εντάξει; Ίσως θα μπορούσαμε να τον γράψουμε καλύτερα; Πρώτα απ' όλα, παραβιάζει τη βασική αρχή του προγραμματισμού - Ενιαία Ευθύνη, οπότε σίγουρα θα μπορούσαμε να το γράψουμε καλύτερα. Αλλά πώς; Σε αυτό το σημείο έρχεται να σας βοηθήσει το ήδη αναφερθέν PORO. Αρκεί να διαχωρίσετε μια κλάση RegistrationService, η οποία θα είναι υπεύθυνη για ένα μόνο πράγμα: την ειδοποίηση όλων των σχετικών υπηρεσιών. Με τον όρο υπηρεσίες θα θεωρήσουμε τις επιμέρους ενέργειες που έχουμε ήδη ξεχωρίσει παραπάνω. Στον ίδιο ελεγκτή το μόνο που χρειάζεται να κάνετε είναι να δημιουργήσετε ένα αντικείμενο RegistrationService και να καλέσετε σε αυτό τη μέθοδο "fire!". Ο κώδικας έχει γίνει πολύ πιο ξεκάθαρος, ο ελεγκτής μας καταλαμβάνει λιγότερο χώρο και κάθε μία από τις κλάσεις που δημιουργήθηκαν πρόσφατα είναι πλέον υπεύθυνη για μία μόνο ενέργεια, οπότε μπορούμε εύκολα να τις αντικαταστήσουμε αν χρειαστεί.
κλάση RegistrationService
def fire!(params)
user = User.new(params)
if user.valid? && ip_validator.valid?(registration_ip)
user.save!
after_registered_events(user)
end
user
end
private
def after_registered_events(user)
BonusesCreator.new.fire!(user)
AccountsSynchronizator.fire!(user)
EmailSender.fire!(user)
end
def ip_validator
@ip_validator ||= IpValidator.new
end
end
class RegistrationController < ApplicationController
def create
user = RegistrationService.new.fire!(registration_params)
end
end
Ωστόσο, το Plain Old Ruby Object μπορεί να αποδειχθεί χρήσιμο όχι μόνο για ελεγκτές. Φανταστείτε ότι η εφαρμογή που δημιουργείτε χρησιμοποιεί ένα σύστημα μηνιαίας χρέωσης. Η ακριβής ημέρα δημιουργίας μιας τέτοιας χρέωσης δεν είναι σημαντική για εμάς, πρέπει μόνο να γνωρίζουμε ότι αφορά ένα συγκεκριμένο μήνα και έτος. Φυσικά μπορείτε να ορίσετε την ημέρα για την πρώτη ημέρα κάθε μήνα και να αποθηκεύσετε αυτή την πληροφορία στο αντικείμενο της κλάσης "Date", αλλά ούτε πρόκειται για πραγματική πληροφορία, ούτε την χρειάζεστε στην εφαρμογή σας. Με τη χρήση του PORO μπορείτε να δημιουργήσετε μια κλάση "MonthOfYear", τα αντικείμενα της οποίας θα αποθηκεύουν την ακριβή πληροφορία που χρειάζεστε. Επιπλέον, όταν εφαρμόζετε σε αυτήν την ενότητα "Comparable", θα είναι δυνατή η επανάληψη και η σύγκριση των αντικειμένων της, όπως ακριβώς όταν χρησιμοποιείτε την κλάση Date.
class MonthOfYear
include Comparable
attr_reader :year, :month
def initialize(month, year)
raise ArgumentError unless month.between?(1, 12)
@year, @month = έτος, μήνας
end
def (other)
[year, month] [other.year, other.month]
end
end
Παρουσιάστε μου το Rails.
Στον κόσμο του Rails, έχουμε συνηθίσει το γεγονός ότι κάθε κλάση είναι ένα μοντέλο, μια προβολή ή ένας ελεγκτής. Έχουν επίσης την ακριβή τους θέση στη δομή του καταλόγου, οπότε πού μπορείτε να βάλετε τον μικρό μας στρατό PORO; Σκεφτείτε μερικές επιλογές. Η πρώτη σκέψη που μας έρχεται στο μυαλό είναι: αν οι κλάσεις που δημιουργήσαμε δεν είναι ούτε μοντέλα, ούτε προβολές, ούτε ελεγκτές, θα πρέπει να τις τοποθετήσουμε όλες στον κατάλογο "/lib". Θεωρητικά, είναι μια καλή ιδέα, ωστόσο αν όλα τα αρχεία PORO σας θα καταλήξουν σε έναν κατάλογο και η εφαρμογή θα είναι μεγάλη, αυτός ο κατάλογος θα γίνει γρήγορα ένα σκοτεινό μέρος που φοβάστε να ανοίξετε. Επομένως, αναμφίβολα, δεν είναι καλή ιδέα.
AwesomeProject
├──Εφαρμογή
│ ├─ελεγκτές
│ ├─μοντέλα
│ └─views
│
└─lib
└─ υπηρεσίες
#all poro εδώ
Μπορείτε επίσης να ονομάσετε ορισμένες από τις κλάσεις σας που δεν είναι ActiveRecord Models και να τις τοποθετήσετε στον κατάλογο "app/models", και να ονομάσετε αυτές που είναι υπεύθυνες για το χειρισμό άλλων κλάσεων services και να τις τοποθετήσετε στον κατάλογο "app/services". Αυτή είναι μια αρκετά καλή λύση, αλλά έχει ένα μειονέκτημα: όταν δημιουργείτε ένα νέο PORO, κάθε φορά θα πρέπει να αποφασίζετε κάθε φορά αν είναι περισσότερο μοντέλο ή υπηρεσία. Με αυτόν τον τρόπο, μπορεί να φτάσετε σε μια κατάσταση όπου έχετε δύο σκοτεινά σημεία στην εφαρμογή σας, μόνο μικρότερα. Υπάρχει ακόμη μια τρίτη προσέγγιση, δηλαδή: η χρήση κλάσεων και ενοτήτων με namespaced. Το μόνο που χρειάζεται να κάνετε είναι να δημιουργήσετε έναν κατάλογο που έχει το ίδιο όνομα με έναν ελεγκτή ή ένα μοντέλο, και να τοποθετήσετε όλα τα αρχεία PORO που χρησιμοποιούνται από τον συγκεκριμένο ελεγκτή ή μοντέλο μέσα σε αυτά.
AwesomeProject
├──Εφαρμογή
│ ├─ελεγκτές
│ │ ├─registration_controller
│ │ │ │ └─registration_service.rb
│ │ └─registration_controller.rb
│ ├─models
│ │ ├─settlement
│ │ │ │ └─month_of_year.rb
│ │ │ └─settlement.rb
│ └─views
│
└─lib
Χάρη σε αυτή τη ρύθμιση, όταν τη χρησιμοποιείτε, δεν χρειάζεται να προηγείται του ονόματος μιας κλάσης ένα namespace. Έχετε αποκτήσει συντομότερο κώδικα και πιο λογικά οργανωμένη δομή καταλόγου.
Ελέγξτε με!
Είναι μια ευχάριστη έκπληξη το γεγονός ότι όταν χρησιμοποιείτε το PORO, οι δοκιμές μονάδας της εφαρμογής σας είναι ταχύτερες και ευκολότερες στη συγγραφή και αργότερα είναι πιο πιθανό να γίνουν κατανοητές από άλλους. Καθώς κάθε κλάση είναι πλέον υπεύθυνη για ένα μόνο πράγμα, μπορείτε να αναγνωρίσετε τις οριακές συνθήκες νωρίτερα και να προσθέσετε εύκολα τα κατάλληλα σενάρια δοκιμών σε αυτές.
describe MonthOfYear do
subject { MonthOfYear.new(11, 2015) }
it { should be_kind_of Comparable }
describe "creating new instance" do
it "αρχικοποίηση με το σωστό έτος και μήνα" do
expect { described_class.new(10, 2015) }.to_not raise_error
end
it "εγείρει σφάλμα όταν ο δεδομένος μήνας είναι λανθασμένος" do
expect { described_class.new(0, 2015) }.to raise_error(ArgumentError)
expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
end
end
end
Ελπίζω να ξανασυναντηθούμε!
Τα παραδείγματα που παρουσιάσαμε δείχνουν σαφώς ότι η χρήση του PORO βελτιώνει την αναγνωσιμότητα των εφαρμογών και τις καθιστά πιο αρθρωτές και, κατά συνέπεια, πιο εύκολες στη διαχείριση και την επέκτασή τους. Η υιοθέτηση της αρχής της Ενιαίας Ευθύνης διευκολύνει την ανταλλαγή συγκεκριμένων κλάσεων, εάν είναι απαραίτητο, και μάλιστα χωρίς να παρεμβαίνει σε άλλα στοιχεία. Επίσης, καθιστά τον έλεγχό τους μια απλούστερη και ταχύτερη διαδικασία. Επιπλέον, με αυτόν τον τρόπο η διατήρηση των μοντέλων και των ελεγκτών του Rails σε σύντομο χρονικό διάστημα είναι πολύ πιο εύκολη, και όλοι γνωρίζουμε ότι τείνουν να γίνονται περιττά μεγάλα κατά τη διαδικασία της ανάπτυξης.