Ανάλυση
Δαπανών
|
|
Ιανουάριος
|
Φεβρουάριος
|
Μάρτιος
|
…
|
Δαπάνη 1
|
10.00
|
|
|
|
Δαπάνη 2
|
23.56
|
|
|
|
Δαπάνη 3
|
12.00
|
|
|
|
Δαπάνη 4
|
45.78
|
|
|
|
…
|
…
|
|
|
|
Δαπάνη ν
|
…
|
|
|
|
Σύνολα
Κατηγορίας
|
Κατηγορία 1
|
33.56
|
|
|
|
Κατηγορία 2
|
12.00
|
|
|
|
Κατηγορία 3
|
45.78
|
|
|
|
|
|
|
|
|
Πριν κάποιο καιρό,
χρειάστηκε να υλοποιήσω στη δουλειά ένα interface όπως
αυτό που βλέπετε παραπάνω.
Η δομή είναι σχετικά απλή.
Υπάρχουν κάποιες δαπάνες, οι οποίες ανήκουν σε κατηγορίες δαπανών, και έλαβαν
χώρα σε κάποιο συγκεκριμένο μήνα. Το πρώτο μέρος του πίνακα είναι λοιπόν πολύ
απλό.
Στο δεύτερο μέρος, έπρεπε να
βγούν σύνολα ανά κατηγορία δαπάνης.
Στο τρίτο μέρος υπάρχει
γενικό σύνολο, το σύνολο για όλες τις κατηγορίες για το δεδομένο μήνα.
Στην real-life
υλοποίηση είχα βέβαια πιο σύνθετα requirements. Ακολουθούσαν κι άλλα μέρη, τα οποία βασίζονταν σε
σύνολα επί συνόλων, μετά επόμενα που βασίζονταν στα προηγούμενα με κάποιες
φόρμουλες κτλ κτλ Ένα μικρό χάος …
Τώρα, αυτά τα requirements δεν
ήρθαν όλα μαζί. Ξεκινήσαμε από την απλή απαρίθμηση στο grid των δαπανών.
Οπότε, η αρχική υλοποίηση ήταν το bind ενός DataTable σε
ένα grid.
Σε όλα αυτά τα ωραία λοιπόν,
προστέθηκαν σιγά-σιγά τα Σύνολα κατηγοριών, και τα γενικά σύνολα … μέχρι που
ήρθε πλέον η ώρα να προστεθούν derived κελιά από τιμές άλλων κελιών, formulas κτλ
κτλ. Ένα χάος «βρωμιάς». Τι να σου κάνει και το καημένο το DataSet.Compute(...) σε
βάθος 3-4 nested υπολογισμών;;; Η ώρα ήταν ήδη αργά, το deadline πολύ
πολύ κοντά, κι εγώ και μόνο στη σκέψη ότι θα έπρεπε να βγάλω αυτούς τους υπολογισμούς
με το χέρι .. φ ρ ί κ α ι ρ ν α άσκημα !!! Έπρεπε να βρώ κάτι άλλο, κάτι πιο
εύκολο, και σίγουρα κάτι που θα μπορούσα να χρησιμοποιήσω για να λύσω το ίδιο
πρόβλημα στο μέλλον.
Σκέφτηκα λοιπόν. Αν μπορούσα
να κάνω bind αυτά τα κελιά σε κάτι το οποίο μου έδινε πίσω μια
τιμή, και είχα κάπου και το όρισμα ενός composite τέτοιου
πράγματος, ίσως την πάλευα με αυτό το composition να φτιάξω
κάτι το οποίο θα μου επέτρεπε – στο τέλος – να γράψω πολύ λιγότερο κώδικα για
αυτούς καθ’ αυτούς του υπολογισμούς, να μην ανατινάξω το μυαλό μου από τη
βαρεμάρα, και κατά συνέπεια να ξυπνήσω μεν σπίτι μου το πρωί, αλλά και να
ξυπνήσω λίγο πιο χαρούμενος και καλοδιάθετος, κυρίως γιατί έγραψα κάτι
ενδιαφέρον που μου έλυσε τα χέρια τώρα και στο μέλλον και δεν αναγκάστηκα να
γράψω χίλιες non-reusable γραμμές
βαρετού και error-prone κώδικα.
Αλλά τι; Πώς;
Έπρεπε να γράψω ένα
πρωτότυπο, κάτι λίγο πιο απλουστευμένο, στο οποίο θα μπορούσα να πειραματιστώ
λίγο για να βρω μια βολική λύση. Το post αυτό είναι το … postmortem αυτού του πρωτότυπου, και κυρίως στόχο έχει να δείξω
που με οδήγησε αυτή η ιστορία στη συνέχεια.
Ξεκινώντας λοιπόν, κατήργησα
την παράμετρο «μήνας». Θα έπαιζα με μια στήλη μόνο αρχικά.
Επίσης, κατήργησα τα πεδία Quantity / Price
στο DataSet μου,
και έκανα την παραδοχή ότι θα έπαιζα με ένα μόνο cell, το Price
αρχικά. Το point άλλωστε δεν
ήταν αυτό, ήταν να βρω τον τρόπο να γλιτώσω το βαρετό κωδικιλίκι μες στη νυχτιά
:D
Οκ, στο πρωτότυπο λοιπόν.
Αρχικά για να φτιάξω τα δεδομένα μου έγραψα 5-10 γραμμές να κάνω initialize ένα
μικρό DataTable με
το schema που ήθελα:
static DataSet _items = null;
static object[][]
_dataPattern = {
//
Pr. Qua. Ct. ID
new object[]{10.00,
2.00, 1, 1},
new object[]{10.00, 2.00,
1, 2},
new object[]{10.00,
2.00, 2, 3},
new object[]{10.00,
2.00, 2, 4},
new object[]{10.00,
2.00, 3, 5}
};
/// <summary>
/// static constructor for
initialization
/// </summary>
static
Program() {
// ok, init the DataSet, the Datatable, add the Columns etc
etc
_items = new DataSet();
// init the columns
DataTable expenseItems = _items.Tables.Add("ExpenseItems");
expenseItems.Columns.Add("Price", typeof(Double));
expenseItems.Columns.Add("Quantity",
typeof(Double));
expenseItems.Columns.Add("Category",
typeof(int));
expenseItems.Columns.Add("ID",
typeof(int));
// and now fill in the data ...
foreach (object[] row
in _dataPattern) {
expenseItems.Rows.Add(row);
}
}
Καλά ως εδώ, αλλά τώρα
αρχίζουν τα δύσκολα. Τώρα χρειαζόμουν ένα intreface, το οποίο θα όριζε τη συμπεριφορά του βασικού calculator για
κάθε ένα κελί στο UI.
Κάτι σαν αυτό:
/// <summary>
/// Base inteface
definition for a Calculator.
/// it just returns a
double value.
/// </summary>
public interface ICalculator {
double Value { get; }
}
Πολύ απλό. Πάρα πολύ. Τώρα έπρεπε να το κάνω
να μιλάει με ένα DataSet, και να μου υπολογίζει
«κάτι», βάσει των τιμών σε κάποια κελιά του πίνακα. Άρα, χρειαζόμουν ένα filter expression για να βρω με
ποια rows του
DataTable να
παίξω, κι ένα compute expression για να του λέω τι πράξεις θα κάνει.
Οπότε, κλασσούλα που
υλοποιεί το interface, και έχει αυτά τα properties:
/// <summary>
/// Base class for all calculators regarding Datarows.
/// </summary>
public abstract class DataRowsCalculator
: ICalculator
{
public DataRowsCalculator(DataTable
data, string computeExpression) {
this.Data = data;
this.ComputeExpression
= computeExpression;
}
public DataRowsCalculator(DataTable
data, string computeExpression, string filterExpression) : this(data,
computeExpression) {
this.RowFilter = filterExpression;
}
#region data properties and methods
private DataTable m_Data = null;
/// <summary>
/// Gets / Sets the
DataTable we're operating on
/// </summary>
protected DataTable
Data {
get {
// always tell the
user what happened !!!
if (null == m_Data)
throw new
Exception("Data
has not been initialized");
return m_Data;
}
set {
// check !!!
if (null == value)
throw new
ArgumentNullException("Data");
m_Data = value;
}
}
private string
m_RowFilter = null;
/// <summary>
/// A filter that will be
applied to my DataTable, to limit the
/// rows to the ones i
really need
/// </summary>
protected string
RowFilter {
get {
return m_RowFilter;
}
set {
m_RowFilter = value;
}
}
private string
m_Expression = null;
/// <summary>
/// A filter that will be
applied to my DataTable, to limit the
/// rows to the ones i
really need
/// </summary>
protected string
ComputeExpression {
get {
if(string.IsNullOrEmpty(m_BLOCKED EXPRESSION
throw new
Exception("ComputeExpression
has not been initialized");
return m_Expression;
}
set {
if(string.IsNullOrEmpty(value))
throw new
ArgumentNullException("ComputeExpression");
m_Expression = value;
}
}
/// <summary>
/// Instructs the DataTable
to compute the expression on the selected rows and return
/// the result as a double
/// </summary>
/// <param name="expression"></param>
/// <returns></returns>
protected static double Compute(DataTable
data, string expression, string filter) {
return (double)data.Compute(expression,
filter);
}
#endregion
public double Value {
get {
return DataRowsCalculator.Compute(this.Data, this.ComputeExpression,
this.RowFilter);
}
}
}
Πέρα από τον κώδικα στον constructor και
τα properties, το μόνο που μας ενδιαφέρει σε αυτό το κομμάτι
κώδικα είναι η static Compute μέθοδος
που υπολογίζει τελικά το αποτέλεσμα, και η υλοποίηση του Value property
που το μόνο που κάνει είναι να καλεί τη static μέθοδο.
Τίποτα δυσνόητο.
Το «ζουμί» σε αυτή την
κλάσση, είναι ότι είναι ορθάνοιχτη σε οποιονδήποτε υπολογισμό σε μια στήλη ενός
DataTable. Μπορεί να εκτελεστεί είτε σε όλες, είτε σε
«φιλτραρισμένες» γραμμές στον πίνακα. Μια πολύ καλή αρχή με άλλα λόγια.
Θα μπορούσα να είχα αποφύγει
το παραπέρα subclassing, αλλά θα μου
«βρώμιζε» τον κώδικα, οπότε «σκέφτηκα και βρήκα το πιο σωστό, τον γάτο να
τσακώσω, σα μούτρο αναρχικό», και αποφάσισα να βγάλω subclasses κατά
βούληση, που κάνουν την εκάστοτε πράξη στα δεδομένα. Μιας και ήθελα για αρχή
μόνο προσθέσεις ( αποφάσισα να βγάλω αρχικά μόνο τα σύνολα ανά κατηγορία, και
το γενικό σύνολο ), η πρώτη subclass ήταν ο RowSumCalculator:
/// <summary>
/// Computes the sum of the
rows found here ...
/// </summary>
public class RowSumCalculator : DataRowsCalculator
{
public RowSumCalculator(DataTable
data, string columnName, string filter)
: base(data, string.Format("sum({0})", columnName), filter)
{
// no work necesary here !!! :P
}
}
Κάπου εδώ, την ψυλλιάστηκα
ότι μάλλον η προηγούμενη κλασσούλα ήταν τελικά καλή ιδέα, και μάλλον ήταν πολύ
βολικό τελικά που δεν έκανα virtual το Value property. Θα
μπορούσα να κάνω προσθέσεις, αφαιρέσεις, πολλαπλασιασμούς κτλ κτλ με 4 γραμμές
κώδικα τη φορά, και να κάνω και τον τελικό κώδικα που τα χρησιμοποιεί πολύ πιο
ξεκάθαρο και κατανοητό στον «αναγνώστη».
Ήρθε λοιπόν η στιγμή να
δοκιμάσω το πρώτο μικρό τεστάκι μου, και να βγάλω ( στην κονσόλα, σαν
πραγματικός άντρας :D ) τα σύνολα ανα κατηγορία δαπάνης:
// ok, let's see .. what
do I need ???
// first Calculators for the categories ...
// "buffer' them, it'll come in handy ...
IDictionary<int, ICalculator> categoryCalcs = new Dictionary<int, ICalculator>();
// create them
...
for (int i = 1; i
< 4; i++) {
categoryCalcs
= new RowSumCalculator(_items.Tables["ExpenseItems"], "Price",
string.Format("Category
= {0}", i));
Console.WriteLine("Category
{0}: {1}", i, categoryCalcs
.Value);
}
Και γουάου !!! πολύ όμορφα
και αναμενόμενα το αποτέλεσμα ήταν 3 γραμμούλες με το σωστό συνολάκι ανα
κατηγορία. No big deal ως εδώ όμως, το μόνο που
κατάφερα ήταν να γράψω περίπου 50 γραμμές κώδικα, για να αποφύγω να γράψω … 4 …
αν το έκανα hard-coded. Το μυστικό σε αυτή τη φράση όμως είναι το hard-coded. Αυτός ο
κώδικας θα μου μείνει, και διαβάζεται. Ο άλλος θα ήταν one-off και υποψήφιος για copy-paste
inheritance και
μπελάδες. Τέλος πάντων όμως.
Ωραία ως εδώ. Αλλά τώρα,
έπρεπε να βγάλω το γενικό σύνολο. Άρα, έπρεπε να βρω ένα τρόπο να κάνω «compose» αυτούς τους υπάρχοντες σε έναν τρίτο, που θα
μπορούσε να προσθέσει τα αποτελέσματά τους.
Πάμε λοιπόν για μια ακόμα
υλοποίηση του ICalculator interface, αυτή τη φορά για πιο composite περιπέτειες.
Σε ακριβώς αυτό το σημείο,
μου «έσκασε» ακόμα μια ανάμνηση – πρίν κάποιον καιρό πάλι, είχα γράψει ένα
μικρό κομμάτι κώδικα σε ένα post
του Δημήτρη (papadi) που στην ουσία υλοποιούσε το Iterator – Visitor pattern. Το είχα κάνει σε μια
κλασσούλα που έπαιρνε ως παραμέτρους ένα enumeration από controls, κι
έναν multicast delegate
ο οποίος περιείχε function pointers σε
μεθόδους οι οποίες έκαναν visible/not visible/enabled/readonly τα controls βάσει
… οποιονδήποτε συνθηκών είχαν μέσα τους.
Δεν είχα το χρόνο να κοιτάξω
το post, αλλά θυμήθηκα το principle της λειτουργίας του και σκέφτηκα ότι αν έπαιζα έτσι,
θα είχα έναν aggregate calculator όσο ευέλικτος ήταν και ο DataRowsCalculator. Θα μπορούσα να βγάζω subclases κατά
βούληση με ένα method definition κι
ένα IΕnumerable instance. Καλή ιδέα !
/// <summary>
/// Defines a basic
"aggregate" calculator, one that combines the values
/// of it's child
calculators ...
/// </summary>
public class AggregateCalculator : ICalculator
{
/// <summary>
/// Define the footprint of
a method that operates on
/// a collection of
ICalculators, and returns a double
/// </summary>
/// <param name="calcs"></param>
/// <returns></returns>
public delegate double AggregateOperation(IEnumerable<ICalculator>
calcs);
#region private members
/// <summary>
/// The list of calculators
I'll be working with ...
/// </summary>
private IEnumerable<ICalculator> m_Children = null;
/// <summary>
/// The actual function
pointer to the method that will
/// implement my
computation
/// </summary>
private AggregateOperation
m_Operation = null;
#endregion
public AggregateCalculator(IEnumerable<ICalculator> calcs, AggregateOperation
operation) {
// ok, i can't be bothered to write properties, so I'll be
doing my checks in here instead ...
if (null == calcs)
throw new ArgumentNullException("calcs",
"Cannot be initialized to null");
if (null ==
operation)
throw new ArgumentNullException("operation",
"Cannot be initialized to null");
if (operation.GetInvocationList().Length > 1)
throw new ArgumentException("Cannot
use multi-cast delegates for an AggregateOperation", "operation");
m_Operation = operation;
m_Children = calcs;
}
/// <summary>
/// ok, implement the base
interface, by just calling my
/// delegate method to get
back the result, whatever that may be
/// </summary>
public double Value {
get {
return m_Operation(m_Children);
}
}
}
Δε μου πήρε παραπάνω από 10
λεπτά η όλη διαδικασία. Το actual
coding όπως
βλέπετε, μάλλον λιγότερο από 5. Κι ήμουν έτοιμος να το δοκιμάσω …
Τώρα σε αυτή τη φάση
συγκρατήθηκα. Δεν έκανα subclass για την πράξη της πρόσθεσης,
απλώς του έδωσα ένα anonymous delegate να φάει, κι έκανα τη
δουλειά μου – κράτησα το subclassing για πιο «σοβαρές» περιπτώσεις.
// hmmm ... ok, cool. Now I need to
define the Totals calculator, innit ?
// I'll need a delegate instance for that though ...
AggregateCalculator.AggregateOperation
sumOperation =
delegate(IEnumerable<ICalculator> calcs) {
double d = 0.0;
foreach (ICalculator
calc in calcs)
d += calc.Value;
return d;
};
AggregateCalculator
totalCategories = new AggregateCalculator(categoryCalcs.Values,
sumOperation);
// way cool ???!?! :D
Console.WriteLine("Total : {0}", totalCategories.Value);
Το έτρεξα και … et voilat
!!! Έπαιξε χαρούμενα και όμορφα, και με έκανε ένα πολύ πολύ ευτυχισμένο anjelinio :]
Αν δεί κάποιος το μικρό αυτό
sample prototype,
ίσως σκεφτεί ότι είναι overkill. Αυτό
ισχύει στο prototype μόνο όμως. Στην «πραγματική ζωή» χρειαζόταν να κάνω
πιο περίπλοκες πράξεις, και είχα σίγουρα πολύ περισσότερα tables να
υπολογίσω.
Τελικά μετά το prototype αυτό,
είμαι σίγουρος ότι γλίτωσα αρκετές ώρες debugging, αν μη τι άλλο γιατί με αυτό το approach επικεντρώθηκα στο process, στο τι πράξεις χρειάζεται να κάνω και στο δέσιμο
των caclulators σε
αυτό που τελικά γίνεται μια αλυσίδα από dependend calculators.
Δεν επικεντρώθηκα σε trivial κώδικα για υπολογισμούς στο DataTable, χιλιάδες ενδιάμεσες μεταβλητές, πολλές γραμμές
κώδικα και ότι άλλο μπορεί να μετατρέψει μια σελίδα κώδικα σε .. μακαρονάδα.
Ήταν καθαρό, reusable, και πολύ λιγότερο error prone.
Ε, και ήταν και πιο όμορφο
ρε παιδιά … πολύ πιο όμορφο.
Αλλά τέλος πάντων. Στο
συμπέρασμα.
Αυτό το approach πλησιάζει
πολύ σε αυτό που λεν’ “functional progarmming”. Δεν το σκέφτηκα
εκείνη τη στιγμή, αλλά ισχύει. Κι όταν βρήκα το χρόνο, με οδήγησε στο να ψάξω
λίγο περισσότερο το functional programming, ειδικά εν όψει LINQ, το οποίο τελικά είναι απλώς μια πολύ όμορφη
υλοποίηση η οποία εκμεταλλεύεται τα νέα functional features της C#. To LINQ με
άλλα λόγια, το είδα σαν το “killer
app” για το functional programming σε C#.
Αποφάσισα λοιπόν να ξαναδώ
τούτο το μικρό sample, και σιγά-σιγά να το
περάσω πρώτα από Generics ( όπως είδατε είναι hard coded
να γυρνάνε double όλα
), και μετά στην Orcas εποχή. Κι όταν κάνω κάθε βήμα, θα το post-άρω κι εδώ να το συζητάμε …
Καλό βράδυ, και may the source be with us. To
sample είναι attached στο post.
Angel
O:]