I have a new application that is mostly generic. Customers have Users. User's add Providers. The Providers are notified they have been requested to provide information. Providers either accept and submit a form of fields, or decline.
Although it's mostly generic, there is some specialization per Customer. For the most part, I was able to handle the specialization by parameterizing Customers with static data (e.g. a list of questions, field types, min/max number of providers per user, etc.); however, I also need some simple behavioral specialization.
I've always used structs for my database models, so to accomplish the latter, I had a struct that subtyped the Customer struct and added a number of functions. A factory function would create and populate this struct with functions specific to the customer. For example (we need to fix the code highlighter!):
(struct q customer (activate-user
get-user-data
display-link?
application-css
application-name
validate-provider-meta-data
validate-provider-customer-data
...)
#:transparent)
(define/contract (build-q customer-obj)
(-> customer? q?)
(match (customer-username customer-obj)
[ "tla" (build-q-tla customer-obj) ]
[ _ (error "build-q: invalid customer") ]))
(define (build-q-tla obj)
(q (customer-id obj)
; other customer fields
...
activate-user ; not quest specific
get-user-data ; not quest specific
display-link? ; not quest specific
tla:application-css
tla:application-name
tla:validate-provider-meta-data
tla:validate-provider-quest-data
...))
It was efficient (no dynamic method lookup), simple, and worked ok because it was a flat class hierarchy of just one level. A typical use:
((q-get-user-data obj) conn obj reference-key type)
Which is now:
(send obj get-user-data conn obj reference-key type)
And... as I typed that, I just realized I probably don't need the second obj
now since get-user-data
can refer to this
I'm pretty sure I'll need a deeper class hierarchy, and while I was contemplating this, I saw Ryan's talk Adding method dispatch for a deeper hierarchy is where I saw I was re-inventing the wheel too much.
It was trivial to convert my code to use Racket classes. For the time being, I'm going to continue to use simple structs for my database records, and there was one small advantage of my previous approach - I could use a q
where a customer
was needed, but with my current approach, rather than q
subtyping customer
it composes a customer
.
Although Rails use of object orientation has some niceties, for the most part, I felt the magical implicit aspects went too far. I'll need further research to determine whether I want to use classes for database records vs. my current structs, but for the simple behavioral specialization I needed, classes make sense to me, and my code is now much more maintainable.
Kudos to the folks who created Racket's class system! It's nice to have that tool for the cases where it fits well, and I love how it feels more Rackety than previous OO systems I've used.
Brian