Upcoming SlideShare
×

# Using Grok to Walk Like a Duck - Brandon Craig Rhodes

3,439

Published on

Using Grok to Walk Like a Duck - Brandon Craig Rhodes

Published in: Technology, Education
1 Like
Statistics
Notes
• Full Name
Comment goes here.

Are you sure you want to Yes No
• Be the first to comment

Views
Total Views
3,439
On Slideshare
0
From Embeds
0
Number of Embeds
1
Actions
Shares
0
23
0
Likes
1
Embeds 0
No embeds

No notes for slide

### Using Grok to Walk Like a Duck - Brandon Craig Rhodes

1. 1. “ Using Grok to Walk Like a Duck” The Zope 3 Component Architecture Brandon Craig Rhodes Plone Conference 2008, Washington, DC
2. 2. How many methods does a Plone ATFolder have?
5. 5. Both Zope 2 and Plone solve problems by piling more and more methods on an object
6. 6. Zope 3 does something different Zope 3 uses Adapters
7. 7. What are adapters? Let's talk programming.
8. 8. Many programming languages use static typing
9. 9. float half(int n) { return n / 2.0; }
10. 10. float half( int n) { return n / 2.0; }
11. 11. Python typing is dynamic
12. 12. def half(n): return n / 2.0
13. 13. You don't worry about whether an object is of the right type
14. 14. You simply try using it
15. 15. “ Duck Typing” (Alex Martelli)
16. 16. “ Duck Typing” Walks like a duck? Quacks like a duck? It's a duck!
17. 17. def half(n): return n / 2.0
18. 18. def half(n): return n / 2.0 (Is n willing to be divided by two? Then it's number-ish enough for us!)
19. 19. Now, imagine...
20. 20. Imagine a wonderful duck-processing library to which you want to pass an object
21. 21. But... The object you want to pass isn't a duck?
22. 22. What if it doesn't already quack?
23. 23. What if it bears not the least resemblance to a duck!?
24. 24. Example!
25. 25. You have a “Message” object from the Python “email” module
26. 26. >>> from email import message_from_file >>> e = message_from_file(open('msg.txt')) >>> print e <email.message.Message instance at ...> >>> e.is_multipart() True >>> for part in e.get_payload(): ... print part.get_content_type() text/plain text/html
27. 27. multipart/mixed text/plain multipart/alternative text/plain text/html image/jpeg Messages can be recursive
28. 28. Imagine that we are writing a Plone email browsing system
29. 29. And we want to show the parts in a TreeWidget
30. 30. multipart/mixed
31. 31. multipart/mixed text/plain multipart/alternative image/jpeg
32. 32. multipart/mixed text/plain multipart/alternative text/plain text/html image/jpeg
33. 33. The Tree widget operates on any object with: method name() - returns name under which this tree node should be displayed method children() - returns list of child nodes in the tree method __len__() - returns number of child nodes beneath this one
34. 34. How can we add these behaviors to our Message?
35. 35. (How can we make an object which is not a duck behave like a duck?)
36. 36. 1. Subclassing
37. 37. Create a “TreeMessage” class that inherits from the “Message” class...
38. 38. class TreeMessage ( Message ): def name (self): return self.get_content_type() def children (self): if not self.is_multipart(): return [] return [ TreeMessage (part) for part in self.get_payload() ] def __len__ (self): return len(self.children())
39. 39. What will the test suite look like?
40. 40. Remember: “ Untested code is broken code” — Philipp von Weitershausen, Martin Aspeli
41. 41. Your test suite must instantiate a “TreeMessage” and verify its tree-like behavior...
42. 42. txt = “”” From: persephone@gmail.com To: brandon@rhodesmill.org Subject: what an article! Did you read Arts & Letters Daily today? “”” m = message_from_string(txt, TreeMessage ) assert m. name () == 'text/plain' assert m. children () == [] assert m. __len__ () == 0
43. 43. We were lucky!
44. 44. Our test can cheaply instantiate Messages.
45. 45. txt = “”” From: persephone@gmail.com To: brandon@rhodesmill.org Subject: what an article! Did you read Arts & Letters Daily today? “”” m = message_from_string(txt, TreeMessage ) assert m. name () == 'text/plain' assert m. children () == [] assert m. __len__ () == 0
46. 46. What if we were subclassing an LDAP connector?! We'd need an LDAP server just to run unit tests!
47. 47. We were lucky (#2)!
48. 48. The “message_from_string()” method let us specify an alternate factory!
49. 49. txt = “”” From: persephone@gmail.com To: brandon@rhodesmill.org Subject: what an article! Did you read Arts & Letters Daily today? “”” m = message_from_string(txt, TreeMessage ) assert m. name () == 'text/plain' assert m. children () == [] assert m. __len__ () == 0
50. 50. Final note: we have just broken the “Message” class's behavior!
51. 51. Python library manual 7.10.1 defines “Message”: __len__(): Return the total number of headers, including duplicates.
52. 52. >>> t = “”” From: persephone@gmail.com To: brandon@rhodesmill.org Subject: what an article! Did you read Arts & Letters Daily today? “”” >>> m = message_from_file(t, Message) >>> print len(m) 3 >>> m = message_from_file(t, TreeMessage) >>> print len(m) 0
53. 53. So how does subclassing score?
54. 54. No harm to base class
55. 55. No harm to base class Cannot test in isolation
56. 56. No harm to base class Need control of factory Cannot test in isolation
57. 57. No harm to base class Breaks if names collide Need control of factory Cannot test in isolation
58. 58. No harm to base class Breaks if names collide Need control of factory Cannot test in isolation Subclassing: D
59. 59. 2. Using a mixin
60. 60. Create a “TreeMessage” class that inherits from both “Message” and a “Mixin”...
61. 61. class Mixin (object): def name (self): return self.get_content_type() def children (self): if not self.is_multipart(): return [] return [ self.__class__(part) for part in self.get_payload() ] def __len__ (self): return len(self.children()) class TreeMessage ( Message , Mixin ): pass
62. 62. Your test suite can then inherit from a fake, mocked-up “message”...
63. 63. class FakeMessage ( Mixin ): def get_content_type (self): return 'text/plain' def is_multipart (self): return False def get_payload (self): return '' m = FakeMessage () assert m. name () == 'text/plain' assert m. children () == [] assert m. __len__ () == 0
64. 64. How does a mixin rate?
65. 65. No harm to base class
66. 66. No harm to base class Can test mixin by itself
67. 67. No harm to base class Need control of factory Can test mixin by itself
68. 68. No harm to base class Breaks if names collide Need control of factory Can test mixin by itself
69. 69. No harm to base class Breaks if names collide Need control of factory Can test mixin by itself Mixin: C
70. 70. 3. Monkey patching
71. 71. To “monkey patch” a class, you add or change its methods dynamically...
72. 72. def name (self): return self.get_content_type() def children (self): if not self.is_multipart(): return [] return [ Message (part) for part in self.get_payload() ] def __len__ (self): return len(self.children()) Message . name = name Message . children = children Message . __len__ = __len__
73. 73. Is this desirable?
74. 74. Don't care about factory
75. 75. Changes class itself Don't care about factory
76. 76. Changes class itself Broken by collisions Don't care about factory
77. 77. Changes class itself Broken by collisions Don't care about factory Patches fight each other
78. 78. Changes class itself Broken by collisions Don't care about factory Ruby people do this Patches fight each other
79. 79. Changes class itself Broken by collisions Don't care about factory Ruby people do this Monkey patching: F Patches fight each other
81. 81. Touted in the Gang of Four book (1994)
82. 82. Idea: provide “Tree” functions through an entirely separate object Message MessageTreeAdapter get_content_type () name () is_multipart () call children () get_payload () __len__ ()
83. 83. class MessageTreeAdapter (object): def __init__ (self, message): self. m = message def name (self): return self. m .get_content_type() def children (self): if not self. m .is_multipart(): return [] return [ MessageTreeAdapter (part) for part in self. m .get_payload() ] def __len__ (self): return len(self.children())
84. 84. How does wrapping look in your code?
85. 85. Message object Adapted object IMAP library (or whatever) returns a Message “msg” TreeWidget tw = TreeWidget(MessageTreeAdapter(msg))
86. 86. Test suite can try adapting a mock-up object
87. 87. class FakeMessage (object): def get_content_type (self): return 'text/plain' def is_multipart (self): return True def get_payload (self): return [] m = MessageTreeAdapter ( FakeMessage ()) assert m. name () == 'text/plain' assert m. children () == [] assert m. __len__ () == 0
88. 88. How does the Adapter design pattern stack up?
89. 89. No harm to base class
90. 90. No harm to base class Can test with mock-up
91. 91. No harm to base class Don't need factories Can test with mock-up
92. 92. No harm to base class Don't need factories Can test with mock-up No collision worries
93. 93. No harm to base class Don't need factories Can test with mock-up Wrapping is annoying No collision worries
94. 94. No harm to base class Don't need factories Can test with mock-up Wrapping is annoying Adapter: B No collision worries
95. 95. Q: Why call wrapping “ annoying”?
96. 96. The example makes it look so easy!
97. 97. Message object Adapted object IMAP library (or whatever) returns a Message “msg” TreeWidget tw = TreeWidget(TreeMessageAdapter(msg))
98. 98. A: The example looks easy because it only does adaptation once !
99. 99. But in a real application, it happens all through your code...
100. 100. objects objects 3 rd party Producers Adapters 3 rd party Consumers Your application C(msg) DB GUI A(famtree) IMAP email msg Web Widget B(msg) Genealogy A B C C(msg)
101. 101. This makes you repeat yourself. This also locks you in to using that particular adapter, since you use it by name in your code.
102. 102. How can you avoid repeating yourself, and scattering information about adapters and consumers everywhere?
103. 103. Message object Adapted object IMAP library (or whatever) returns a Message “msg” TreeWidget tw = TreeWidget(TreeMessageAdapter(msg))
105. 105. The key is seeing that this code conflates two issues! tw = TreeWidget(TreeMessageAdapter(msg))
106. 106. Why does this line work? tw = TreeWidget(TreeMessageAdapter(msg))
107. 107. It works because a TreeWidget needs what our adapter provides . tw = TreeWidget(TreeMessageAdapter(msg))
108. 108. But if we call the adapter then the need = want is hidden inside of our head! tw = TreeWidget(TreeMessageAdapter(msg))
109. 109. We need to define what the TreeWidget needs that our adapter provides!
110. 110. An interface is how we specify a set of behaviors
111. 111. (1988) (1995) An interface is how we specify a set of behaviors
112. 113. For the moment, forget Zope-the-web-framework
113. 114. Instead, look at Zope the Component Framework: zope.interface zope.component
114. 115. With three simple steps, Zope will put adapters around classes for you — and rid your code of manual adaptation!
115. 116. 1. Define an interface 2. Register our adapter 3. Request adaptation
116. 117. from zope.interface import Interface class ITree (Interface): def name (): “”” Return this tree node's name. ””” def children (): “”” Return this node's children. ””” def __len__ (): “”” Return how many children. ””” Define
118. 119. class TreeWidget (...): def __init__(self, arg): tree = ITree (arg) ... Request
119. 120. class TreeWidget (...): def __init__(self, arg): tree = ITree (arg) ... Zope will: 1. Recognize need 2. Find the registered adapter 3. Wrap and return the argument Request
120. 121. class TreeWidget (...): def __init__(self, arg): tree = ITree (arg) ... (Look! Zope is Pythonic!) i = int(32.1) l = list('abc') f = float(1024) Request
121. 122. And that's it!
122. 123. And that's it! Define an interface Register our adapter Request adaptation
123. 124. No harm to base class Don't need factories Can test with mock-up Adapters now dynamic! Registered adapter: A No collision worries
124. 125. objects objects 3 rd party Producers Adapters 3 rd party Consumers Your application C(msg) DB GUI A(famtree) IMAP email msg Web Widget B(msg) Genealogy A B C C(msg)
125. 126. What adapters provide What consumers need C(msg) DB GUI A(famtree) IMAP email msg Web Widget B(msg) Genealogy A B C C(msg)
126. 127. DB GUI IMAP email Web Widget Genealogy A B C
127. 128. The finale Adapting for the Web
128. 129. dum ... dum ... dum ... DAH DUM!
129. 131. Grok
130. 132. Web framework built atop Zope 3 component architecture
131. 133. Grok makes Zope 3 simple to use (and to present!)
132. 134. Imagine a Person class
133. 135. The Person class was written by someone else
134. 136. The Person class is full of business logic, and stores instances in a database
135. 137. We want to browse Person objects on the Web
136. 138. What might the Web need the object to do?
137. 139. 1. What's at a URL 3. What is its URL 2. HTML document <HTML> <HEAD> <TITLE>Person JOE </TITLE> </HEAD> <BODY> This page presents the basic data we have regarding Joe. ... Person http://host/person_app/Joe http://host/person_app/Joe
138. 140. 1.
139. 141. What's at this URL?
140. 142. What's at this URL? # how Zope processes this URL: r = root j = ITraverser(r).traverse('person_app') k = ITraverser(j).traverse('Joe') return k http://host/person_app/Joe
141. 143. # what we write: class PersonTraverser (grok. Traverser ): grok.context(PersonApp) def traverse (self, name ): if person_exists( name ): return get_person( name ) return None What's at this URL? http://host/person_app/Joe
142. 144. 2.
143. 145. How does a Person render?
144. 146. app.py class PersonIndex (grok. View ): grok.context( Person ) grok.name(' index ') app_templates/personindex.pt < html >< head >< title >All about < tal tal:replace =”context/name”/> </ title ></ head >... How does a Person render?
145. 147. 3.
146. 148. What is a person's URL?
147. 149. class PersonURL (grok. MultiAdapter ): grok.adapts(Person, IHTTPRequest) grok.implements(IAbsoluteURL) def __init__ (self, person, req): self.person, self.req = person, req def __call__ (self): base = grok.url(grok.getSite()) return base + ' / ' + self.person.name What is a person's URL?
148. 150. 6 + 3 + 8 = 17 lines
149. 151. 6 + 3 + 8 = 17 lines And the object has not been harmed!
150. 152. 1. What's at a URL 3. What is its URL 2. HTML Document <HTML> <HEAD> <TITLE>Person JOE </TITLE> </HEAD> <BODY> This page presents the basic data we have regarding Joe. ... Person PersonTraverser PersonURL PersonIndex http://host/person_app/Joe http://host/person_app/Joe
151. 153. Other Zope adapter uses
152. 154. Other Zope adapter uses Indexing — Index, Query, Search, ... Data schemas — Schema, Vocabulary, DublinCore ... Form generation — AddForm, EditForm, ... Security — SecurityPolicy, Proxy, Checker, ... Authentication — Login, Logout, Allow, Require, ... Copy and paste — ObjectMover, ObjectCopier, ... I18n — TranslationDomain, Translator, ... Appearance — Skins, macros, viewlets, ... Much, much more!
153. 155. Other Zope adapter uses And... “Vice”, the Plone RSS/Atom feed engine that Paul Bugni presented on yesterday!
154. 156. How does Vice give the AT content types RSS superpowers?
155. 157. The same 3 steps! Define an interface Register adapters Request adaptation