Πρόσφατα στη δουλειά χρειάστηκε να περάσω ένα DataSet από μια σειρά από «φίλτρα», ή «transformations».
Μετά από δύο – τρείς μέρες, χρειάστηκε να περάσω ένα άλλο DataSet από μια σειρά «φίλτρα» ή «validators».
Επίσης, πολύ συχνά μου χρειάζεται να περάσω κάθε incoming request από μια αντίστοιχη σειρά από «φίλτρα».
Κοντολογίς, πάρα πολύ συχνά, η ίδια ιστορία. Φτιάξε τη λίστα από τα αντικείμενα-φίλτρα, γράψε το απαραίτητο for-each, και γύρνα το αποτέλεσμα στον caller …
Τα παραπάνω περιγράφονται πολύ όμορφα στο Filter Chain pattern. Το Filter Chain, το οποίο είναι γνωστό με διάφορα ονόματα (Chain of Responsibility, Intercepting Filter κ.α.), προσωπικά το συνάντησα στα J2EE Core Patterns, όπου υπάρχει ήδη μια υλοποίηση στο spec του εκάστοτε J2EE Container.
Έτσι, αποφάσισα μια όμορφη μέρα να γράψω κι εγώ μια σχετικά απλή υλοποίηση. Μου προσφέρει τη δυνατότητα να ορίσω ( κλασσικά σε XML ) τα φίλτρα μιας λίστας, και από αυτή την περιγραφή να παίρνω instances τα οποία χρησιμοποιώ ανάλογα την περίπτωση …
Ας δούμε λοιπόν.
Το βασικό interface είναι το IFilter. Περιέχει τα απολύτως απαραίτητα. Ένα όνομα, και μια μέθοδος η οποία δέχεται ένα object σαν παράμετρο, και επιστρέφει ένα object σαν αποτέλεσμα. Τίποτα σπουδαίο:
namespace FilterChain
{
/// <summary>
/// Summary description for IFilter.
/// </summary>
public interface IFilter
{
/// <summary>
/// An identifying name for the chain
/// </summary>
string Name {get;set;}
/// <summary>
/// defines the method that will get called by the chain, so
/// as to process a given input through this filter instance
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
object Proccess(object input);
}
}
Το αμέσως επόμενο interface, είναι το IFilterChain. Εξίσου απλό, περιέχει ότι περιέχει και το IFilter, και προσθέτει ένα property που ορίζει τον τρόπο με τον οποίο το chain θα εκτελέσει τα φίλτρα του, και το collection με τα instances των φίλτρων:
namespace FilterChain
{
/// <summary>
/// Defines the running mode of a FilterChain:
/// 1. Sequential will feed the output of Filter 1 to Filter 2 and so on
/// 2. Parallel will feed all Filters with the initial input instance
/// </summary>
public enum FilterChainMode {
Sequential = 0,
Parallel = 1
}
/// <summary>
/// IFilterChain defines the basic interface for a sequence of
/// proccesses, executed on an 'input' object.
/// </summary>
public interface IFilterChain : IFilter
{
/// <summary>
/// Retrieves the runing mode of the FilterChain
/// </summary>
FilterChainMode ChainMode {get;set;}
/// <summary>
/// Collections of filter instances.
/// </summary>
Filter[] Filters {get; set;}
}
}
Υπάρχουν μόνο δύο σημειώσεις εδώ ...
Πρώτα, το ChainMode (Sequential, Parallel) καθορίζει το πώς θα εκτελεστούν τα φίλτρα. Sequential, είναι σε σειρά. Τα επομενο φίλτρο θα εκτελεστεί, αλλα προσοχή(!) το input που θα πάρει, θα είναι το αποτέλεσμα του προηγούμενου φίλτρου στην αλυσίδα !
Με αυτό τον τρόπο π.χ. μπορώ αν περάσω το HttpContext ενός request στην αλυσίδα, να προσθέτω items στο HttpContext.Items ή στο Session κτλ κτλ και κάθε επόμενο processing να έχει access στις αλλαγές/προσθήκες του προηγούμενου. Δεν ακούγεται σπουδαίο αλλά, πιστέψτε με, είναι ένα pattern που το βρίσκεις συνεχώς μπροστά σου.
Το Parallel mode, δίνει ως input σε όλα φίλτρα το αρχικό input. Με αυτόν τον τρόπο, μπορώ να δημιουργήσω filter chains για validation για παράδειγμα. Αν και αυτό το σενάριο δεν είναι το σύνηθες, stadar θα σας φανεί χρήσιμο στο μέλλον … το δικό μου μυαλό σκέφτηκε ας πούμε notifications over different mediums ( email, sms, MSN Alerts κτλ. κτλ. )
Επίσης, ένα "accidental" feature είναι οτι αφού το IFilterChain κάνει inherit το IFilter, μπορούμε να έχουμε nested filter chains :D ... πολύ χάρηκα όταν το συνειδητοποίησα αυτό, και είπα να το μοιραστώ μαζί σας :]
Μέσα στο ίδιο class library, υπάρχουν υλοποιήσεις για το FilterChain & Filter. Ο βασικός λόγος ύπαρξής τους είναι για να κάνω enforce κάποιους κανόνες ( ότι το όνομα π.χ. δε μπορεί να είναι null ή string.Empty ), και για την υλοποίηση της βασικής λογικής του FilterChain ανάλογα με το FilterChainMode που του δίνουμε. Από ‘κεί και πέρα όμως, υλοποιούν και ένα πολύ βασικό μέρος της όλης υπόθεσης. Το parsing ενός config file, και τη δημιουργία των instances του FilterChain και των Filters του.
Όπως πάντα, έχω χρησιμοποιήσει κατά κόρον XmlSerialization, και οι 2 κλάσεις είναι XmlSerialization-ready. Αυτό αποδείχθηκε ευχή και κατάρα, για τους λόγους που θα αναλύσω στο τέλος αυτού του post. Ας δούμε ένα πολύ απλοϊκό παράδειγμα πρώτα …
<?xml version="1.0" encoding="utf-8" ?>
<test-chain id="test-chain" chain-mode="Sequential">
<filters>
<test-filter id="test filter 1"/>
<filter id="test filter 2"/>
</filters>
</test-chain>
Εδώ, καθορίζω ένα απλό chain, με 2 φίλτρα τα οποία θα τρέξουν σε σειρά. Το πρώτο είναι ένα custom filter, subclass του Filter, ενώ το άλλο είναι απλώς η base υλοποίηση που υπάρχει μέσα στο library και δεν κάνει στην ουσία .. τίποτα.
Δυστυχώς, και επειδή χρησιμοποιώ XmlSerialization, πρέπει να κάνω override το Filter[] Filters property του FilterChain, έτσι ώστε να του πώ ότι υπάρχουν διάφοροι δικοί μου τύποι οι οποίοι θα συμπεριληφθούν σαν elements μέσα στο <filters> tag.
Κατά τα άλλα, κάνω και override το Process της base FilterChain, μόνο και μόνο για να γράφω κάτι στην κονσόλα. Δε χρειάζεται στην πραγματικότητα. Ιδού λοιπόν τα 2 sources:
namespace FilterChainTest
{
/// <summary>
/// Summary description for TestChain.
/// </summary>
[XmlRoot("test-chain")]
public class TestChain : FilterChain.FilterChain
{
public TestChain() : base()
{
//
// TODO: Add constructor logic here
//
}
[XmlArrayItem("test-filter", typeof(FilterChainTest.TestFilter)),
XmlArrayItem("filter", typeof(Filter))]
[XmlArray("filters")]
public override Filter[] Filters {
get {
return base.Filters;
}
set {
base.Filters = value;
}
}
public override object Proccess(object input) {
System.Console.WriteLine(string.Format("Chain with name: {0} started.", base.Name));
object retVal = base.Proccess (input);
System.Console.WriteLine(string.Format("Chain with name: {0} ended.", base.Name));
return retVal;
}
}
}
namespace FilterChainTest
{
/// <summary>
/// Summary description for TestFilter.
/// </summary>
[XmlRoot("test-filter")]
public class TestFilter : Filter
{
public TestFilter() : base()
{
//
// TODO: Add constructor logic here
//
}
public override object Proccess(object input) {
System.Console.WriteLine(string.Format("Filter with name: {0} called.", base.Name));
return null;
}
}
}
Το μόνο σημείο που χρειάζεται ίσως λίγο επεξήγηση, είναι εκεί που ορίζω τους τύπους οι οποίοι αντιστοιχούν στα elements μέσα στο <filters> tag:
[XmlArrayItem("test-filter", typeof(FilterChainTest.TestFilter)),
XmlArrayItem("filter", typeof(Filter))]
[XmlArray("filters")]
public override Filter[] Filters { …
Βασικά είναι απλό. Γράφεις ένα XmlArray attribute, το οποίο λέει στο serializer/deserializer ότι θα συναντήσει ένα element στην xml, τα περιεχόμενα του οποίου θα είναι ένα array / collection. Και πάνω απ’αυτό, σε ένα μόνο attribute ορίζουμε XmlArrayItems καθένα από τα οποία αντιστοιχεί ένα element name με ένα τύπο …
Οκ λοιπόν, μετά από όλα αυτά, η χρήση …
using System;
using System.IO;
using FC = FilterChain;
using FilterChainTest;
namespace FilterChainTest
{
/// <summary>
/// Summary description for Class1.
/// </summary>
class MainClass
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
try{
FC.IFilterChain chain = FC.FilterChain.FromFile(Path.Combine(System.Environment.CurrentDirectory, "../../resources/test-chain.xml"), typeof(TestChain));
chain.Proccess("test");
Console.ReadLine();
}catch(Exception e){
throw;
}
}
}
}
Τίποτα σπουδαίο. Σε 3 γραμμές έχεις κάνει τη δουλειά σου πολύ όμορφα – νομίζω :P
Υ.Γ. … κι επανέρχομαι στο θέμα του XmlSerialization. Όπως αποδεικνύεται, αν στη base κλάση σου έχεις ορίσει ένα property με XmlArray, δεν μπορείς απλώς στα subclasses να προσθέσεις XmlArrayItems … ο Deserializer βλέπει μόνο αυτά της μαμάς κλάσης, και αγνοεί τελείως ( μα τελείως !;!;!;) τα δικά σου … Οπότε, αναγκαστικά έκανα μια ολόκληρη .. χακιά να την πώ; Μπακαλιά; … δεν ξέρω τι, αλλά ορίζω και το XmlArray και τα XmlArrayItems στη subclass, και πρίν κάνω deserialize, σκανάρω τον τύπο του FilterChain το οποίο θέλω να κάνω deserialize, διαβάζω τα XmlSerialization attributes του, και τα ταϊζω στον XmlSerializer, κάνοντας έτσι override τα defaults, και παίρνω πίσω ολόκληρο και πανέμορφο το collection με τα φίλτρα του chain μου … Όλα αυτά γίνονται στην FromFile(…) ( static method η οποία δημιουργεί ένα chain από ένα xml αρχείο ):
/// <summary>
/// Creates the required custom XmlArrayItemAttribute attributes in an
/// XmlAttributeOverrides instance, so as to properly deserialize the
/// Filter instances for the given FilterChain Type.
/// </summary>
/// <param name="chainType"></param>
/// <returns></returns>
private static XmlAttributeOverrides CreateAttributes(Type chainType) {
// ok, build the XmlAttributeOverrides that we'll return ..
XmlAttributeOverrides overrides = new XmlAttributeOverrides();
XmlAttributes attrs = new XmlAttributes();
attrs.XmlArray = new XmlArrayAttribute("filters");
// Get the property info, so as to rip the XmlArrayItemAttributes off that ...
PropertyInfo prop = chainType.GetProperty("Filters");
XmlArrayItemAttribute[] propAttrs = (XmlArrayItemAttribute[])prop.GetCustomAttributes(typeof(XmlArrayItemAttribute), false);
// add those XmlArrayItem attributes to the collection
foreach(XmlArrayItemAttribute propAttr in propAttrs)
attrs.XmlArrayItems.Add(propAttr);
// and now .. override the BASE CLASS's attributes on the Filters property !!!
overrides.Add(typeof(FilterChain), "Filters", attrs);
// return the new attribute collection
return overrides;
}
/// <summary>
/// Instantiates a given IFilterChain, from an XML file.
/// </summary>
/// <param name="filePath"></param>
/// <param name="chainType"></param>
/// <returns></returns>
public static IFilterChain FromFile(string filePath, Type chainType){
if(File.Exists(filePath)){
FileStream ioIn = File.OpenRead(filePath);
try {
return (IFilterChain)(new XmlSerializer(chainType, CreateAttributes(chainType))).Deserialize(ioIn);
}finally {
ioIn.Close();
}
}else
throw new ArgumentException(string.Format("File {0} does not exist", filePath), "filePath");
}
Με αυτά και μ’αυτά … πάει 8 και η ώρα, και πρέπει να μας αφήσω :D Happy chainin’ !!!
Angel
O:]