Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 219 lines (163 sloc) 7.874 kB
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
1 # MOTIVATION: As rails apps are growing, people are noticing the drawbacks
2 # of the ActiveRecord pattern. Several apps I have seen, and several
3 # developers I have spoken to are looking towards other patterns for object
4 # persistence. The major drawback with ActiveRecord is that the notion
5 # of the domain object is conflated with what it means to store/retrieve
6 # it in any given format (like sql, json, key/value, etc).
7 #
8 # This is an attempt to codify the Repository pattern in a way that would
9 # feel comfortable to beginner and seasoned Ruby developers alike.
10 #
56e91ac @bokmann initial braindump
bokmann authored
11 # In the spirit of "make it look the way you want it to work, then make it
12 # work like that", here is a vision of the Repository pattern ala Ruby. I
13 # don't want to fault ActiveRecord for being an implementation of the
14 # ActiveRecord pattern; At RailsConf 2006, in his keynote, Martin Fowler
15 # said that ActiveRecord was "the most faithful implementation of the
16 # Activerecord pattern [he] had ever seen", and he was the one that
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
17 # documented that pattern in the first place. So lets respect AR for what
56e91ac @bokmann initial braindump
bokmann authored
18 # it is, and look at the repository pattern, done with Rails Flair.
19
20
21
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
22 # Person is a plain old Ruby object. It knows nothing about its persistence,
23 # but as a domain model, it knows something about its own attributes,
24 # relationships to other domain objects, as well as understands what it
25 # means to be 'valid'. Of course, it also has behavior.
26
56e91ac @bokmann initial braindump
bokmann authored
27 class Person
28 include ActiveModel::Relationships
51a0184 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
29 include ActiveModel::Validations
30
56e91ac @bokmann initial braindump
bokmann authored
31 has_many :hobbies
32 belongs_to :fraternity
33
34 attr_accessor :name
35 attr_accessor :password
36 attr_accessor :sex
37 attr_accessor :born_on
51a0184 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
38
39 validates_presence_of :name, :password
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
40
41 def favorite_drink
42 if born_on < 7.years.ago
43 "apple juice"
44 elsif born_on < 18.years.ago
45 "coke or pepsi"
46 elsif < 21.years.ago
47 "mountain dew"
48 else
49 "beer"
50 end
51 end
56e91ac @bokmann initial braindump
bokmann authored
52 end
53
54 # conventions like _on and _at are still used.
55
56
57 # The person has no clue it is 'Persistent Capable'. All of that is handled
58 # in the Repository. Notice we could call the Repo anything we want.
59
60 class PersonStore < ActiveRepository::Base
61 repository_for :person
62
63 # other things that control the persistence can go here
64 table :users
65 encrypts_attribute :password
66 map_attribute_to_column :sex, :gender
67 end
68
51a0184 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
69 # I'm using inheritance here to enforce an opinion. Of course this could
70 # be a mixin, but my current thoughts are that this Repository should only
71 # ever be a repository - after all, we are trying to get away from
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
72 # ActiveRecord's notion of "I'm the model and my own data store!".
51a0184 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
73 # Inheritance would be an appropriate way to signal this *is a* repository.
74 # My fear is as a mixin, someone would think they are being clever by mixing
75 # the repository directly into the domain model, essentially recreating
76 # ActiveRecord and making this effort all for nothing.
56e91ac @bokmann initial braindump
bokmann authored
77
78 # I would really like to have the ability to 'new' model objects as normal:
79
80 p = Person.new
81
82 # but it might be necessary to create them through the store, like:
83
84 p = PersonStore.create #, or
85 p = PersonStore.build
86
87 # saving is no longer done on the object, but through the repository
88
89 PersonStore.save(p)
90
51a0184 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
91 # the save would be smart enough to call is_valid? if Validations were present.
92
56e91ac @bokmann initial braindump
bokmann authored
93 # all the usual finder suspects are on the repository
94 p = PersonStore.find(5)
95 p = PersonStore.first
96 p = PersonStore.all
97 p = PersonStore.find_by_name("Chris")
98
99 # we could also create things like scopes, etc.
100
101
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
102 # Since Person is nothing special, you could easily swap in a different
103 # persistance Repository:
56e91ac @bokmann initial braindump
bokmann authored
104
412e20d @steveklabnik Class -> class
authored
105 class PersonStore < RedisRepository::Base
56e91ac @bokmann initial braindump
bokmann authored
106 ...
107 end
108
109 # or even:
110
412e20d @steveklabnik Class -> class
authored
111 class PersonStore < RestfulResource::Repository
56e91ac @bokmann initial braindump
bokmann authored
112 repository_for :person
113 base_uri "http://coolstuff.livingsocial.com"
114 end
115
116
117 # Swapping repositories might give you radically different methods (only an
118 # ActiveRepository would give you a find_by_sql method, for instance), but
119 # thats ok. The "Persistant Capable" classes don't change with a change in
120 # the persistence mechanism. The "Persistent Aware" classes, like the
121 # controllers, would.
122
123 # And it might even be possible to have multiple repositories in the same
124 # app...
125
412e20d @steveklabnik Class -> class
authored
126 class PersonStore < ActiveRepository::Base
56e91ac @bokmann initial braindump
bokmann authored
127 #...
128 end
129
130 # and
131
412e20d @steveklabnik Class -> class
authored
132 class RemotePersonStore < RestfulResource::Repository
56e91ac @bokmann initial braindump
bokmann authored
133 #...
134 end
135
136 # and then you could do stuff like:
137
138 p = RemotePersonStore.find(5)
139 PersonStore.save(p)
140
141 # and essentially use two repositories as an ETL engine.
142
143
144 # One nice thing we get from ActiveRecord would have to change slightly -
145 # the migrations.
146 # Actually, the migrations could work pretty much as they do now, but devs
147 # would have to make the corresponding attr_accessor declarations in their
148 # models.
149 #
150 # if an attr_accessor was declared that didn't have a corresponding column in
151 # the db, then it could warn on application init. That warning for that field
152 # could be silenced in the repository with something like:
153
154 not_persisted :current_location
155
156 # and in reverse, if a migration added a column that couldn't map to an
157 # attr_accessor, it could warn unless the repo had a line like:
158
159 ignore_column :eye_color
160
51a0184 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
161 # The generator could stick the attr_accessors in the class
162 # automatically if we wanted it to. I wouldn't do anything 'magical' like
163 # have the persistence engine inject it with meta... that would make the
164 # attributes hard to discover, and could make multiple repositories in an
165 # app step on each other in nondeterministic ways. By having attr_accessors,
166 # the model becomes the 'system of record' for what it means to be that
167 # kind of domain object. I like that.
168
56e91ac @bokmann initial braindump
bokmann authored
169 # (of course, nosql dbs may have their own requirements met with their own
170 # dsls).
171
172 # You could even have the store do something like:
173
174 synthetic_field :age, { Date.today - born_on }
175
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
176 # while you could do exactly the same thing by adding 'age' method to the
177 # model, having it in he Repository could be useful for an ETL task, for
178 # defining the transform step. Imagine one database that has a born_on
179 # field, and another one that has an age field, and you are transforming
180 # data to go into the one with age... in the store with the born_on,
181 # set the store to have a synthetic field :age. In the other store, set
182 # it to ignore the born_on date. Then in the model you define attr_reader
183 # for :age. Both stores see the domain model as exactly what it needs in
184 # order to make each database happy, and the domain model code stays clean.
185 #
186 # if you needed to map an attribute to a different column:
56e91ac @bokmann initial braindump
bokmann authored
187
188 map_attribute_to_column :sex, :gender
189
190 # One potentially awesome tweak to the dsl is how this would
191 # handle polymorphic tables:
192
193 class PersonStore < ActiveRepository::Base
194 repository_for :person
195 repository_for :client
196 repository_for :administrator
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
197 polymorphic_model_column :type # would automatically store the class
198 # type, just like AR polymorphism
56e91ac @bokmann initial braindump
bokmann authored
199 end
200
201
202
6263086 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
203 # Given this pattern, I think relationship declarations go in the models,
56e91ac @bokmann initial braindump
bokmann authored
204 # since there they can add the appropriate methods to return collections,
51a0184 @bokmann adding revisions and comments based on reviews from several colleagues
bokmann authored
205 # etc, and since the repo knows what models it is supposed to manage, it can
206 # get access to the same knowledge to do whatever it needs to do. If they
207 # were declared in the repo, it would be inappropriate for the model to
56e91ac @bokmann initial braindump
bokmann authored
208 # introspect there, otherwise the model would become 'persistence aware'.
209 # They 'feel' a little attr_accessor-like things to me.
210
211 # Finally, while the model as code looks completely unaware of its storage,
212 # underneath the covers at runtime the repository could 'enhance' it with
213 # things like dirty flags for fields, is_dirty? & is_new? methods, etc.
214 # In fact, for years I used an object-oriented database in Java named
215 # Versant, and it had a 'bytecode enhancement' step that did exactly this
216 # during the compile - it modified the classes bytecode with dirty flags
217 # and other persistence helpers.
218
Something went wrong with that request. Please try again.