/
dynamic_toolkit.txt
2537 lines (1891 loc) · 90 KB
/
dynamic_toolkit.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
== Mastering the Dynamic Toolkit ==
=== Playing to Ruby's Strengths ===
If you've done even a little bit of Ruby, you probably have a sense of the great
flexibility and power it offers as a language. This chapter is designed to
underscore that point, specifically by showing you what can be accomplished when
you take the power of Ruby and unleash it onto itself. When we say that
everything is an object in Ruby, it's easy to forget that classes, modules,
and methods all fall into that category as well. With enough imagination, we
can think of all sorts of interesting applications that fall out of this elegant
design.
Take the fact that all of our programmatic constructs can be represented as
first-order data objects and combine it with the ability to modify any of them at
run-time. Then mix in the idea that everything from defining
a new function to calling a method that does not exist can be detected and
handled by custom code. Top this off with first-rate reflection capabilities
and what you'll find is that Ruby is a perfect foundation for writing highly
dynamic applications.
On the surface, these ideas may seem a bit esoteric or academic in nature. But
when you get down to it, there are a lot of practical uses for having such a
high degree of flexibility baked into Ruby. Because Ruby's dynamic nature is a
huge part of what makes the language what it is, you'll find no shortage of real
examples in this chapter. These run the gamut from dynamic interface generation
to safely modifying pre-existing code at runtime. But to get our feet wet,
we'll dive in with the same head-first approach found in the rest of the chapters
of this book. We're about to look at the code behind Jim Weirich's
+BlankSlate+ class, which provides an excellent case study of what can be
accomplished with the dynamic toolkit Ruby provides us.
If you feel a bit overwhelmed at first, don't be discouraged, each individual topic
will be discussed later on in the chapter in greater detail. For now, let's
just try to have fun and see just how powerful Ruby really is.
=== BlankSlate: A BasicObject on Steroids ===
Although Ruby 1.9 has BasicObject as a very lightweight object designed to be
used for implementing objects with dynamic interfaces and other similar tasks,
Ruby 1.8 users weren't so lucky. In light of this, +BlankSlate+ became a fairly
common tool for those who needed an object that didn't do much of anything. One
of the practical applications of this somewhat abstract object was in implementing
the XML generator in the 'builder' gem. If you've not seen XML Builder before, it
is a tool which turns Ruby code like this:
-------------------------------------------------------------------------------
builder = Builder::XmlMarkup.new(:target=>STDOUT, :indent=>2)
builder.person { |b| b.name("Jim"); b.phone("555-1234") }
-------------------------------------------------------------------------------
Into XML output like this:
-------------------------------------------------------------------------------
<person>
<name>Jim</name>
<phone>555-1234</phone>
</person>
-------------------------------------------------------------------------------
Without going into too much detail, it is obvious from this example that
+Builder::XmlMarkup+ implements a dynamic interface which can turn your method
calls into matching XML output. But if it had simply inherited
from +Object+, you'd run into certain naming clashes, wherever a tag had the same
name as one of `Object`'s instance methods.
Builder works by capturing calls to missing methods, which means it has trouble
doing its magic whenever a method is actually defined. For example: if +XmlMarkup+ were
just a subclass of +Object+, with no methods removed, you wouldn't be able produce
the following XML, due to a naming conflict.
-------------------------------------------------------------------------------
<class>
<student>Greg Gibson</student>
</class>
-------------------------------------------------------------------------------
The underlying issue here is that +Kernel#class+ is already defined for a different
purpose. Of course, if we instead inherit from an object that has very few methods
to begin with, this greatly lessens our chance for a clash.
BasicObject certainly fits the bill, as you can see with a quick glance at its
instance methods via irb:
-------------------------------------------------------------------------------
>> BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__]
-------------------------------------------------------------------------------
These methods form the lowest common denominator for Ruby, so +BasicObject+ is
pretty reasonable in its offerings. The key thing to remember is that a
+BasicObject+ is fully defined by this limited set of features, so you shouldn't
expect anything more than that. While this makes perfect sense in Ruby 1.9's
object heirarchy, it's somewhat interesting to see that +BlankSlate+ takes an
entirely different approach.
On Ruby 1.8, there was no +BasicObject+ class to speak of, so instead of
starting off with a tiny set of methods to begin with, +BlankSlate+ had to
do something to get rid of the significant baggage that rides along with Ruby's
+Object+ class. This is done in an especially clever way, and quick irb session
complete with the expected noise that results from removing potentially
important methods shows the primary interesting features of +BlankSlate+:
-------------------------------------------------------------------------------
>> class A < BlankSlate; end
=> nil
>> A.new
NoMethodError: undefined method `inspect' for #<A:0x42ac34>
...
>> A.reveal(:inspect)
=> #<Proc:0x426558@/Users/sandal/devel/rbp_code/dynamic_toolkit/blankslate.rb:43 (lambda)>
>> A.new
NoMethodError: undefined method `to_s' for #<A:0x425004>
...
>> A.reveal(:to_s)
=> #<Proc:0x422e30@/Users/sandal/devel/rbp_code/dynamic_toolkit/blankslate.rb:43 (lambda)>
>> A.new
=> #<A:0x425004>
>> A.new.methods
NoMethodError: undefined method `methods' for #<A:0x425004>
from (irb):8
from /Users/sandal/lib/ruby19_1/bin/irb:12:in `<main>'
>> A.reveal(:methods)
=> #<Proc:0x41ed6c@/Users/sandal/devel/rbp_code/dynamic_toolkit/blankslate.rb:43 (lambda)>
>> A.new.methods
=> [:inspect, :to_s, :methods, :__id__, :instance_eval, :__send__]
-------------------------------------------------------------------------------
After reading through this code, you should be able to get a sense of how it
works. `BlankSlate` isn't really a blank slate at all, instead, it's an object
that acts like a blank slate by hiding all of its methods until you tell them
explicitly to reveal themselves. This clever bit of functionality allows
+BlankSlate+'s initial instance methods to be kept to the absolute minimum.
Everything else can be explicitly revealed later, as needed.
`BlankSlate` does this per-subclass, so you can have different customized minimal
objects for different purposes in your system. Predictably, you can also
re-hide functions, including those which you add yourself.
-------------------------------------------------------------------------------
>> A.new.foo
=> "Hi"
>> A.hide(:foo)
=> A
>> A.new.foo
NoMethodError: undefined method `foo' for #<A:0x425004>
from (irb):18
from /Users/sandal/lib/ruby19_1/bin/irb:12:in `<main>'
>> A.hide(:inspect)
=> A
>> A.new
NoMethodError: undefined method `inspect' for #<A:0x40a484>
...
-------------------------------------------------------------------------------
All in all, although it is a bit more heavy-weight than +BasicObject+,
the +BlankSlate+ class may have its uses even on Ruby 1.9 due to this ability to
seamlessly hide and restore functionality on the fly. If you were thinking this
sounds like complicated stuff, you might be surprised. The core implementation
of +BlankSlate+ is relatively straightforward. Of course, the devil is in the
details, but the most interesting bits can be understood with a little
explaination.
-------------------------------------------------------------------------------
class BlankSlate
class << self
# Hide the method named +name+ in the BlankSlate class. Don't
# hide +instance_eval+ or any method beginning with "__".
def hide(name)
if instance_methods.include?(name) and
name !~ /^(__|instance_eval)/
@hidden_methods ||= {}
@hidden_methods[name] = instance_method(name)
undef_method name
end
end
def find_hidden_method(name)
@hidden_methods ||= {}
@hidden_methods[name] || superclass.find_hidden_method(name)
end
# Redefine a previously hidden method so that it may be called on a blank
# slate object.
def reveal(name)
unbound_method = find_hidden_method(name)
fail "Don't know how to reveal method '#{name}'" unless unbound_method
define_method(name, unbound_method)
end
end
instance_methods.each { |m| hide(m) }
end
-------------------------------------------------------------------------------
As you can see, the class is simply three short class methods, followed by the
call that causes all of `BlankSlate`'s instance methods to be hidden. Let's start
by taking a closer look at the +hide()+ method.
-------------------------------------------------------------------------------
def hide(name)
if instance_methods.include?(name) && name !~ /^(__|instance_eval)/
@hidden_methods ||= {}
@hidden_methods[name] = instance_method(name)
undef_method name
end
end
-------------------------------------------------------------------------------
Here you can see that +BlankSlate+ first checks to make sure the method name
passed to +hide()+ exists within the currently visable instance methods. Once
it checks to make sure it not one of the special reserved methods, it begins the
process of storing and hiding the specified method.
The technique used here is simply to initialize a +@hidden_methods+ hash within
the class, and then assign the method name as a key to the associated
`UnboundMethod` object. An `UnboundMethod` can be thought of as roughly similar to
a `Proc` object, but rather than being truly anonymous, it is later hooked up to
an object that knows how to make use of the function, which is typically an
object of the same class. As a trivial example, we can play around with
+String#reverse+ to illustrate this point:
-------------------------------------------------------------------------------
>> a = String.instance_method(:reverse)
=> #<UnboundMethod: String#reverse>
>> a.bind("foo").call
=> "oof"
-------------------------------------------------------------------------------
We'll take a closer look at this a little later, but suffice to say, by grabbing
the +UnboundMethod+ before removing the method definition, we have a way of
restoring the behavior in the future.
Since I assume that you can get the gist of what's going on with
+find_hidden_method()+ just by inspection, we can jump straight into the most
interesting code in +BlankSlate+, the method that actually restores the old
functionality.
-------------------------------------------------------------------------------
# Redefine a previously hidden method so that it may be called on a blank
# slate object.
def reveal(name)
unbound_method = find_hidden_method(name)
fail "Don't know how to reveal method '#{name}'" unless unbound_method
define_method(name, unbound_method)
end
-------------------------------------------------------------------------------
Here, the +find_hidden_method()+ helper is used to recall an UnboundMethod by
name. If it doesn't manage to find a matching name in the +@hidden_methods+
hash, an error is thrown. However, assuming the lookup went according to plan,
we can see that the method is redefined to call the newly re-bound method. All
the original arguments are passed on to the restored method call, so you end up
with the original behavior restored.
Although we've shown the key components of +BlankSlate+ here, we didn't go into
the full details yet. It's worth mentioning that because +BlankSlate+ inherits
from +Object+ and not +BasicObject+, it has to do some additional magic to deal
with modules inclusion, and it also must handle methods added to
+Object+ / +Kernel+. We'll get to these a little later, in the '"Registering Hooks and
Callbacks"' section of this chapter. For now, let's just quickly review the
concepts we've touched on.
In this initial exploration phase, we've caught a glimpse of +define_method+,
+instance_methods+, +instance_method+, +undef_method+, and +UnboundMethod+. Or
in English, we've seen an example of how to use reflection to determine the
names of the instance methods on a class, copy their implementations into
objects that could then be keyed by name in a hash, undefine them, and later
restore them by building up a new definition programatically. You have probably
noticed that even though these concepts are very high level, they're essentially
ordinary Ruby code, without any sort of magic. The rest of this chapter will
serve to reinforce that point.
Now that we've seen a few of these concepts in action, we'll slow down and discuss
what each one of them actually means, while diving even deeper into dynamic Ruby
territory. In this next example, we'll look at a favorite topic for budding
Rubyists. I'm going to share the secrets behind building flexible interfaces
that can be used for domain-specific applications. The heavy handed term for
this sort of thing is an "Internal Domain-Specific Language", but we don't
need to be so fancy, as it creates a lot of misconceptions. Building pleasant
domain-specific interfaces is a key feature of Ruby, and deserves some
discussion no matter what you want to call it.
=== Building Flexible Interfaces ===
As a heads up, you might start to feel a bit of deja-vu in this section. What
we'll cover here is basically a recap of what was discussed in the '"Designing
Beautiful APIs"' chapter, mixed in with a little dynamic help here and there.
Though each step may seem fairly inconsequential, the end result is quite
powerful.
When implementing a flexible domain specific interface, the idea is that we want
to strip away as much boilerplate code as possible so that every line expresses
something meaningful in the context of our domain. We also want to build up a
vocabulary to work with, and express our intents in that vocabulary as much as
possible. A domain specific interface puts Ruby in the background, available
when you need it, but not as in-your-face as ordinary programmatic interfaces
tend to be. An easy comparison would be to look at the difference between some
elementary Test::Unit code and its RSpec equivalant footnote:[This example is
from the RSpec homepage at http://rspec.info, with minor modifications].
First, we'll look at the vanilla +Test::Unit+ code.
-----------------------------------------------------------------------------
class NewAccountTest < Test::Unit
def setup
@account = Account.new
end
def test_must_start_with_a_zero_balance
assert_equal Money.new(0, :dollars), @account.balance
end
end
-----------------------------------------------------------------------------
To a Rubyist, this code might seem relatively clear, straightforward, and
expressive. However, its defining characteristic is that it looks like any
other Ruby code, with all the associated benefits and drawbacks. Others prefer
a different approach, which you can clearly see in this RSpec code:
-----------------------------------------------------------------------------
describe Account, " when first created" do
before do
@account = Account.new
end
it "should have a balance of $0" do
@account.balance.should eql(Money.new(0, :dollars))
end
end
-----------------------------------------------------------------------------
When we read RSpec code, it feels like we're reading specifications rather than
Ruby code. Many people feel this is a major advantage, because it encourages to
express ourselves in a domain specific context. When it comes to testing, this
does create some controversy because while the RSpec code is arguably more
readable here, the +Test::Unit+ code is certainly less magical. But in the
interest of avoiding politics, I've shown this example to illustrate the
difference between two styles, not to advocate one over the other.
Even though some particular uses of domain-specific interfaces can be a
touchy subject, you'll find many cases where they come in handy. To help you
get a feel for how they come together, we'll be looking at some problems and
their solutions. We're about to walk through the life-cycle of
wrapping a nice interface on top of Prawn's +Document+ class. Don't worry
about the particular domain, instead, focus on the techniques we use so that you
can make use of them in your own projects.
==== Making instance_eval() optional ====
In the last chapter, we covered a common pattern for interface simplification,
allowing you to turn code like this:
-------------------------------------------------------------------------------
pdf = Prawn::Document.new
pdf.text "Hello World"
pdf.render_file "hello.pdf"
-------------------------------------------------------------------------------
Into something like this:
-------------------------------------------------------------------------------
Prawn::Document.generate("hello.pdf") do
text "Hello World"
end
-------------------------------------------------------------------------------
As you'll recall, this trick is relatively straightforward to implement:
-------------------------------------------------------------------------------
class Prawn::Document
def self.generate(file, *args, &block)
pdf = Prawn::Document.new(*args)
pdf.instance_eval(&block)
pdf.render_file(file)
end
end
-------------------------------------------------------------------------------
However, there is a limitation that comes with this sort of interface. Because
we are evaluating the block in the context of a +Document+ instance, we do not have
access to anything but the local variables of our enclosing scope. This means
the following code won't work:
-------------------------------------------------------------------------------
class MyBestFriend
def initialize
@first_name = "Paul"
@last_name = "Mouzas"
end
def full_name
"#{@first_name} #{@last_name}"
end
def generate_pdf
Prawn::Document.generate("friend.pdf") do
text "My best friend is #{full_name}"
end
end
end
-------------------------------------------------------------------------------
It'd be a shame to have to revert to building this stuff up manually, and a bit
messy to rely on storing things in local variables. Luckily, there is a middle
of the road option: We can optionally yield a +Document+ object. Here's how
we'd go about doing that:
-------------------------------------------------------------------------------
class Prawn::Document
def self.generate(file, *args, &block)
pdf = Prawn::Document.new(*args)
block.arity < 1 ? pdf.instance_eval(&block) : block.call(pdf)
pdf.render_file(file)
end
end
-------------------------------------------------------------------------------
This new code preserves the old +instance_eval+ behavior, but allows a new
approach as well. We can now write the following code without worry:
-------------------------------------------------------------------------------
class MyOtherBestFriend
def initialize
@first_name = "Pete"
@last_name = "Johansen"
end
def full_name
"#{@first_name} #{@last_name}"
end
def generate_pdf
Prawn::Document.generate("friend.pdf") do |doc|
doc.text "My best friend is #{full_name}"
end
end
end
-------------------------------------------------------------------------------
Here, the code is an ordinary closure, and as such, can access the
instance methods and variables of the enclosing scope. Although we need to go
back to having an explicit receiver for the PDF calls, our +Document.generate+
method can still do its necessary setup and teardown for us, salvaging some of
its core functionality.
The feature that makes this all possible is +Proc#arity+. This method tells you
how many arguments, if any, the code block was given. Here's a few examples
as an illustration:
-------------------------------------------------------------------------------
>> lambda { |x| x + 1 }.arity
=> 1
>> lambda { |x,y,z| x + y + z }.arity
=> 3
>> lambda { 1 }.arity
=> 0
-------------------------------------------------------------------------------
As you can see, because +Proc+ objects are just objects themselves, we can do
some reflective inquiry to find out how many arguments they're expecting to
process. Although not strictly related to our task, it's worth mentioning that
you can accomplish the same thing with methods, as well.
-------------------------------------------------------------------------------
>> Comparable.instance_method(:between?).arity
=> 2
>> Fixnum.instance_method(:to_f).arity
=> 0
-------------------------------------------------------------------------------
Although our use of an +arity+ check was confined to a relatively simple task
here, the technique is general. Any time you want to conditionally handle
something based on how many block arguments are present, you can use this
general approach.
That having been said, even if you never use this trick for anything else,
knowing how to selectively +instance_eval+ a block is important. As you'll see
through the rest of this section, a key part of developing a pleasant
domain-specific interface is maintaining flexibility. If you limit yourself to
an all-or-nothing choice between your sexy shortcuts and the bland low level
API, frustration is inevitable. Of course, because Ruby is so dynamic, you should
never be forced to make this decision.
We'll now move on to another key component of flexible interface design, the use
of +method_missing+ and +send+ to dynamically route messages within your
objects.
==== Handling Messages with method_missing() and send() ====
Continuing on a theme, we can look at a bit more Prawn code to see how to make
things a bit more dynamic. We'll be talking about elementary drawing operations
here, but you can substitute your own problem mentally. As in other examples,
the actual domain does not matter.
In Prawn, there are two ordinary ways to generate some shapes and then draw them
onto the page. The first is the most simple, by just drawing the paths, and
then calling one of +stroke+, +fill+ or +fill_and_stroke+:
-------------------------------------------------------------------------------
Prawn::Document.generate("shapes.pdf") do
fill_color "ff0000"
# Fills a red circle
circle_at [100,100], :radius => 25
fill
# Strokes a transparent circle with a black border and a line extending
# from its center point
circle_at [300,300] :radius => 50
line [300,300], [350, 300]
stroke
# Fills and strokes a red hexagon with a black border
polygon [100, 250], [200, 300], [300, 250],
[300, 150], [200, 100], [100, 150]
fill_and_stroke
end
-------------------------------------------------------------------------------
This isn't too bad, but for some needs, a block form is better. This makes it
clearer what paint operation is being used, and may be a bit easier to extend:
-------------------------------------------------------------------------------
Prawn::Document.generate("shapes.pdf") do
fill_color "ff0000"
# Fills a red circle
fill { circle_at [100,100], :radius => 25 }
# Strokes a transparent circle with a black border and a line extending
# from its center point
stroke do
circle_at [300,300] :radius => 50
line [300,300], [350, 300]
end
fill_and_stroke do
# Fills and strokes a red hexagon with a black border
polygon [100, 250], [200, 300], [300, 250],
[300, 150], [200, 100], [100, 150]
end
end
-------------------------------------------------------------------------------
This may be a bit more readable, especially the middle one where multiple paths
need to be stroked. However, it still feels like more work than we'd really
want. Wouldn't things be nicer this way?
-------------------------------------------------------------------------------
Prawn::Document.generate("shapes.pdf") do
fill_color "ff0000"
fill_circle_at [100,100], :radius => 25
stroke_circle_at [300,300] :radius => 50
stroke_line [300,300], [350, 300]
fill_and_stroke_polygon [100, 250], [200, 300], [300, 250],
[300, 150], [200, 100], [100, 150]
end
-------------------------------------------------------------------------------
This has a nice, declarative feel to it. Obviously though, we don't want to
define 4 methods for every graphics drawing operation. This is especially true
when you think of the nature of what each of these would look like. Let's take
stroke for example:
-------------------------------------------------------------------------------
def stroke_some_method(*args)
some_method(*args)
stroke
end
-------------------------------------------------------------------------------
Repeat that ad-nausem for every single drawing method, and keep up this pattern
every time a new one is added? No Way! Maybe this sort of repetition would be
tolerated over in Java-land, but in Ruby, we can do better. The answer lies in
dynamically interpreting method calls.
When you attempt to call a method that doesn't exist in Ruby, you see an
exception raised by default. However, Ruby provides a way to hook into this
process and intercept the call before an error can be raised. This is done
through a method called +method_missing+.
To give a very brief introduction to how it works, let's take a quick spin in
irb:
-------------------------------------------------------------------------------
>> def method_missing(name, *args, &block)
>> puts "You tried to call #{name} with #{args.inspect}"
>> puts "Epic Fail!"
>> end
=> nil
>> 1.fafsafs
You tried to call fafsafs with []
Epic Fail!
=> nil
>> "kitten".foo("bar", "baz")
You tried to call foo with ["bar", "baz"]
Epic Fail!
-------------------------------------------------------------------------------
By including a +method_missing+ hook at the top level, we see all unknown
messages get routed through our new method and print out our message. As you
can see, the name of the message as well as the arguments are captured. Of
course, this sort of global change is typically a very bad idea, and serves only
as an introduction. But if you're feeling ambitious, take a moment to think
about how this technique could be used to solve the problem we're working on
here, before reading on.
Did you have any luck? If you did attempt this exercise, what you would find is
that +method_missing+ isn't very useful on its own. Typically, it is used to do
part of a job and then route the work off to another function. The way we do
this is by making use of +Kernel#send+, which allows us to call a method by just
passing a symbol or string, followed by any arguments:
-------------------------------------------------------------------------------
>> "foo".send(:reverse)
=> "oof"
>> [1,2,3].send("join", "|")
=> "1|2|3"
-------------------------------------------------------------------------------
Does this clue make things a bit clearer? For those that didn't try to build
this on their own, or if you attempted it and came up short, here's how to make
it all work:
-------------------------------------------------------------------------------
# Provides the following shortcuts:
#
# stroke_some_method(*args) #=> some_method(*args); stroke
# fill_some_method(*args) #=> some_method(*args); fill
# fill_and_stroke_some_method(*args) #=> some_method(*args); fill_and_stroke
#
def method_missing(id,*args,&block)
case(id.to_s)
when /^fill_and_stroke_(.*)/
send($1,*args,&block); fill_and_stroke
when /^stroke_(.*)/
send($1,*args,&block); stroke
when /^fill_(.*)/
send($1,*args,&block); fill
else
super
end
end
-------------------------------------------------------------------------------
As the documentation describes, this hook simply extracts the paint command out from the
method call, and then sends the remainder as the function to execute. All
arguments (including an optional block) are forwarded on to the real method.
Then, when it returns, the specified paint method is called.
It's important to note that when the patterns do not match, +super+ is called.
This allows objects up the chain to do their own +method_missing+ handling,
including the default which raises a +NoMethodError+. This prevents something
like +pdf.the_shiny_kitty+ from failing silently, as well as the more subtle
+pdf.fill_cirlce+.
Although this is just a single example, it should spark your imagination for all
the possibilities. But it also hints in at the sort of looseness that comes
with this approach. Prawn will happily accept
+pdf.fill_and_stroke_start_new_page+ or even +pdf.stroke_stroke_stroke_line+
without complaining. Any time you use +method_missing+, these are the tradeoffs
you must be willing to accept. Of course, by making your hooks more robust, you
can get a bit more control, but that starts to defeat the purpose if you take it
too far.
The best approach is to use +method_missing+ responsibly and with moderation.
Be sure to avoid accidental silent failures by calling super for any case you do
not handle, and don't bother using it if you want things to be ironclad. In
cases where there are a relatively small set of methods you want to generate
dynamically, a solution using +define_method+ might be preferred. That having been
said, when used as a shortcut alternative to a less pleasant interface,
+method_missing+ can be quite helpful, especially in cases where the messages
you'll need to accept are truly dynamic.
The techniques described so far combined with some of the methods shown in the
previous chapter will get you far in building a domain specific interface.
We're about to move on to other dynamic Ruby topics, but before we do that,
we'll cover one more cheap trick that leads to clean and flexible interfaces.
==== Dual purpose accessors ====
One thing you will notice when working with code that has an +instance_eval+
based interface is that using ordinary setters can be ugly. Because you need to
disambiguate between local variables and method calls, stuff like this can
really cramp your style:
-------------------------------------------------------------------------------
Prawn::Document.generate("accessors.txt") do
self.font_size = 10
text "The font size is now #{font_size}"
end
-------------------------------------------------------------------------------
It's possible to make this look much nicer, as you can see:
-------------------------------------------------------------------------------
Prawn::Document.generate("accessors.txt") do
font_size 10
text "The font size is now #{font_size}"
end
-------------------------------------------------------------------------------
The concept here isn't a new one, we talked about it last chapter. We can use
Ruby's default argument syntax to determine if we're supposed to be getting or
setting the attribute:
-------------------------------------------------------------------------------
class Prawn::Document
def font_size(size = nil)
return @font_size unless size
@font_size = size
end
alias_method :font_size=, :font_size
end
-------------------------------------------------------------------------------
As I said before, this is a relatively cheap trick with not much special to it.
But the first time you forget to do it and find yourself typing +self.foo = bar+
in what is supposed to be a domain specific interface, you'll be sure to remember
this technique.
One thing to note is that you shouldn't break the normal behavior of setters
from the outside. We use +alias_method+ here instead of +attr_writer+ to
ensure down the line that there won't be any difference between the following
two lines of code:
-------------------------------------------------------------------------------
pdf.font_size = 16
pdf.font_size(16)
-------------------------------------------------------------------------------
Though not essential, this is a nice way to avoid potential headaches at
a very low cost, so it's a good habit to get into when using this technique.
When we combine all the tactics we've gone over so far, we've got all the
essential components for building flexible domain specific interfaces. Before
we move on to the next topic, let's review the main points to remember about
flexible interface design:
* As mentioned in the previous chapter, using +instance_eval+ is a good base
for writing a domain-specific interface, but has some limitations
* You can use a +Proc#arity+ check to provide the user a choice between
+instance_eval+ and yielding an object.
* If you want to provide shortcuts for certain sequences of method calls, or
dynamic generation of methods, you can use +method_missing+ along with
+send()+
* When using +method_missing+, be sure to use +super()+ to pass unhandled calls
up the chain so they can be handled properly by other code, or eventually
raise a NoMethodError.
* Normal attribute writers don't work well in +instance_eval+ based interfaces.
Offer a dual purpose reader/writer method, and then alias a writer to it, and
both external and internal calls will be clear.
With these tips in mind, we'll move on to another topic. It's time to shift
gears from per-class dynamic behavior to individual objects.
=== Implementing Per-Object Behavior ===
An interesting aspect of Ruby is that not only can objects have per-class
method definitions, but they can also have per-object behaviors. What this
means is that each and every object carries around its own unique identity, and
that the class definition is simply the blueprint for the beginning of an
object's lifecycle.
Let's start with a simple irb session to clearly illustrate this concept.
-------------------------------------------------------------------------------
>> a = [1,2,3]
=> [1, 2, 3]
>> def a.secret
>> "Only this object knows the secrets of the world"
>> end
=> nil
>> a.secret
=> "Only this object knows the secrets of the world"
>> [1,2,3,4,5].secret
NoMethodError: undefined method `secret' for [1, 2, 3, 4, 5]:Array
from (irb):17
from :0
>> [1,2,3].secret
NoMethodError: undefined method `secret' for [1, 2, 3]:Array
from (irb):18
from :0
-------------------------------------------------------------------------------
Here, using a familiar method definition syntax, we add a special method called
+secret+ to the array we've assigned to +a+. The remaining examples show that
only +a+ gets this new method definition. If the last one surprised you a bit,
remember that most objects in Ruby are not immediate values, so two arrays set
to +[1,2,3]+ are not the same object, even if they contain the same data. More
concisely:
-------------------------------------------------------------------------------
>> [1,2,3].object_id
=> 122210
>> a.object_id
=> 159300
-------------------------------------------------------------------------------
So when we talk about each object having its own behavior, we mean exactly that
here. You may be wondering at this point what uses there might be for such a
feature. An interesting abstract example might be to note that class methods
are actually just per-object behavior on an instance of the class +Class+, but I
think it'd be more productive to give you a concrete example to sink your
teeth into.
We're going to take a look at how to build a simple stubbing system for use in
testing. In the testing chapter, I recommended 'flexmock' for this purpose, and I
still do, but going through the process of building a tiny stubbing framework
will show a good use case for our current topic.
Our goal is to create a system that will generate canned responses to certain
method calls, without modifying their original classes. This is an important
feature, because we don't want our stubbed method calls to have a global effect
during testing. Our target interface will be something like this:
-------------------------------------------------------------------------------
user = User.new
Stubber.stubs(:logged_in?, :for => user, :returns => true)
user.logged_in? #=> true
-------------------------------------------------------------------------------
We'll start with a very crude approach in irb to get a feel for the problem:
-------------------------------------------------------------------------------
>> class User; end
=> nil
>> user = User.new
=> #<User:0x636b4>
>> def user.logged_in?
>> true
>> end
=> nil
>> user.logged_in?
=> true
>> another_user = User.new
=> #<User:0x598d0>
>> another_user.logged_in?
NoMethodError: undefined method `logged_in?' for #<User:0x598d0>
from (irb):40
from :0
-------------------------------------------------------------------------------
This is essentially the behavior we want to capture, per-object customization
that doesn't affect the class definition generally. Of course, to do this
dynamically is going to take a little more work than the manual version. Our
first hurdle is that the technique used in the earlier `BlankSlate` example
doesn't work out of the box here:
-------------------------------------------------------------------------------
>> user.define_method(:logged_in?) { true }
NoMethodError: undefined method `define_method' for #<User:0x40ed90>
from (irb):17
from /Users/sandal/lib/ruby19_1/bin/irb:12:in `<main>'
-------------------------------------------------------------------------------
As it turns out, each object hides its individual space for method definitions
(called a singleton class) from plainview. However, we can reveal it by using a
special syntax.
-------------------------------------------------------------------------------
>> singleton = class << user; self; end
=> #<Class:#<User:0x40ed90>>
-------------------------------------------------------------------------------
My earlier clues about class methods being per-object behavior on an
instances of +Class+ should come to mind here. We often use this syntax when we
need to define a few class methods:
-------------------------------------------------------------------------------
class A
class << self
def foo
"hi"
end
def bar
"bar"
end
end
end
-------------------------------------------------------------------------------
The possible new thing here is that +self+ can be replaced by any old object.
So when see +class << user; self; end+, what's really going on is we're just
asking our object to give us back its singleton class. Once we have that in
hand, we can define methods on it. Well, almost:
-------------------------------------------------------------------------------
>> singleton.define_method(:logged_in?) { true }
NoMethodError: private method `define_method' called for #<Class:#<User:0x40ed90>>
from (irb):19
from /Users/sandal/lib/ruby19_1/bin/irb:12:in `<main>'
-------------------------------------------------------------------------------
Because what we're doing is not exactly business as usual, Ruby is throwing some
red flags up reminding us to make sure we know what we're doing. But since we
do, we can use +send+ to bypass the access controls: