Controller /
Dispatcher: Inversion of Control, και Dependency Injection.
Κάλιο αργά παρά ποτέ … :P
Μέχρι τώρα, είδαμε τις επιλογές
μου για το μέρος του Controller σε ένα Model View Controller σχήμα, πάνω σε ASP.NET.
Προς το παρόν, έχουμε έναν
τρόπο με τον οποίο μπορούμε να επεξεργαστούμε όλα τα incoming requests, ο οποίος μας
αφήνει να «επεξεργαστούμε» κεντρικά τόσο το request, όσο και τη σελίδα η οποία τελικά θα
το εξυπηρετήσει.
Σε αυτό ακριβώς το σημείο,
κρύβεται το «μαγικό» feature του MVC. Έχω
όλα τα δεδομένα από το request, και
έχω και το instance της σελίδας η οποία θα την εξυπηρετήσει. Άρα, εδώ είναι το σημείο στο
οποίο μπορώ να «ξεφορτώσω» κώδικα από την ίδια τη σελίδα, και να τον μεταφέρω
σαν κάποιο pluggable module σε αυτό το σημείο, προτού ο controller επιστρέψει το page instance στο σύστημα.
Υπάρχουν 2 patterns τα οποία είναι πολύ σχετικά με αυτό το σημείο. To Dependency Injection, και το Invesion of Control. Μάλλον, για να το θέσω καλύτερα, τα DI & IoC είναι
περισσότερο τεχνικές, οι οποίες υλοποιούνται με τη χρήση διάφορων patterns, αντί για
«καθαρά» ατομικά patterns τα ίδια. Πολύ συχνά ο κόσμος φαίνεται να τα χρησιμοποιεί ως ακρώνυμα
εννοώντας το ίδιο πράγμα, αλλά η ταπεινή μου γνώμη είναι ότι πρόκειται για δύο
εντελώς διαφορετικά πράγματα.
To Dependency
Injection είναι πολύ απλό. Ας πούμε ότι η
σελίδα μας που υλοποιεί το View, χρειάζεται κάποια δεδομένα τα οποία θα παρουσιάσει στο
χρήστη. Άρα, η σελίδα μας έχει ένα dependency σε αυτά τα δεδομένα. Δε μπορεί να
λειτουργήσει χωρίς αυτά.
Συνήθως, στον κώδικα της
σελίδας, θα περίμενε να βρεί κάποιος τον κώδικα ο οποίος διαβάζει τις όποιες
παραμέτρους έρχονται στη σελίδα, και φέρνει τα ανάλογα δεδομένα από τη βάση, ή
από κάποιο business ή data object. Εξυπηρετεί
το dependency του στα δεδομένα χωρίς τη βοήθεια κανενός.
Χρησιμοποιώντας το dependency injection paradigm, το σύστημα
είναι υπεύθυνο να δημιουργήσει αυτά τα δεδομένα, και να τα δώσει αυτό στη
σελίδα μας, ικανοποιώντας έτσι το dependency στα δεδομένα.
Προσωπικά εκτιμώ ότι χρησιμοποιώντας
τη dependency injection νοοτροπία, κάνω ευκολότερη τη μετάβαση από ένα view implementation σε ένα άλλο. Μη
σκεφτείτε μόνο την αλλαγή κάποιου UI, σκεφτείτε την πιθανότητα διαφορετικού τύπου content ανα view. Π.χ.
όταν κάποτε δούλευα σε μια καθαρά web design εταιρία, παρουσιάστηκε η ανάγκη να δίνουμε το content ενός site στη vodaphone σε
ένα πολύ συγκεκριμένο xml format, απαραίτητο
για να μετασχηματιστεί σε content για το vodaphone live. Έχοντας
όλες τις σελίδες στο site μας να περιέχουν τον data access κώδικά τους, μας οδήγησε στο να κάνουμε όλες αυτές τις σελίδες copy-paste και rewrite για να σερβίρουν Vodaphone XML …
Σε ένα διπλανό site, στο οποίο
χρησιμοποιούσα πολύ πειραματικά MVC, dependency injection και XSLT για την παραγωγή του content, η προσθήκη ενός ακόμη content-type μας πήρε 1-2
μέρες.
To Inversion of Control ίσως είναι κι αυτό
απλώς παρεξηγημένο από τον κόσμο. Για παράδειγμα, το προηγουμενο σενάριο, αυτό
του dependency injection θα μπορούσε να χαρακτηριστεί ως inversion of control από
την οπτική του ότι δεν είναι πλέον το view στον έλεγχο της «παραγωγής» των δεδομένων
του. Αυτός ο έλεγχος έχει παραδοθεί στον controller, ή στο εκάστοτε IoC container. Αλλά η δική μου ταπεινή γνώμη είναι
ότι το Inversion of Control δεν πρέπει να μένει στο
μυαλό μας μόνο σε αυτό το επίπεδο.
Η άποψή μου είναι ότι όταν
ξέρεις τα processes που πρέπει να τρέξουν για να εκτελεστεί μια ενέργεια στο σύστημα σου,
σχεδιάζεις τα πάντα ως ένα sequence από ατομικά tasks τα οποία πρέπει
να εκτελεστούν, ορίζεις τα interfaces τα οποία καθένας τύπος task πρέπει να
υλοποιεί, και αφήνεις το instrumentation / orchestration στο σύστημα από κάτω.
Με άλλα λόγια, δεν αφήνεις τα
εκάστοτε μέρη του συστήματος να παίρνουν αποφάσεις, πέραν από το τι είναι
απολύτως απαραίτητο για να εκτελέσουν την ατομική τους εργασία. Κάθε υλοποίηση
απλώς γεμίζει κώδικα σε προκαθορισμένες μεθόδους οι οποίες καλούνται από το
σύστημα, ποτέ η μια από την άλλη.
Δεν αφήνεις τον προγραμματιστή
να πάρει αποφάσεις … άρα … Inversion of Control !!! :P
Και το sample …
Όλα αυτά τα «pattern-ικά» δεν τα
σκεφτόμουν υλοποιώντας το σημερινό «επεισόδιο» κώδικα. Τα αναγνωρίζεις όμως
μετά, βλέποντας το σύστημά σου, από ψηλά.
Όπως έλεγα και στο προηγούμενο post, συνδυάζω τον
controller, με
ακόμη ένα pattern, το filter chain. Πρό λίγων
εβδομάδων, είχα γράψει ένα άρθρο όπου έδειχνα μια υλοποίηση του filter chain το οποίο
μπορείτε να βρείτε εδώ, σαν επιπλέον reference τόσο για το pattern όσο και για
τον sample κώδικα που θα δείτε στο attachment.
Η αλυσίδα που θα δείτε στο sample είναι σχετικά
απλή. Ορίζεται σαν xml ως εξής:
<?xml version="1.0" encoding="utf-8" ?>
<simple-view-chain>
<filters>
<authorization-filter/>
<view-dispatcher-filter/>
<view-init-filter>
<!-- define the class
type that implements this view's datasource -->
<datasource-type class="Controller.DataSources.CustomerEditBean" assembly="Controller" >
<parameters>
<!-- add an
http-param to auto-set the UserID property of the datasource instance -->
<http-param id="CustomerID" auto-set="true"/>
</parameters>
</datasource-type>
</view-init-filter>
</filters>
</simple-view-chain>
Ορίζει τρία είδη
φίλτρων:
- AuthorizationFilter.
Θεωρητικά ελέγχει αν έχουμε current χρήστη, και αν όχι
και χρειάζεται authentication για το συγκεκριμένο view κάνει redirect στη login σελίδα μας. Στο sample δεν κάνει στην πραγματικότητα
τίποτα … :P
- ViewDispatcherFilter.
Αναγνωρίζει σε ποιο view πρέπει να προωθήσει το request, και δημιουργεί ένα instance του.
- ViewInitFilter.
Αναλαμβάνει να «αρχικοποιήσει» properties του View, κάνοντας
στην ουσία dependency injection τις τιμές από τυχόν request parameters, data source objects κτλ.
Παρακάτω έχω το class diagram του SimpleViewChain: 
Όπως βλέπετε, έχω το SimpleViewChain, subclass του FilterChain για το οποίο είχα μιλήσει στο προηγούμενο post μου, τα τρία ειδικά φίλτρα και ένα generic logging φίλτρο, και το data object για το SimpleViewChain, το SimpleViewData.
To SimpleViewData ορίζει ένα structure το οποίο περιέχει όλα τα απαραίτητα
δεδομένα για να ολοκληρωθεί η λειτουργία της αλυσίδας:
using System;
using System.Web;
using
Controller.View;
namespace Controller.ViewChain
{
/// <summary>
/// Wraps all the required data for the successful processing
of the SimpleViewChain
/// chain of filters. It is the object that will get passed
through all the filters, before
/// it is returned to the caller of the chain for final
processing
/// </summary>
public class SimpleViewData
{
private
HttpContext m_Context = null;
private
ISimpleView m_View = null;
private
string m_Url = null;
public
SimpleViewData() { }
public
SimpleViewData(HttpContext context) {
this.Context
= context;
}
public
SimpleViewData(HttpContext context, string url) : this(context)
{
this.Url
= url;
}
/// <summary>
/// Wraps the HttpContext property of this simple data object,
so
/// as to ensure that properties are intialized correctly, and
not used
/// before they are initialized
/// </summary>
public string Url
{
get
{
if
(null == m_Url)
throw
new ApplicationException("Url has not yet been initialized");
return
m_Url;
}
set
{
if
(null == value)
throw
new ArgumentNullException("Url", "Cannot
be initialized to Null");
m_Url = value;
}
}
/// <summary>
/// Wraps the HttpContext property of this simple data object,
so
/// as to ensure that properties are intialized correctly, and
not used
/// before they are initialized
/// </summary>
public HttpContext Context {
get
{
if
(null == m_Context)
throw
new ApplicationException("Context has not yet been
return
m_Context;
}
set
{
if
(null == value)
throw
new ArgumentNullException("Context", "Cannot
be initialized to Null");
m_Context = value;
}
}
/// <summary>
/// Wraps the ISimpleView property of this simple data object,
so
/// as to ensure that properties are intialized correctly, and
not used
/// before they are initialized
/// </summary>
public ISimpleView View
{
get
{
if
(null == m_View)
throw
new ApplicationException("View has not yet been initialized");
return
m_View;
}
set
{
if
(null == value)
throw
new ArgumentNullException("View", "Cannot
be initialized to Null");
m_View = value;
}
}
}
}
Όπως βλέπετε, όλα τα getters πετάνε exception σε περίπτωση που το αντίστοιχο member variable δεν έχει αρχικοποιηθεί. Αυτό το
κάνω για καλύτερο debugging της αλυσίδας σε περίπτωση που κάποια υλοποίηση ενός φίλτρου έχει
τοποθετηθεί σε λάθος σειρά ας πούμε, και κάποιο member του SimpleViewData που χρειάζεται δεν έχει αρχικοποιηθεί από κάποιο προηγούμενο φίλτρο.
Το άλλο ενδιαφέρον στο SimpleViewData, είναι το View property το οποίο επιστρέφει ένα
ISimpleView. Ένα interface. Αυτό μας
δίνει τη δυνατότητα να αλλάζουμε implementations. Για παράδειγμα, μπορεί το view μου να κάνει render HTML. Αλλά μπορεί
να κάνει render και οποιοδήποτε άλλο content-type. Εφ’ όσον η υλοποίηση κάνει implement το ISimpleView, έχω
τη δυνατότητα να αλλάζω implementations χωρίς να «σπάω» το compatibility με το υπάρχον σύστημα.
Ας ρίξουμε τώρα μια ματιά και
στο ίδιο το ISimpleView: 
Όνομα και πράμα έτσι; Ένας πολύ
απλός βασικός ορισμός για ένα view. Έχει ένα property που λέγεται Data και είναι
οποιουδήποτε τύπου. Υπο κανονικές συνθήκες, δε θα αφήναμε τον τύπο σε object, θα ορίζαμε view interfaces ανάλογα με τον τύπο των
δεδομένων τους, όπως π.χ. DataSets ή κάποια business objects. Π.χ. AsyncOperationView, του
οποίου το Data property θα ήταν κάποιο αντικείμενο που λειτουργεί ως façade για μια
ασύγχρονη διαδικασία.
Για το πολύ απλό παράδειγμά
μας όμως, αυτό το πολύ απλό interface μας φτάνει. ( Σκέφτομαι τώρα ότι
το IView είναι φοβερός πειρασμός για μια generic κλάση … )
Οκ, ως εδώ είδαμε ότι η αλυσίδα
μας έχει ένα κάποιο data object, και το
τελικό μας view, η
σελίδα, υλοποιεί κάποιο συγκεκριμένο interface. Πως «δένουμε» τα δύο; Το όλο ..
μυστικό, βρίσκεται στο τρίτο φίλτρο της αλυσίδας, τον ViewInitFilter.
Ο ορισμός του φίλτρου στην xml είναι ως εξής:
<view-init-filter>
<!-- define the class type
that implements this view's datasource -->
<datasource-type class="Controller.DataSources.CustomerEditBean" assembly="Controller" >
<parameters>
<!-- an http-param to
auto-set the CustomerID property of the d-source instance -->
<http-param id="CustomerID" auto-set="true"/>
</parameters>
</datasource-type>
</view-init-filter>
Στο datasource-type element, ορίζεται το class name και το assembly της κλάσης η οποία θα αποτελέσει το datasource για το view μας. Ένα instance αυτού του τύπου θα μας δώσει το .Data του view.
Πέρα από αυτό, μπορούμε στην xml να ορίσουμε
κάποιες παραμέτρους, οι τιμές των οποίων θα δωθούν αυτόματα στο datasource instance. Μαγεία; Για
παράδειγμα, από το definition παραπάνω, εάν στο Request υπάρχει παράμετρος CustomerID, και στο datasource ένα property με το ίδιο όνομα, η τιμή της παραμέτρου θα τεθεί αυτόματα ως τιμή στο property, κάνοντας
μάλιστα και το parse στον σωστό τύπο ! ( Είχα αναφερθεί σε αυτό σε ένα πολύ παλιό αρθράκι,
αν θέλετε να δείτε περισσότερα, είναι εδώ )
Έτσι, το ViewInitFilter θα
δημιουργήσει ένα instance του Controller.DataSources.CurstomerEditBean, και
αν υπάρχει CustomerID παράμετρος στο Request, θα τη θέσει στο CustomerID property του instance αυτού. Στη συνέχεια, θα
δώσει αυτό το instance στο view μας μέσω του Data property … και
η ζωή συνεχίζεται.
Ας ρίξουμε και μια ματιά σε
αυτό το datasource class, και μετά
στον κώδικα του view μας.
using System;
using
System.Data;
using
System.Data.OleDb;
namespace
Controller.DataSources
{
/// <summary>
/// Retieves / Updates data regarding Customers
/// </summary>
public class CustomerEditBean
{
private
int m_CustomerID = -1;
public
CustomerEditBean() { }
public int CustomerID {
get
{
return
m_CustomerID;
}
set
{
//
always check !
if
(-1 >= value)
throw
new ArgumentException("Cannot initialize CustomerID to zero or less");
m_CustomerID = value;
}
}
private
DataSet m_CustomerData = null;
public DataView Data {
get
{
//
initialize the data if we have none ...
if
(null == m_CustomerData)
FetchData();
//
Check what data I need to fetch ...
if
(this.CustomerID > 0)
{
//
Fetch single customer data
m_CustomerData.Tables[0].DefaultView.RowFilter = string.Format("CustomerID =
{0}", this.CustomerID);
}
return
m_CustomerData.Tables[0].DefaultView;
}
}
public bool Update() {
return
true;
}
private
void FetchData() {
m_CustomerData = new
DataSet();
OleDbDataAdapter
adapter = new OleDbDataAdapter("select * from Customer", "Provider=Microsoft.Jet.OLEDB.4.0;Data
Source=F:\\projects\\MVC\\mvc_sample.mdb");
adapter.Fill(m_CustomerData);
}
}
}
Τίποτα φοβερό. Παίζω με μια
τοπική Access, κι
ένα μόνο πίνακα, τον customers. Η
υλοποίηση είναι λίγο … άτσαλη, σε καμία περίπτωση δε θα γράφατε τέτοιο κώδικα
για production
υλοποίηση, αλλά για ένα γρήγορο παράδειγμα με 2 rows στο table είναι μια χαρά :P
Θεωρητικά, όπως και τα views, έτσι και τα data sources θα πρέπει να υλοποιούν
κάποια συγκεκριμένα interfaces ανάλογα τη χρήση και τα data τους. Προς χάρην
… χρόνου, όμως, στο sample κώδικα το έχω αφήσει «ελεύθερο» αυτό το
σημείο, και έχω «δέσει» το datasource με το view στον ίδιο τον
κώδικα του view:
using System;
using
System.Data;
using
System.Configuration;
using System.Web;
using
System.Web.Security;
using
System.Web.UI;
using
System.Web.UI.WebControls;
using
System.Web.UI.WebControls.WebParts;
using
System.Web.UI.HtmlControls;
using
Controller.View;
using
Controller.DataSources;
public partial class _Default : System.Web.UI.Page,
ISimpleView
{
protected void Page_Load(object
sender, EventArgs e)
{
if(null!=this.Data)
Response.Write(string.Format("Injected DataObject: {0}", this.Data.GetType().ToString()));
if (this.Data is CustomerEditBean) {
CustomerEditBean
bean = (CustomerEditBean)this.Data;
this.lstCustomers.DataSource
= bean.Data;
this.lstCustomers.DataBind();
}
}
#region ISimpleView Members
private object m_Data = null;
public object Data
{
get
{
return
m_Data;
}
set
{
m_Data = value;
}
}
#endregion
}
Βλέπετε, ότι το view απαιτεί ένα CustomerEditBean instance για να παίξει. Σε αυτό το σημείο, εάν υλοποιούσαμε ένα interface για τα datasources, θα
μπορούσαμε να αλλάζουμε implementations κατά
βούληση, από το xml αρχείο. Στο επόμενο sample όμως αυτά … :P
Για όσους ως τώρα δεν έχουν
καταλάβει τελικά τι συμβαίνει σε αυτό το sample ( μεταξύ αυτών κι εγώ .. είναι
Κυριακή πρωί και μάλλον δεν έχω συνέλθει από χτές … ), συνοψίζω:
- Έρχεται ένα request για τη σελίδα http://locallhost/mvc/Default.aspx?CustomerID=1
- O Controller μας, δέχεται το request, και φτιάχνει μια SimpleViewChain
για αν το επεξεργαστεί.
- Το πρώτο φίλτρο στην αλυσίδα ελέγχει αν ο
χρήστης είναι logged in. Στη
συγκεκριμένη υλοποίηση … είναι πάντα.
- Το δεύτερο φίλτρο διαβάζει το request path και κάνει compile ένα instance της σελίδας.
- Τα τρίτο φίλτρο δημιοργεί ένα instance του datasource,
του κάνει inject
τις τιμές των όποιων παραμέτρων του, και το κάνιε με τη σειρά του inject στο view instance.
- Και τέλος, η αλυσίδα επιστρέφει το data structure της στον Controller, οποίος δίνει το initialized πλέον view πίσω στο ASP.NET για να επιστρέψει content στον client.
To απλό sample που έχω attached δείχνει μια σελίδα, η οποία περιέχει το grid με τους customers. Αν περάσετε
ως GET παράμετρο ένα CustomerID, η σελίδα θα επιστρέψει τα δεδομένα μόνο του
συγκεκριμένου αυτού customer.
Ως επίλογο, πρέπει να πώ ότι το
sample αυτό είναι μόνο μια «διατομή» ενός και μόνο σεναρίου. Δε γίνεται όλα τα
.aspx ενός site να έχουν το ίδιο datasource – αυτό συμβαίνει στο σημερινό sample. Στη συνέχεια
αυτών των άρθρων, θα ασχοληθώ λίγο με το view management, κοιτώντας
πως μπορούμε να δημιουργήσουμε έναν απλό μηχανισμό για να ορίζουμε τα views της εφαρμογής
μας, και τα επιμέρους στοιχεία τους όπως datasources, παραμέτρους κτλ. Μετά κι από
αυτό, θα ήταν χρήσιμο να δούμε που μπορούμε να κάνουμε πιο optimized όλο αυτό το μηχανισμό,
με τεχνικές caching και object pooling.
Αλλά όλα αυτά … μετά από μια
‘βδομάδα ! Άυριο φεύγω για μια ‘βδομάδα διακοπές στο όμορφο Γύθειο Λακωνίας !!!
Καλή μας εβδομάδα λοιπόν.
Υ.Γ. Κάποια κομμάτια κώδικα στο
sample, χρησιμοποιούν
κώδικα που έχω γράψει για την εταιρία στην οποία δουλεύω. Έχω αφήσει τα namespaces ίδια, ελπίζοντας ότι εφ’ όσον δεν ενοχλεί την εταιρία να δίνει μέρη του
κώδικά της ελεύθερα, δεν πειράζει και αυτόν που το διαβάζει, ως τυχόν έμμεση
διαφήμιση. Σε περίπτωση που χρησιμοποιήσετε αυτόν τον κώδικα, παρακαλώ βάλτε
κάπου ένα σχόλιο ως την ελάχιστη αναγνώριση. Σας ευχαριστώ.
Angel
O:]