Fluent Interfaces example using Delphi part II
Here is the second part of this interesting topic. As I'm still trying to redeem myself from my non popular first example, I'm sure this one will reach the expectations. For this example I'm trying to mimic the way LINQ works, using generics and delegates and I have adapted my solution using fluent Interfaces as well. This solution presents a IQueryList which contains a TList<T> which can be queried like we were using SQL. So, we can select certain values and apply a where clause to filter the final list. This example will give you a hint on how to correctly implement fluent interfaces and how to extend this functionality for your applications.
Delegates:
uses Generics.Collections, Generics.Defaults; type TProc<T> = procedure (n : T) of Object; TProcList<T> = procedure (n : TList<T>) of Object; TFunc<T> = reference to function() : T; TFuncParam<T, TResult> = reference to function(param : T) : TResult; TFuncList<T, TResult> = reference to function(n : TList<T>) : TResult; TFuncListSelect<T, TResult> = reference to function(n : TList<T>) : TList<T>;
IQueryList:
IQueryList<T, TResult> = interface function Where(const param : TFuncParam<T, TResult>) : IQueryList<T, TResult>; function OrderBy(const AComparer: IComparer<T>) : IQueryList<T, TResult>; function Select(const param : TFuncListSelect<T, TResult>) : IQueryList<T, TResult>; function FillList(const param : TFuncList<T, TResult>) : IQueryList<T, TResult>; function Distinct() : IQueryList<T, TResult>; function List() : TList<T>; end; TQueryList<T, TResult> = class(TInterfacedObject, IQueryList<T, TResult>) private FList : TList<T>; protected function Where(const param : TFuncParam<T, TResult>) : IQueryList<T, TResult>; function FillList(const param : TFuncList<T, TResult>) : IQueryList<T, TResult>; function Select(const param : TFuncListSelect<T, TResult>) : IQueryList<T, TResult>; function Distinct() : IQueryList<T, TResult>; function List() : TList<T>; function OrderBy(const AComparer: IComparer<T>) : IQueryList<T, TResult>; public constructor Create(); destructor Destroy(); override; class function New: IQueryList<T, TResult>; end; constructor TQueryList<T, TResult>.Create(); begin FList := TList<T>.Create; end; destructor TQueryList<T, TResult>.Destroy; begin if Assigned(FList) then FList.Free; inherited; end; function TQueryList<T, TResult>.Distinct: IQueryList<T, TResult>; var list : TList<T>; i : integer; begin list := TList<T>.Create(); for i := 0 to FList.Count-1 do begin if not list.Contains(FList[i]) then list.Add(FList[i]); end; FList.Free; FList := list; result := Self; end; function TQueryList<T, TResult>.FillList(const param : TFuncList<T, TResult>): IQueryList<T, TResult>; begin param(FList); Result := Self; end; function TQueryList<T, TResult>.List: TList<T>; begin result := FList; end; class function TQueryList<T, TResult>.New: IQueryList<T, TResult>; begin result := Create; end; function TQueryList<T, TResult>.OrderBy(const AComparer: IComparer<T>): IQueryList<T, TResult>; begin FList.Sort(AComparer); result := Self; end; function TQueryList<T, TResult>.Select(const param: TFuncListSelect<T, TResult>): IQueryList<T, TResult>; begin FList := param(FList); result := Self; end; function TQueryList<T, TResult>.Where(const param: TFuncParam<T, TResult>): IQueryList<T, TResult>; var list : TList<T>; i: Integer; Comparer: IEqualityComparer<TResult>; begin list := TList<T>.Create(); for i := 0 to FList.Count-1 do begin Comparer := TEqualityComparer<TResult>.Default; if not Comparer.Equals(Default(TResult), param(FList[i])) then list.Add(FList[i]); end; FList.Free; FList := list; result := Self; end;
Example Implementation <Integer, Boolean>:
procedure DisplayList(); var IqueryList : IQueryList<Integer, Boolean>; item : integer; begin //Create the list and fill it up with random values IqueryList := TQueryList<Integer, Boolean> .New() .FillList(function ( list : TList<Integer> ) : Boolean var k : integer; begin for k := 0 to 100 do list.Add(Random(100)); result := true; end); //Display filtered values for item in IqueryList .Select(function ( list : TList<Integer> ) : TList<Integer> var k : integer; selectList : TList<Integer>; begin selectList := TList<Integer>.Create; for k := 0 to list.Count-1 do begin if Abs(list.items[k]) > 0 then selectList.Add(list.items[k]); end; list.Free; result := selectList; end) .Where(function ( i : integer) : Boolean begin result := (i > 50); end) .Where(function ( i : integer) : Boolean begin result := (i < 75); end) .OrderBy(TComparer<integer>.Construct( function (const L, R: integer): integer begin result := L - R; //Ascending end )).Distinct.List do WriteLn(IntToStr(item)); end;
This example fills up an initial list with 100 random numbers and then I query the list to give me all the values from the list which absolute value is greater than 0 and the values are between 50 and 75. From this list I want all the values ordered by value and I do not want repeated numbers (distinct method).
Example Implementation <String, String>:
procedure DisplayStrings(); const Chars = '1234567890ABCDEFGHJKLMNPQRSTUVWXYZ!'; var S: string; IqueryList : IQueryList<String, String>; item : String; begin //Fill up the list with random strings IqueryList := TQueryList<String, String> .New .FillList(function ( list : TList<String> ) : String var k : integer; l : Integer; begin Randomize; for k := 0 to 100 do begin S := ''; for l := 1 to 8 do S := S + Chars[(Random(Length(Chars)) + 1)]; list.Add(S); end; result := ''; end); //Query the list and retrieve all items which contains 'A' for item in IqueryList.Where(function ( i : string) : string begin if AnsiPos('A', i) > 0 then result := i; end).List do WriteLn(item); end;
The string example is quite similar, it fills up a list with 100 random string values and then it filters the list displaying only items which contains the character "A".
Everything is based on interfaces so the garbage collector can step in and avoid memory leaks and you can find a sound widespread of generics, delegates and chaining methods all in once. This solution gives you control on the way data is treated and it can work with any type as it is using generics.
I have included all those examples in my personal repository on Google project. Please feel free to use it and comment everything you like/dislike so we all can improve those examples. The Unit testing side includes interesting examples:
Enjoy it!.
I look forward to your comments.
I have included all those examples in my personal repository on Google project. Please feel free to use it and comment everything you like/dislike so we all can improve those examples. The Unit testing side includes interesting examples:
Enjoy it!.
I look forward to your comments.
Jordi
Related Links:
Good luck debugging this mess or if you need to make some subtle change in the future.
ReplyDeleteThat is the biggest problem with "fluent" programming. It may well be clever to be able to code in a "stream of consciousness" style, but that is not a recipe for maintainable code.
The slightest change in the fluent stream of consciousness can have huge ramifications "downstream". Ramifications that may be not be immediately apparent.
And when that happens, trying to debug without the benefit of intermediate results is going to be nothing but an exercise in frustration and a minefield of "tool-tip" induced side-effects.
Seriously, I cannot believe that the same development community that spits on "with" can at the same time - and with a straight face - embrace and promote this sort of approach.
Hi Jolyon,
DeleteThanks for your comment. Don't misundertand me, but this only shows an example. It is quite difficult in fact to use this in any app, but everything is about finding a balance.
I know it would be difficult to debug, but it is possible. The code is highly modificable and it can give you new ideas to look at code from a different perpective. When I develop using C# and LINQ, I know that internally is generating all this mess but it is transparent for the user and it is presented in a user-friendly way.
I implemented something similar (what I would have called interface chaining) in my persistence framework (as an exercise having used LINQ in the job). Mine was a little more simple in that it didn't use delegates (so a little less flexible too I guess). It ended up something like this:
DeleteCustomers.Select.Where('City').Equals('Dublin').OrderBy('Created')
While I very much like this style of declaration, it is something that as Jolyon quite vehenemtly points out - was pretty difficult to debug. Like it none the less.
D.
Thanks Darren,
DeleteI also like this kind of declaration as it is fluent and more human readable. (Apart from using delegates which mess a lot with the code). Hopefully Embarcadero will implement Alpha notation in the future and we will be able to write things like:
for item in IqueryList.Where( s => s = 'Dublin')do
WriteLn(item);
Jordi
Confusing greek letters? ;) It's called lambda
DeleteYes, sorry, I don't know what I was thinking!!!!
Delete@Jolyon: It's not the fault of fluent programming. It's the fault of Delphi and what passes for anonymous functions. I could write the entire first example in 3 lines of code (including import) and the second in 5 lines of code using Python. Delphi simply lacks LINQ (.NET), list comprehensions (Python, others) or another construct to make this type of programming simple and clear. There's nothing wrong with the *goal*; Raymond Hettinger says that he believes that any statement that can be expressed in a single English sentence should be able to be written in a single line of code. The problem is that Delphi lacks the constructs to do this.
DeleteOtherwise I'm completely with you... trying to decipher what the code was doing was painful. But there's no problem understanding Python like
new_list = [num for num in old_list if 50 <= num <= 75]
Heck, it can be rendered to unique values and sorted in one line too:
new_list = sorted(set(num for num in old_list if 50 <= num <= 75))
What I don't understand is how the author could write all that code to do this and not realize in the end that there's something seriously wrong/broken with Delphi if it takes that much code to do something that can be expressed in such few words. People need to spend less time writing vast amounts of code like this and more time demanding Embarcadero improve the language for all the money they're being paid. Languages available for free shouldn't be more expressive.
Hi Jordi - and don't misunderstand _me_. :)
ReplyDeleteI do appreciate what you are trying to show, and "fluent" certainly has it's place. But I do not believe that place is in a lot of the places that other people try to fit it into.
But I think your (if you'll forgive me) rather dismissive response to the question of debug difficulty is at the root of this industry's failure to properly mature. Developers are too often distracted by and smitten with the latest and greatest ways of creating code that they don't stop to consider the fact that code spends an infinitely small proportion of it's life being created, and the vast, overwhelming majority of it's life simply existing. Being modified and added to.
Creating code doesn't need to be made easy. Living with code is what needs to be made easy.
For me, that means promoting debuggability above convenience and "slick"ness (the best of all worlds is obviously the ideal, but if a technique is supported by a caveat that "yes it makes debugging harder but..." then it fails from the outset (in terms of it's useful application to the task at hand).
As I say, fluent has a place but it is a very, VERY limited place (I have used it myself in my testing framework, but this is a framework which itself does not need to facilitate debugging in the environment in which it is used - that's what the tests are for that you use the framework itself to write).
imho. ymmv
Hi Jolyon,
DeleteThanks for your comment again, I really appreciate it. I totally agree with you as to why it would be hardly difficult to debug. My idea would be using this kind of concept in a Framework rather than in production code. At least using Delphi XE, it was quite easy to debug and go through all the delegates. Hopefully one day Embarcadero will apply alpha notation and then we will back on business.
This whole concept was after getting a deep understanding of LINQ and then I decided to go and try to mimic the behaviour using Delphi. Delphi is still way behind but using generics, delegates and chaining methods I was able to mimic the functionality.
Jordi
Hi Jordi, I see you are working hard writing greats post. How do you get time :) ? Well, I released an alfa version of a persistence engine in Delphi inspired in JDO and using fluent interfaces for query data. I need a lot of help, so I like it so much if you want colaborate: http://jcangas.github.com/SummerFW4D/overview.html
ReplyDeleteHi Jorge,
DeleteThanks for your comment. Nowadays I'm extremely busy but I always have time to publish interesting stuff. I have taken a look at your project and it looks really interesting. I would be more than happy to collaborate in your project! :).
First I don't understand the need to redeclare already existing types (TProc and TFunc - look into SysUtils)
ReplyDeleteThen it might be better to actually refer to other existing collections libraries (DeHL is discontinued) like delphi-coll or delphi-spring instead of reinventing this another time (yes, it's just for demonstration purpose).
Your examples are just to much. You have a function that internally uses half a dozen extension methods some of them even reduncant (the between 50 and 75 condition - by the way you are not taking the bounds into account, since you say greater or less - can be done with one Where call).
Your FillList method has a weird logic and is useless imo - whats the benefit instead of putting something into a fluent interface method that does not belong there. A for loop with calling .Add would have been better (actually it makes sense that this method does not exist in .Net).
Extension methods like Where, Select, Distinct, Reverse or OrderBy make sense when you like to get the data in the list in a certain form because that can be much more readable.
Unfortunately (at least in my opinion) you are showing how this coding style can get more confusing instead of actually be helpful.
About the debugging. Actually the library code itself should be unit tested - so no need to debug it (meaning: stepping through the code).
If you have a bug in your own code that could be most likely eithe wrong data in the list itself or you have wrong logic in your delegates. Both can easily be debugged.
Hi Stefan,
DeleteThanks for your comment. Maybe you are right and my examples try to illustrate too much but this was just an overview. Maybe those are not the best examples but at least those are a solution for an specific problem.
About the "Where" clause you are right, it is pointless, but I wanted to show that you can chain several "wheres" with different arguments and your final result would be affected as well.
Maybe it would have been clearer using a simple example like this:
for item in IqueryList.Where('Dublin').List do
WriteLn(item);
But then, you can ask, why instead of using a parameter "Dublin" we can write Delphi code to filter the query? That's the reason for the delegates. With this configuration the user can highly customize the query using "Delphi code" and the control of the query relies on it. That's the aim of LINQ. I'm not inventing anything new, I'm just trying to mimic what I learnt from LINQ and how they started using generics, delegates and extension methods in C#. Maybe I should post about it as at least to me it helped me a lot to understand the magic - because at the beginning everything was like magic, just writing something like:
var Customers =
from c in customers
where c.city = 'Dublin'
select c;
And then when I realised that internally it was using the same as I have described in this post, it helped me to deal with it without tears.
Jordi
Just because it is fluent doesn't mean it HAS to be used that way. It is just as simple to write this as:
ReplyDeleteIqueryList := TQueryList;
IqueryList.New();
IqueryList.FillList(....
Thanks SKamradt,
DeleteThat's the beauty of this method, that you can use whichever suits you best.
You can use simple declaration like you suggested:
iQueryList := TQueryList<>.New(); or ever .Create();
and then operate with the variable:
list := iQueryList.List();
Or do it all together:
list := iQueryList.New.FillList().Where().OrderBy().List;
Fluent is not a mere style it is a paradigm. So messy code indicates a misapplied paradigm. I suggest PHP Solar framework as well architected fluent paradigm worth studying, with dependency injection, adapters etc etc. Keith.
ReplyDeleteThanks Keith, I'm sure it should be interesting.
DeleteJordi