So, I saw Nick Hodges 's post today, the interesting part is:
So, I saw Nick Hodges 's post today, the interesting part is:
I was thinking about if statements the other day, and came up with this potential Rule of Thumb: “Every time you write an if statement, you should stop and consider whether you should be creating a new class instead.” Thoughts? (Now, I’ve been around long enough to see how this might go. Before commenting, note that I did not say “Every time you use an if statement, you should create a new class”. I merely said that it is a point at which you should consider it…)
How would you replace "if" in the following scenario using a class?:
type MyCoolEnum = (mceFirst, mceSecond, ..., mceLast);
procedure Something(Value: MyCoolEnum);
begin
if Value = mceFirst then
///...
else
if Value = mceSecond then
///...
else
if Value = ... then
///...
else
if Value = mceLast then
///....
end;
I get that you can do a case, or a lookup table, but humor me and give an example of transforming my monster-if's into a class.
NOTE:
- I'm not trying to debate if using a class rather than an IF is better or vice-versa, I'm trying to understand this concept, having a hard time getting it.
http://www.nickhodges.com/post/Flotsam-and-Jetsam-77.aspx
I was thinking about if statements the other day, and came up with this potential Rule of Thumb: “Every time you write an if statement, you should stop and consider whether you should be creating a new class instead.” Thoughts? (Now, I’ve been around long enough to see how this might go. Before commenting, note that I did not say “Every time you use an if statement, you should create a new class”. I merely said that it is a point at which you should consider it…)
How would you replace "if" in the following scenario using a class?:
type MyCoolEnum = (mceFirst, mceSecond, ..., mceLast);
procedure Something(Value: MyCoolEnum);
begin
if Value = mceFirst then
///...
else
if Value = mceSecond then
///...
else
if Value = ... then
///...
else
if Value = mceLast then
///....
end;
I get that you can do a case, or a lookup table, but humor me and give an example of transforming my monster-if's into a class.
NOTE:
- I'm not trying to debate if using a class rather than an IF is better or vice-versa, I'm trying to understand this concept, having a hard time getting it.
http://www.nickhodges.com/post/Flotsam-and-Jetsam-77.aspx
I guess you'd create a base class then derive from it once for every value of MyCoolEnum overriding the method Something. Then instead of calling Something(value) you'd first create an object of the class corresponding to the enum value and call it's Something method. In the end you wouldn't have MyCoolEnum any more, just instances of classes.
ReplyDeleteMeg Noz so, in stead of doing "if _ then", I'd have to create a bunch of classes, one for each enum value and maybe a factory so in stead of "Something(Value);", I'd do:
ReplyDelete///...
var
LMyCoolEnumInstance: TMyCoolEnumBase;
begin
LMyCoolEnumInstance := MyCoolEnumFactory.CreateInstance(Value);
LMyCoolEnumInstance.Execute;
LMyCoolEnumInstance.Free;
end;
?
Dorin Duminica And in this line:
ReplyDeleteLMyCoolEnumInstance := MyCoolEnumFactory.CreateInstance(Value);
Simply moving the if clause into the factory?
Bill Meyer yeah... I'm not saying that's the solution, but that's the way I see it in converting the "if" into class(es) in order to implement the "class in stead of if" concept, thoughts?
ReplyDeleteI think so. If you remember the classic polymorphism example of TShape, TCircle and TSquare where the last two just override a Draw method: Your initial example would relate to just using one procedure Draw(AShape: TShapeEnum) and within it doing "if AShape = shCircle then...".
ReplyDeleteThe benefit of using classes would be that you only have one "factory" call and the subsequent code wouldn't have to pass the same enum value all the time to different procedures.
At least that's how I understand it.
Dorin Duminica Reminds me of numerous talks on XAML I have attended, where the presenter proudly delares "and I did it without code!" Uh, well, no. What XAML does is provide a means of passing parameters to someone else's very well debugged code. ;)
ReplyDeleteAn alternative to the if statement for a factory might be to use a dictionary to contain the pairs, but then you have to initialize the dictionary. Not sure that's an improvement; more like a distinction without a difference.
In some cases, I have used a constant array of records to contain the pairs of key and class. It avoids the if tree, avoids the initialization of a dictionary, but really, is it so much different? We still require a means of making selections, and that is why we have if and case. It comes down, for me, to where you put the maintence burden, and I am inclined to prefer my const array of record to the truly mind-numbing and pages long if tree.
The solution is called command pattern. Google for "strategy pattern vs. case statement" and you find some nice articles explaining why and how you can do that.
ReplyDeleteWe just had a question on SO where someone implemented that pattern and wanted to have compiletime safety instead of runtime resolving (if you need speed and still want to get rid of some huge case blocks): http://stackoverflow.com/questions/14718688/is-there-any-way-to-declare-a-const-reference-to-a-method-at-compile-time
Stefan Glienke Saw that SO post and implemented bytecode VM's, that's the "lookup" table which I wrote in the post(I get that you can do a case, or a lookup table,...).
ReplyDeleteWhat I'm interested in is how others(you guys) would turn an "if" into one or more classes in order to make it more elegant.
Stefan Glienke One small point, in response to something you wrote on SO: Using absolute is likely to become a maintenance issue, as Allen Bauer seems pretty determined to make that go away.
ReplyDeleteBill Meyer I am actually used to find broken stuff right and left while they continue their crusade to implement new stuff into the compiler. If it's some compiler error that tells me "absolut not supported" that is my smallest problem. ;)
ReplyDeleteDorin Duminica As Nick said you have to ask yourself if it makes sense. We are not saying turn every case and if into a class and introduce virtualness all over the place. But the strategy pattern is called like that for a reason. If your case is deciding on some kind of strategy on how to continue your program flow you might consider this pattern. It might cause reusable and better testable code!
Stefan Glienke LOL! Oh, and I note that the Wikipedia article on the Command patternis lacking a Delphi example, which I am sure you could provide. ;)
ReplyDeleteAlthough the constant initialization was very neat, it still is an alternative to initialize elsewhere at run time.
ReplyDeleteIf you have dispatch methods where you if then else or case through a list - you could f.x. build a factory class and register in the handlers and look them up through a sorted list or hash table in the factory.
Bill Meyer There you go:
ReplyDeleteI am using meta classes so I don't have to create an instance of the strategy because in this example the strategy is stateless and does not need to be an instance.
program StrategyPattern;
{$APPTYPE CONSOLE}
uses
SysUtils;
type
TStrategyClass = class of TStrategy;
TStrategy = class
class function Execute(a, b: Integer): Integer; virtual; abstract;
end;
TAdd = class(TStrategy)
class function Execute(a, b: Integer): Integer; override;
end;
TSubtract = class(TStrategy)
class function Execute(a, b: Integer): Integer; override;
end;
TMultiply = class(TStrategy)
class function Execute(a, b: Integer): Integer; override;
end;
class function TAdd.Execute(a, b: Integer): Integer;
begin
Writeln('Called TAdd.Execute()');
Result := a + b;
end;
class function TSubtract.Execute(a, b: Integer): Integer;
begin
Writeln('Called TSubtract.Execute()');
Result := a - b;
end;
class function TMultiply.Execute(a, b: Integer): Integer;
begin
Writeln('Called TMultiply.Execute()');
Result := a * b;
end;
type
TCalcStrategy = (Add, Subtract, Multiply);
const
CalcStrategies: array[TCalcStrategy] of TStrategyClass =
(
TAdd,
TSubtract,
TMultiply
);
var
a, b, c: Integer;
begin
a := CalcStrategies[Add].Execute(3, 4);
b := CalcStrategies[Subtract].Execute(3, 4);
c := CalcStrategies[Multiply].Execute(3, 4);
Writeln('Result A: ', a);
Writeln('Result B: ', b);
Writeln('Result C: ', c);
Readln;
end.
Stefan Glienke Thanks! I have found many articles online (one of the links in the search you suggested exmplifies this) will show the code which motivates the pattern use, then discuss the pattern, but leave out the resulting code, as though it were obvious. Well, as the exposition often leaves much to be desired (we all know programmers hate to write documentation--often because their writing skills are poor), the article all too often fails to convey understanding.
ReplyDeleteOne of my pet peeves is the coders who are language syntax fanatics, but disdain any attention to the syntax and semantics of their native language... but I digress.
As a picture is worth a thousand words, your example code is a picture which communicates unambiguously something less easily expressed in prose.
Bill Meyer I have found that it is difficult to articulate these patterns. The Factory Pattern I'm reading about now will be particularly hard to describe.
ReplyDeleteBut nothing describes something better than code. ;-)
Nick Hodges I found that the GoF were not great at articulating some of them, either. ;) And even in articulating the summary of the pattern, they often made my eyes cross.
ReplyDeleteCode is the best descriptor, but even there, some code samples for patterns are so convuluted as to mask what they present. Others can be so trivial that they fail to convey the value. Balance is always the issue.
Bill Meyer That's why I like the Head First book -- it's "fun", and the examples, while simple, are illustrative. I confess I've only perused the GoF book. But perhaps I'll read it after HFDP.
ReplyDeleteNick Hodges It's been a while, but I did try the Head First book. Better than the GoF, but still not the Rosetta Stone I had hoped to find. Writing good software is hard; writing good explanations of software is much harder. At least, so it seems, from too many books on which I wasted money.
ReplyDeleteIn fairness, my aversion to Java was a factor in my assessment of the HF Patterns book, and of course, in the GoF.
For why if (and case) statements often are really just "hidden polymorphism", watch this video:
ReplyDelete"The Clean Code Talks -- Inheritance, Polymorphism, & Testing"
I have done this!!! :-) What I did back then was to have an Array whose boundaries would be the enum values and the item type was a class reference using "class of". Then I had a function creating the right object instance from the enum :-)
ReplyDeleteI am on my phone and writing code is very difficult :-)
It should be added that there were few classes and changes would be always done at compile time - no runtime changes :-)
ReplyDeleteIt's interesting to me that most examples I have seen of the Factory Pattern make use of if/then/else trees. Understandable, I suppose, in the context of presenting a minimal example on a web page, but I would hope that in any serious presentation, mention would be made, at least, of alternatives to that tree.
ReplyDeleteStefan Glienke One small quibble, and it applies as well to much of what is done in XAML in .NET: The selector code (if/then or case) has not been eliminated. In the example, the selector is now an indexer, which simply puts the actual work of the selector back inside RTL support, rather than explicit coding in the app.
ReplyDeleteBill Meyer True and the strategy pattern is not about removing code that makes decisions. It is about removing hardcoding them and more important putting the code for the different strategies in their own classes/methods (SRP).
ReplyDeleteI am starting to believe that OOP is crap, and we should be moving on to post-OOP, as in Rust and Go, and that post-oop languages like Go, and Rust, and extremely late-bound systems (like Objective-C) are the future. In these systems there are so many beautiful options that are open to you that are just not available in Delphi.
ReplyDeleteWarren Postma Which is why I can say with confidence that what we do is still far from becoming a science. We move from coding fad to coding fad. Each new paradigm is supposed to be nirvana. But nirvana never comes. Just new glittery stuff in which we have yet to see the downside. ;)
ReplyDelete