Why Am I Beating this Dead Horse?
My friend Adam, as he is wont to do, ranted. He wrote a screed on getters and setters, in his typical Adam rant style. Folk commented. This started a small discussion in the Twitter-verse on the topic, which in turn resulted in Ben's post. More comments. A post from Brian. More comments. More posts.
So I figured, what better way to drive traffic to our unit testing blog than to add more to the topic du jour? Whorish, I know.
Without further adieu, an MXUnit hombre's take on the topic. This is not a response to any of the authors or commenters in the posts mentioned above, although it does cover a lot of the same ground. This is an article about principles that appear to collide. At its core, this article is about Negative Capability.
Should you expose data? If so, how?
This is a two-part discussion. First, should you expose data? Or is the need to expose data an indication of poor design? Second, if you're exposing data, how do you do it? Methods or straight-up variable access via "this" scope?
Why exposing data is bad
This is important. It's the crux of this whole discussion. I believe this is the fundamental source of confusion/angst/rants about this topic.
The argument against exposing data is that when you have simple "Bean" objects that are merely data containers, you end up with an anemic domain. In your anemic domain, your simple objects don't do anything, but are passed around to other objects that do the work. Those objects then must fetch data from the bean, via getXXX(), or via this.XXX. This results in the question, "Why are the other objects asking the object for data and doing the work when the object should be doing the work itself?".
Herein lies the problem, the source of the confusion: Does the presence of getters/setters indicate an anemic domain, of an object not shouldering its responsibilities? Does it mean my object sucks?
The answer is, No. The answer also is, "it depends". Ben hates that answer.
I believe the first step in determining whether the data are accessible for the right reason lies in asking yourself a few questions: Am I exposing the data because other objects need that data to do work? Is the work in question rightfully theirs, or should I (the object) be doing this work myself? Am I exposing the data because an html page needs it? etc.
Good Encapsulation
Let's look at an example where an object with the data does its own work: the java String class. (Ben is smiling right now. Ben knows why.)
String has all kinds of nifty functions: replace(), indexOf(), replaceFirst(), subString(), etc. You use them thusly:
String myString = "I love ColdFusion";
int whereIsCF = myString.indexOf("ColdFusion");
String has the data, so it's natural that it should perform the operations on myString. It should do the finding of substrings. It should return new strings with replace(). It has the data, why should it require you to use another object?
Bad Encapsulation
Now, imagine if you had to work with Strings like this:
String mystring = "I love ColdFusion";
StringService service = new StringService();
int whereIsCF = service.indexOf(mystring,"ColdFusion");
That should look really weird to you. You should be thinking, "Yeah, that's dumb. String has the data, why pass the object to another object, which is going to in turn call string.getStringData() to get the bits and then do the finding, and then return the position? Don't make no sense."
In Sean Corfield's response to Ben's post, he described the pattern something like this:
myBean = createObject("component","MyBean");
myBean.setXXX("XXX");
myOtherObject.doSomethingWithBean(myBean);
If you think about it, that pattern is what's going on in our Bad Encapsulation example.
So when do I expose, and when don't I?
This will probably be the most subjective part of this post. So be it.
I believe if your object is going to be presented in some GUI (web form, eg.), then you have to expose your data. Use snippets. Use codegen. Whatever. Make it available. The alternative is to use the builder pattern. Sorry, Swing FanBoys, but this is the web we're talking about. I've tried using Builder to build webforms. It is untenable. 1) It virtually precludes the designer/developer workflow. 2) In all but the simplest webforms, your object will not have all the data it needs to properly build the form. It's going to need options for dropdown fields that are only tangentially related. It's going to need groups of radio buttons and checkboxes that may or may not relate at all to your specific object. Bottom line, for me: it's a pain in the ass to build webapps this way.
However, I also believe that as you're working with your object, and you find yourself falling into the pattern described above -- where you're merely filling up a bean and passing it around so other objects can do work -- then you're likely getting into iffy design territory. It should start to smell. You should be asking yourself, "Am I creating a StringService?"
Ok, I think I'm getting it, but....
But.... what about The Single Responsibility Principle? If my object does all the work, won't it turn into this monstrous God Object? Am I not simply reverting back to procedural code within my objects, thus creating false OO BS?
That, Detective Spooner, is the right question
The question here is, "How do you know which objects should be doing which work?". A common example is the Bean/DAO combination pattern.
- Should the "Bean" be a data container, and you pass the bean to the DAO, which calls all its getters in the SQL?
- Should the bean be responsible for its own persistence?
- Should the bean be responsible for its own persistence, but instead of the queries directly in the bean, it delegates them to a composed DAO?
- Should the DAO take a bean as an arg, or should it take the bean's raw data as separate args?
Let's assume for a minute you're not using an ORM or framework that provides persistence mechanisms (like Transfer, Reactor, DataFaucet, etc). What are the arguments for the options above? (The astute reader will note that at least one person will comment on this post or another one of the posts mentioned above that there are at least 5 more ways of approaching this, and that way XYZ is best, and then that's how the annoying comment-arguments get started. The astute reader will discover that those arguments miss the point entirely. The point is to think about why you're doing what you're doing. The point is that it's about the journey, not the destination.). Now, where was I? Oh, yes, the options above. Let's look at them briefly:
Bean + DAO: What if I need to swap out DAOs later? What if I change databases? What if I need to support multiple databases in my app? Isn't it a good separation of concerns to have the object not be concerned with its persistence? Isn't the nature of object persistence in general simply a sideshow act, i.e. objects live to do other stuff, and the fact of their persistence is tangential? Isn't God really a woman? But if my database changes, now I have to make a change in two places (Bean and DAO), not one. Doesn't that violate orthogonality? Doesn't this result in high coupling whereby the DAO must know about the internal workings of the bean, to some degree?
Bean does it all itself: Am I really going to need to change databases? Really? Isn't that violating YAGNI? Do I really need to support multiple databases? If the bean does it all itself, doesn't that support the principle that I should only ever need to change something in one place (i.e. similar logic not strewn about the system in various places)? But doesn't this violate the Single Responsibility Principle, i.e. that a class should only ever have one reason to change? But, honestly, isn't the whole reason for this class's existence to be tossed about from form to database and back again? Is it really violating SRP if its responsibility IS to act as a mediator between user and database? Is this kind of schizophrenic back-and-forth normal?
Bean delegates to DAO: But what do I gain? The DAO is still calling the getters. If the DB changes, I still have to change code in two places. However, the "calling" code doesn't know that. it just merrily calls bean.save(form). So... no changes in client code. Also, makes it easy for mocking the DAO in unit tests. But with MXUnit's injectMethod, is that even relevant anymore since method mocking is so easy?
DAO takes bean's data as separate args: The DAO now needs to know nothing about the internal state of the bean. But... isn't the point of objects to encapsulate stuff so I don't need to be throwing around 12 different arguments into some object? And who wants functions that look like dao.save(bean.getThing(),bean.getThing2())...?
As I said... jibba-jabba.
Analysis is important. This back-and-forth is why you're doing what you do, reading this article, instead of saying "Welcome to Wal-Mart... is that a return?".
Part II: How to expose the data
We already talked about why you might, and might not, want to expose the data. In other words, when exposing the data is essential and when it's a design smell. Now, let's talk about how to expose the data. In CF, we have three choices: concrete getters/setters, generic getters/setters, and the "this" scope.
The arguments for getters and setters
- They abstract the underlying data model (what if I want to keep my data in a struct named variables.instance and not in 'this' scope? what if my model changes?)
- Insulate client code from under-the-hood changes
- Enable lazy data loading
- No additional typing overhead... just use snippets
- Via API documentation, they very clearly show exactly what data are publicly available
- The provide the ability to perform custom behaviors when setting or getting (this is the biggun')
The arguments for generic getters/setters (get('key'))
- They abstract the underlying data model without requiring so much typing/extra code
- They appeal to the dynamic nature of ColdFusion. Specifying typed returns on concrete getters/setters really isn't a gain in a dynamic language, so who cares if generic getters/setters deal in Strings?
- When the properties of the object change (new fields added, for example), no additional code is required because the generic getters/setters will already have taken care of it. This leads to more future-friendly objects.
The arguments for 'this'
- YAGNI: You ain't gonna need the custom behaviors that concrete getters/setters allow for... 9X% of the time, you won't need any form of manipulation on the data... you're just getting it directly from the data struct. So 'this' works just fine most of the time
- Unnecessary typing and overhead. Leads to more lines of code to maintain (when the DB changes, for example).
The answer from on high
3 Programmers walk into a bar. One says "this". One says "getter". One says "get". Getter pounds This's fists and says, "I Will Break You.". They get drunk and fight. The next morning they wake up, hungover, black-eyed. Get says, "I shoulda been a Wal-Mart shelf stocker". This says, "Yeah, but then you gotta clean up the crap from the back of the stalls when filthy customers hit the john and..." Getter says, "WTF dude?". You see, This had worked at Wal-Mart. He knew he'd rather deal with mundane, overworked questions of data access techniques any day over cleaning up after explosive asses in a Wal-Mart bathroom.
CTRL-Space-Learn
We CF programmers are underserved in the IDE space. Our tooling, frankly, blows. I believe this will be remedied soon. And when it is, a new argument for getter/setter methods will emerge: Our tooling will make it worthwhile to do so. Let me explain.
When programming Java in Eclipse, NetBeans, IntelliJ, etc, you get code hinting. Code hinting has been around for, oh, I don't know, 25 years or so (SmallTalk IDE?). You're thinking, "yeah, but in CFEclipse, or CFStudio, or Dreamweaver, I get code hinting for CF Functions and tags". Which is true. But you don't get hinting for your own objects. This. flat. out. sucks. Sure, you can write your own dictionaries for CFEclipse, but that's just lipstick on the pig and is virtually unmaintainable. If you've never programmed in another language with proper code hinting, you don't know what you're missing. Let me tell you what you're missing.
You're missing one of the most powerful features of the IDE: the ability to teach yourself as you type. You're missing CTRL-Space-Learn.
When you're in the IDE, working on code that uses components unfamiliar to you (or even components you wrote yourself a while back), you have two choices for learning what functionality is available to you: You open up the component, or you view that component in the browser and get the CF component explorer output. It's a pain. In other languages, you simply type the name of your object instance, hit CTRL-Space, and up pops the methods available to you. Think of the power in that. C'mon, Think. Think of the efficiency. You're working in code that uses some BlogReader.cfc instance, and you want to write some stuff that spits out a list of all entries for a particular blog. You didn't write the code for BlogReader.cfc... someone else did. What do you do? Personally, I'd rather do this: reader = createObject(....); reader - period - CTRL-Space.... up pops everything I can do with that object.
If you're a programmer who likes to discover what objects do, the IDE is your best friend. I find this method of exploration to be one of the invigorating parts of programming with other people's code. When I'm working on the MXUnit Eclipse plugin, I'm CTRL-Space-Learn-ing about the Eclipse API every 30 seconds.
If a better IDE is released for CF, I do hope it includes code hinting for custom components without the need for a dictionary file. If it does, you'll see why getters/setters become much more attractive over generic getters/setters. I would assume, though, that this advantage would also be conferred to the 'this' scope, so this is not about 'this' vs. getter but about generic vs. specific.
So what are you saying, man?
Personally, having tried it all kinds of ways, I've settled into the habit of using private generic getters, in a base object, that encapsulate the underlying data structure. In my objects, which extend Base, I provide concrete getters/setters. They, in turn, set and fetch raw data via get('key') and set('key',value) functions, and possibly perform other operations on the data. This way, in a single place (get() in Base.cfc) I can do lazy loading. I only have a single place where the underlying data structure is defined (Base.cfc). And I can change functionality in Base.cfc to my heart's content without affecting any code that extends it. In addition, since I'm convinced we're going to get an IDE soon that supports hinting, I'm paving the way with concrete getters/setters for in-IDE API docs. I'll get CTRL-Space-Learn for CF. More importantly, my coworkers will get it for the objects I write, and I'll get it for the objects they write.
Concrete getters and setters aren't about you. They're about other people.
Obligatory Irrelevant Java Example
See StringService above
This Is Where Your Eyes Glaze Over (or, The Real Example)
Let's take a small sample application and see what it looks like through some of the various permutations described above. In fact, let's start off with something we didn't talk about at all: a completly procedural example that avoids all these silly pesky OO questions entirely
Straight up procedural
<!--- act_ShowFeeds.cfm ---> <cfquery name="Feeds" datasource="#application.dsn#"> select FeedID, FeedURL, FeedType from Feeds order by FeedSortOrder Desc </cfquery> <cfoutput query="Feeds"> <cffeed source="#FeedURL#" query="feedentries"> <dl> <cfloop query="feedentries"> <dt> <cfif Feeds.FeedType eq "Atom"> <a href="#listFirst(linkhref)#">#feedentries.title#</a> <cfelse> <a href="#rsslink#">#feedentries.title#</a> </cfif> </dt> <dd> <cfif len(feedentries.Summary)> #feedentries.Summary# <cfelse> #left(content,200)# </cfif> </dd> </cfloop> </dl> </cfoutput>
Pretty straight forward: get a list of feeds from the DB, loop over them, and for each feed, print out its title (with link) and a snippet of its description. Different feed types require different behavior. This example doesn't contain any persistence of feed items (we'll add that below). But if it did, it'd most likely just be in other "action" files.
Single Object
<!--- dsp_ShowFeeds.cfm ---> <cfset reader = createObject("component","FeedReader")> <cfset reader.renderFeeds()> <!--- FeedReader.cfc ---> <cfcomponent name="FeedReader.cfc"> <cffunction name="renderFeeds" output="true" access="public"> <cfset var feeds = getFeeds()> <cfoutput>#printFeeds(feeds)#</cfoutput> </cffunction> <cffunction name="getFeeds" access="private"> <cfset var getFeeds = ""> <cfquery name="getFeeds" datasource="#application.dsn#"> select FeedID, FeedURL, FeedType from Feeds order by FeedSortOrder Desc </cfquery> <cfreturn getFeeds> </cffunction> <cffunction name="printFeeds" access="private"> <cfargument name="feeds" type="query"> <cfoutput query="Feeds"> <cffeed source="#FeedURL#" query="feedentries"> <dl> <cfloop query="feedentries"> <dt> <cfif Feeds.FeedType eq "Atom"> <a href="#listFirst(linkhref)#">#feedentries.title#</a> <cfelse> <a href="#rsslink#">#feedentries.title#</a> </cfif> </dt> <dd> <cfif len(feedentries.Summary)> #feedentries.Summary# <cfelse> #left(content,200)# </cfif> </dd> </cfloop> </dl> </cfoutput> </cffunction> </cfcomponent>
Have we gained anything here? Well, it's unit-testable to some degree. We can use it in multiple places easily (although we could do the same with cfinclude in our procedural example, too). Otherwise... pretty ugly. Biggest advantage is probably testability (however minimal).
Multiple objects, doing their own work
This is my ultra extended example. I'm including Base.cfc in here to provide context, although don't count that against the "cost" of the feedreader stuff, since the cost of the base object will be spread out over all objects that use it.
<!--- dsp_ShowFeeds.cfm ---> <cfset reader = createObject("component","FeedReader")> <cfset feeditems = reader.getFeedItems()> <cfoutput from="1" to="#ArrayLen(feeditems)#" index="item"> <dl> <dt><a href="#item.getLink()#">#item.getTitle()#</a></dt> <dd>#item.getAbstract()#</dd> </dl> </cfoutput> <!--- Base.cfc ---> <cfcomponent name="Base.cfc"> <cfset variables.instance = StructNew()> <cffunction name="init"> <cfargument name="ArgumentsScope" type="struct" required="false" default="#StructNew()#"> <cfset var key = ""> <cfset var tmp = {}> <cfloop collection="#ArgumentsScope#" item="key"> <cfif isDefined("set#key#")> <cfset tmp[key] = ArgumentsScope[key]> <cfinvoke component="#this#" method="set#key#" argumentcollection="#tmp#"> <cfset structClear(tmp)> <cfelse> <cfset variables.instance[key] = ArgumentsScope[key]> </cfif> </cfloop> <cfreturn this> </cffunction> <cffunction name="getDSN"> <cfreturn "FeedReader"> </cffunction> <!--- internal generic getters/setters ---> <cffunction name="getValue" hint="returns the specified value from the Order Object" output="no" returntype="any" access="package"> <cfargument name="name" hint="The variable you wish to have returned from the object" required="Yes"> <cfif StructKeyExists(variables.instance,"#name#")> <cfreturn variables.instance["#name#"]> <cfelse> <cfreturn "Undefined"> <!--- could also pre-initialize the value here and return it, thereby providing lazy loading ---> </cfif> </cffunction> <!--- note use of return this. I like me some method chaining ---> <cffunction name="setValue" hint="sets a value in the variables.instance structure" output="No" access="package"> <cfargument name="name" hint="The name of the value to set" required="Yes"> <cfargument name="value" hint="The value to set" required="Yes"> <cfset "variables.instance.#name#" = value> <cfreturn this> </cffunction> <cffunction name="queryToObjects" output="false" access="public" returntype="any" hint="converts a query into an array of objects. if query is empty, an empty array is returned"> <cfargument name="query" type="Query" required="true"/> <cfargument name="cfcPath" type="string" required="true"/> <cfset var objects = ArrayNew(1)> <cfset var obj = ""> <cfloop query="arguments.query"> <cfset obj = QueryToObject(query,arguments.query.currentrow,arguments.cfcPath)> <cfset arrayAppend(objects,obj)> </cfloop> <cfreturn objects> </cffunction> <cffunction name="queryToObject" output="false" access="public" returntype="any" hint="converts a single row of a query into an object"> <cfargument name="query" type="query" required="true"/> <cfargument name="row" type="numeric" required="false" default="1"/> <cfargument name="cfcPath" type="string" required="false" default="" hint="Must either pass the full path to a CFC OR a pre-created object"/> <cfargument name="object" type="any" required="false" default="" hint="A pre-instantiated object. Use in place of the CFCPath argument"/> <cfset var columns = ListToArray(query.columnlist)> <cfset var col = ""> <cfif isSimpleValue(arguments.object)> <cfset arguments.object = createObject("component","#arguments.cfcPath#").init()> </cfif> <cfloop array="#columns#" index="col"> <cfif StructKeyExists(arguments.object,"set#col#")> <cfinvoke component="#arguments.object#" method="set#col#"> <cfinvokeargument name="#col#" value="#Query[col][arguments.row]#"> </cfinvoke> <cfelse> <cfinvoke component="#arguments.object#" method="setValue" name="#col#" value="#Query[col][arguments.row]#"> </cfif> </cfloop> <cfreturn arguments.object> </cffunction> </cfcomponent> <!--- FeedReader.cfc ---> <cfcomponent name="FeedReader.cfc" extends="Base"> <cffunction name="getFeeds" access="public" hint="returns an array of feeds"> <cfset var storedfeeds = getStoredFeeds()> <cfset var feeds = ArrayNew(1)> <cfloop query="storedfeeds"> <cffeed source="#FeedURL#" query="feeditems"> <cfset feeds.addAll( queryToObjects( feeditems, "com.blah.FeedItem") )> </cfloop> <cfreturn feeds> </cffunction> <cffunction name="getStoredFeeds" access="private" extends="Base"> <cfset var getFeeds = ""> <cfquery name="getFeeds" datasource="#getDSN()#"> select FeedID, FeedURL, FeedType from Feeds order by FeedSortOrder Desc </cfquery> <cfreturn getFeeds> </cffunction> </cfcomponent> <!--- FeedItem.cfc ---> <cfcomponent name="FeedItem.cfc" extends="Base"> <cffunction name="init"> <cfargument name="FeedEntryID" type="numeric" required="false" default="0"/> <cfset super.init(arguments)> </cffunction> <!--- bunch of getters and setters, generated in about 30 seconds from snippets, removed for sake of space ---> <!--- make these private b/c we don't want to give people access to them... they're dangerous and we'll abstract it for them down below ---> <cffunction name="getRSSLink" access="private"> <cfreturn getValue("RSSLink")> </cffunction> <cffunction name="getLinkHREF" access="private"> <cfreturn getValue("LinkHREF")> </cffunction> <cffunction name="getLink" returntype="string" hint="abstracts out the bullshit between Atom and RSS"> <cfif len(getlinkHREF())> <cfreturn listFirst(getlinkHREF(),",")> <cfelse> <cfreturn getRSSLink()> </cfif> </cffunction> <cffunction name="getAbstract" access="public"> <cfif len(getSummary())> <cfreturn getSummary()> <cfelse> <cfreturn left(getSummary(),getAbstractLength())> </cfif> </cffunction> <cffunction name="markAsRead"> <cfset var markRead = ""> <cfquery datasource="#variables.instance.dsn#" name="markRead"> UPDATE FeedEntries SET read_flag = 1 WHERE FeedEntryID = #getFeedEntryID()# </cfquery> <cfreturn markRead> </cffunction> </cfcomponent>
This looks like a lot, I know. Consider this, though: This object is now extremely testable. I can unit test FeedReader.cfc easily by overriding the getStoredFeeds function using injectMethod and a spoof query, and I can create a query of local RSS And ATOM feed files that would be used just for testing. I can test the abstract and link formatting in the FeedItem.cfc without touching any feeds at all. Hell, if I wanted, I could even abstract cffeed into a separate function, override it in a unit test with a query spoof, and test the functionality of FeedReader.cfc's getFeeds() method in a snap... no reading feed files... just testing that if given a query of feeds from different sources, I get a full array of feed objects back representing each item in each feed.
Extra points for the easy refactoring that can be done in Base.cfc. Hint: look for eerily duplicate code
Multiple objects; Bean spreading its data to whoever wants it
I'm not going to write this example. But consider it to be the Bean + DAO + Gateway.... example. In a nutshell, you'd have a FeedItem.cfc which had almost no behaviors. Maybe it wouldn't even have the link and abstract functions... Maybe it'd delegate that to a "FeedFormatter.cfc". I dunno. And then you'd be passing your bean to the DAO, and the DAO would be calling FeedItem.getTitle() and whatnot for persistning that to the local store of FeedItems. The markAsRead functionality, for example, would look something like this:
<cfcomponent name="FeedItemDAO.cfc"> <cffunction name="markAsRead"> <cfargument name="FeedItemBean" type="any" required="true"/> <cfset var markRead = ""> <cfquery datasource="#variables.instance.dsn#" name="markRead"> UPDATE FeedEntries SET read_flag = 1 WHERE FeedEntryID = #FeedItemBean.getFeedEntryID()# </cfquery> <cfreturn markRead> </cffunction> </cfcomponent>
Or, more likely, you'd probably have a single save() function, and your code might look something like this:
<cfcomponent name="FeedReader.cfc"> <cffunction name="markFeedItemAsRead"> <cfargument name="FeedItemBean" type="any" required="true"/> <cfset FeedItemDAO.read(FeedItemBean)> <cfset FeedItemBean.setRead(true)> <cfset FeedItemDAO.save(FeedItemBean)> </cffunction> </cfcomponent> <cfcomponent name="FeedItemDAO.cfc"> <cffunction name="save"> <cfargument name="FeedItemBean" type="any" required="true"/> <cfif feedItemBean.getFeedItemID() EQ 0> <cfset insert(FeedItemBean)> <cfelse> <cfset update(FeedItemBean)> </cfif> </cffunction> <cffunction name="update" access="private"> <cfargument name="FeedItemBean" type="any" required="true"/> <cfset var update = ""> <cfquery datasource="#variables.instance.dsn#" name="update"> update FeedItems set content = <cfqueryparam cfsqltype="cf_sql_varchar" value="#FeedItemBean.getContent()#">, whatever = #getWhatever()#, readdate = #getReadDate()#, readflag = #getRead()# where FeedItemID = #FeedItemBean.getFeedItemID()# </cfquery> <cfreturn update> </cffunction> </cfcomponent>
And I ask you, in this case: What do you gain by putting that functionality into the DAO and out of FeedItem itself? What do you lose? How does your answer change if, instead of this being an internal application, you're developing an open source feed reader app, written in CF, and you want it to work with SQL Server, Oracle, Derby, and MySQL?
I'm serious. Think about those questions. Don't blow them off. Reading crap like this on the interwebs doesn't mean squat if you don't wrestle with this stuff.
FutureShock
Brian Kotek wrote "It's a principle, not a blindfold" about YAGNI. I like that. I don't think we should consider how our feedreader app is going to need to grow 5 years from now. But we can at least make some reasonable assumptions about the future. Let's do a quick tally to see where we might stand, future-wise:
- Looking at the ATOM format for linkHREF coming out of CFFEED... it's a comma-delimited list. Ugly. What if a link itself has a comma in it? I can see already that the logic for getting Link URLs is a source of potential bugs. getter +1, 'this' -1
- Our display so far isn't very attractive. Quite 1996. We're going to want to punt it to design. Builder -1
- Someone requested a link to the comments for a feed item, if one is available. How? Add getter to FeedItem.cfc that contains logic around the linkHREF and linkType fields. OO +1 vs. procedural
- Someone requested that the author name appear. In ATOM feeds, this is easy. In RSS feeds, not so much. Gonna have to parse the name out of the AuthorEmail field. getter +1, 'this' -1
- Some 12 year old kid with his gizmos and flashy stuff wants an iPhone page, whatever the hell that is. He wants it to work like iPhone pages, where you click the name of the blog post and the screen swooshes to the right and displays the full feed. WTF? OO +1 vs. procedural. Now, it's just a matter of writing HTML and javascript. No backend changes at all required. No duplication of feed reading/parsing/etc. required.
- You knew it. You just knew it. Some dude wants PostgreSQL support. Everything almost works. But those damn "bit" fields... Separate DAOs +1
- CF9 comes out and they add 3 more columns to the CFFEED query. You want to provide access to those fields in your FeedItem object, but you still want to retain support for CF8. getter +1? 'this' + 1? Oh, wait, it's an ugly field like the linkHREF field that requires you to *do stuff* to it. getter +1 (insert scream from some reader saying, "But that shit never happens!" here.)
- You want to learn some Flex and think a new whizbang Flex front-end would be sweet for your feedreader. You want to pass your FeedItem objects across the wire rather than straight up queries. Will it work with your "smart" FeedItem object as it's currently written? Anemic domain + 1? You tell me.
- Someone's blog URL has changed, and you update the database with the new URL. Next time your cffeed routine runs, though, it doesn't recognize the older feeditems as "read", and so your list of links now has a bunch of old junk that you did, in fact, already read. How hard is it to update your code to fix this?
- Your app is slow. C'mon, n00b. So you decide to speed it up by making the feed reading multi-threaded. Big deal? T'aint nuthin? Do any of the designs above provide an advantage here?
Does context matter?
- You've been reading about onMissingMethod in CF8 and you think, "Hey, that's a great way to provide zero-code implicit getters/setters. I think I'll do that!".
- You hear that CF9 might include implicit getters/setters... does that eliminate all this malarchy entirely? What about the IDE -- CTRL-Space-Learn?
- Do your answers to any of these questions change if you're working by yourself as a freelancer as opposed to working on teams with other people using your code?
- Does clarity of API matter if you're working on a team of really smart people? Does the value of in-IDE API support diminish as the quality of team members increases? i.e. Does your answer to the specifc/generic/this conundrum differ based on the team in which the code will be used?
Summary
If you've gotten this far, you're in two camps: you didn't read the top and instead scrolled down here for the answers. Or you read this far and you're just hoping that the dude writing this says "OK, people, here's the deal".
Here's the deal
If your objects are anemic objects with no behavior at all, giving their data to other objects to do the work, ... that's a smell to me. This is why people say "getters are bad". More than likely, something has to be done with that data. Some behavior is likely required. If the object with the data isn't performing that behavior, then some other object must be doing it, and it's probably doing it by having the object in question passed into it, then calling its getters. You gotta ask yourself this question: Am I writing a StringService?. Should this object, my original object, be doing its own work?
If your objects have getters because, Dammit, you ain't writing no forms inside your objects, well, that makes sense to me. If you want objects to populate forms, you need to expose the data. It's that simple. People don't say "getters suck" for this reason. They're not talking about web forms. They're talking about "the-wrong-object-is-doing-the-job" problem. Stop your fretting, Peregrine Took.
If there's extremely little chance of swapping databases, providing multi-database support, etc, then why do you need separate DAOs? Fight amongst yourselves. I know where I stand on this one.
As for generic getters/setters, IDE support for custom object code hinting will change everything. Just you wait.
I could write another follow-up post simply on the testability aspect of the various approaches discussed above. I know, unit testing isn't done by the majority of CF Developers. But for those who do unit test, of the approaches above, which design decisions lead to easier-to-test code? Which provide the ability to "spoof" or abstract certain functionality for testing purposes in order to test other specific behavior?
Summary, for real this time
This post isn't about answers. It's about questions... about learning what questions to ask yourself as you make the design decisions you'll be faced with. Write yourself a small application and try it out in a few different ways: bean+dao. smart object doing the work. bean + composed DAO. What did you like? What felt right? If you had to change something big a half year from now, which of your 3 apps would you prefer to work on? Try using 'this' and generic getters/setters and concrete getters/setters. Assume a co-worker is going to be using your object in one of theirs. What would they rather work with? What gets them ramped up quicker? Try writing code that passes nothing but raw data into the DAO object such that the DAO doesn't call obj.getSomething().... did you like that? What does the code look like after you're finished with it? Imagine you have to add a few new columns.... ask 2 co-workers which code they'd rather maintain were they tasked with modifying your DAO/Bean stuff. And finally, try writing unit tests for the app, designed various ways. Were there differences in testability between the various apps?
These are the questions that matter. I said at the beginning of this post that it was about Negative Capability... the ability to hold contradictory, conflicting thoughts about a thing and be OK with it. In some ways, it means embracing the pain. I suppose in the context of this discussion, it means accepting that the answers to the questions "Should I expose data?" and "If so, how?" are influenced by factors not specifically related to the code itself. Code doesn't exist in a vacuum. People read it. People maintain it. IDEs parse it. Et cetera. I'm not suggesting that "everything is relative" and "it always depends" and "there are no right answers". There are! I do, though, believe that in order to answer these two questions, the answers to *the other questions* will help get you there. Take heart, Merry!
Of course, you could always trade in your keyboard for a Wal-Mart smock. "Cleanup, Women's Stall 4...."
6 comments:
Holy Crap dude, that was beautiful. I should just rant in private to you so you can write stuff like this all the time. After reading some of the other crap circulating I was starting to regret gettting it going, folks linger too much on the part I was being an ass about and not focusing on the other part (my fault). You did a great job Marc. If you use This scope I would hope ctrl space learn would work too but nothing big there. Also I want to point out that while I used YAGNI to defend THIS scope I was mostly going through all that to get to my punch line of just using structures :)
This is an awesome post and one I think I'll be reading again. Thanks for taking the time to write it! You've raised some really good points here and made an excellent job of showing that their is not just "one true way".
Awesome post! I only read PART I (need to get some work done), but am loving it so far. Will return tonight to finish this up. Really good stuff, can't wait to finish.
Good stuff, Marc,
I have one major correction: With respect to Wal-mart greetings, shouldn't it be "… is that a return OR EXCHANGE ..."?
The Wal-Mart merchandise exchange/return analogy is actually one of the themes I hear in your well thought-out post. In other words, when we maintain our code (evolve it) we exchange certain parts of it - effectively swapping out implementations. In some cases, we look at the effort associated with evolving something and simply opt for a total "return" - or re-engineering the app from the ground up - "Do you have a receipt"? One thing never changes; and that is nothing stays the same. So, today I cast my vote for adaptability and agility in design. In order to do this, I think our software needs to be testable and testability naturally seems to lead to smaller components, but more of them. It's kind of like dealing with Legos vs. cinder blocks. And I don't think we have any metrics for how much it costs to re-engineer a large procedural legacy system vs. re-engineering a well-designed OO system, but, I would much rather be brought on a project that has a bunch of Legos with tests than a couple big-ass cinder blocks.
When it comes to instance data (set/get), which make up an object's state, accessor methods were suggested in order to control the effective state transition of an object. I think when dealing with certain kinds of data or needs we might need this kind of control, in other kinds of data it's not as important. I also sense the potential thread-safety issues when dealing with instance data ... that is, anything shared and not synchronized, whatever that might be.
I think we all want some simple rules we can follow: "Always use accessor methods and private instance data", or "do this" or "don't do that". It is a pain to wear depends, but we do need to catch that stuff that eeks out of our technical dogmas, deal with it, and admit that we need a changing. So, it depends …
bill
i just reread your comment, bill. and i would like to submit it to the blogosphere for Comment of the Year award.
well done, sir.
Rich business object vs service object. I have been wrestling with how much delegation should be assigned to each. It's hard to find the balance.
Right now, on one of my projects, I have a business object that has a reference to my DAO. My BO gets the initial query result, and does all kinds of checking and then logs the data into a database table.
From the client page, the implementation looks like this
myBO=new BO();
myBO.setData(data);
myBO.processImage();
myBO.hasErrors();
myBO.getErrors();
myBO.getImageCount();
'processImage' has data validation and invoke DAO method to log the data.
Having a rich business object/bean in this situation feels 'cleaner' and encapsulated, rather than exposing the validation logic on the client page and then once validated, passing data in the service object, which then calls the DAO to log the data.
Post a Comment