Saturday, August 8, 2009

How smart should an object be?

Since I'm relatively new to OOP, one thing I've been struggling with lately is how smart should I make my domain objects. Should an object know how to save itself? Take the following code for example:


user = new User();
user.setFirstName("Tony");
user.setLastName("Nelson");
user.save();


This type of code follows the Active Record design pattern, where domain objects contain your CRUD operations.

Hibernate, which acts as a Data Access Object, takes a slightly different approach. Rather than having the object save itself, Hibernate performs the CRUD.


user = entityNew("User");
user.setFirstName("Tony");
user.setLastName("Nelson");
entitySave(user);


Since Hibernate has become the ORM of choice in ColdFusion, I've decided that an object should not know how to save itself. Now with that in mind, should an object know how to populate itself? Consider the following code:


user = new User();
user.setFirstName(arguments.firstName);
user.setLastName(arguments.lastName);
user.setEmail(arguments.email);
user.setPassword(arguments.password);
user.setBirthDate(arguments.birthDate);
user.setGender(arguments.gender);
user.setHeight(arguments.height);
etc...


Pretty sure we've all seen code like this, where you're just taking all the arguments and calling their corresponding setters by adding "set" in from the argument name. Wouldn't it be a lot simple to just do:


user = entityNew("User");
user.populate(arguments);
entitySave(user);


But again, since a user shouldn't know how to save itself, it probably shouldn't know how to populate itself either, which gives us:


user = entityNew("User");
entityPopulate(user,arguments);
entitySave(user);


Mmmm... entityPopulate() would be a handy little tool. Now what about validation? Same theory applies...


user = entityNew("User");
entityPopulate(user,arguments);
errors = entityValidate(user);
if(ArrayLen(errors) == 0) {
entitySave(user);
}

Well it's short, although I'm not sure it's entirely clear what we're saving since it's so generic. But again, maybe that's a good thing. Ideally, you could get rid of all the "entity" stuff and make a really generic Service that looked like this:


user = new("User");
populate(user,arguments);
errors = validate(user);
if(ArrayLen(errors) == 0) {
save(user);
}


That almost looks as good as pseudo-code, with the only thing that's a little code-y being ArrayLen(errors). Speaking of which, there should be a generic length() function that can accept an array, query, struct, or string and return its length, regardless of data type. Man, that would be pretty sweet. Then the code would look even shorter.


user = new("User");
populate(user,arguments);
errors = validate(user);
if(length(errors) == 0) {
save(user);
}


Seems pretty clean. Just for fun, how would that look using smarter objects and a little method chaining?


user = new("User").populate(arguments);
errors = user.validate();
if(length(errors) == 0) {
user.save();
}


Not too shabby. And now you see my struggles.

2 comments:

  1. I agree that a User object shouldn't know "how" to save itself, but I think it's fine for it to "be able" to save itself. My preference is to compose a Gateway into the User object, which then uses EntitySave() to save the object. This allows me to call a save() method on the User, but the User doesn't know how it's being saved - it just knows to ask the Gateway to do the job.

    I do something similar with validations, where I can call a validate() method on a User object, and it asks another composed object to do the validations.

    For populating the object, I tend to just create a method in the User object itself (in a base object, actually, which the User object extends). I suppose I could create an ObjectPopulator object, and compose that into the User object as well, but I haven't taken it that far yet.

    I do think there is value in being able to ask a User to save, populate and validate itself. If instead of using User.populate(), User.save() and User.validate(), we use EntityPopulate(User), EntitySave(User) and EntityValidate(User), where do those functions reside? In a Service Object? That just seems more like writing procedural code that uses objects, rather than writing Object Oriented code.

    ReplyDelete
  2. While I agree with using an external object to perform the validation on objects since there's probably some relatively complex logic going on, it seems a little unnecessary to me to relay the save method to a Gateway just to call EntitySave(). And technically the User object isn't really saving itself, it's passing that logic off to Hibernate to perform the save. If you put the call to EntitySave() inside the base object that the User extends, then you're still hiding the implementation of how the object is saved, but with a lot less code and smaller objects, since you're not having to inject a Gateway into each User object.

    Ideally (maybe?) I would use a base object that has populate(), save(), delete(), validate() which in turn call EntityPopulate(this), EntitySave(this), EntityDelete(this), validationService.validate(this). That way your objects are aware that they can be populated/validated/saved, but the implementation is hidden inside the base object.

    ReplyDelete