Exercise List: Thinking Out The Domain Model
Click here to view the current Online Demo
Click here to view / download the current Code Base
With the procedural portion of the project done and behind me, it is time to start converting this project to one that uses Object Oriented Programming. Obviously, this step is the hard one as it requires a completely different way of thinking about projects. Traditionally, I have done everything from a very data-centric view point. That is why, after I designed the interface, the very next thing I did was design the database schema. To me, applications are all about data and how it relates.
When it comes to object oriented design, you have to think primarily in terms of domain objects and functionality. It might seem like this is an easy task since after all, isn't the domain model supposed to reflect real life? But, it is not the case, especially if you have been raised on the data centric vision of the world. To be honest, I am not even sure what goes into modelling a domain.
Luckily, I was able to spend some time consulting with Clark Valberg on this matter and it was quite an eye opening experience. Clark kept telling me that when modelling a domain and defining the domain objects, you are not supposed to think about the implementation at all; don't think about the database design, don't even think about whether or not it is an object that will ever be instantiated. Instead, think purely about the glossary of domain items that are required to define the problem space.
So, for example, he was saying that a "Joint" is a domain object. I argued that I would never need to view an individual joint and therefore, I would not need an object to model it (I only needed it as part of a query). To this, he again said that I was worry about implementation and coming at things from a very data-centric view - that that will all come later and is not really part of the initial domain modelling.
I understand what he is saying, but it is so hard for me to disconnect myself from thinking about the inevitable code files. I think this must be the hardest step for people who are switching from a database / procedural code behavior pattern into the more abstract thinking of objects and object oriented design and programming.
After a good deal of back and forth and several discarded pages of diagrams, Clark suggested that one of the obstacles in my way was that the application, Exercise List, didn't really lend itself well to object oriented programming; the application doesn't have a lot of functionality that needs to be encapsulated; it doesn't have a lot of responsibilities that need to be distributed; really, it is a very data-centric application that revolves around the lookup, modification, and deletion of data rows with almost no additional business logic. The problem was that the classes (ColdFusion components) that I developed were going to mirror my database almost exactly, and, not that this couldn't be done with object oriented programming, but, that this is not a project that was going to effectively teach me how to use object oriented principles.
This was a little disheartening, but I think I understand what he is saying. Many of my applications are very data-centric, and I think this is part of the reason it has taken me so long to make the OOP push; it never felt all that relevant. It has only been on very large projects that have multiple points of interaction and lots of complicated business logic where I really felt that encapsulating business logic into service objects and what not would really provide a great benefit.
All that said, I am still going to push ahead with this project because I feel that any exposure to a new way of thinking is well worth it. If I can come away with even one good thing, then the outcome will have been worth the effort.
Ok, so what kind of objects are we going to need for Exercise List? The way I see it, there are really only two objects into which I will need to set data and from which I will need to get data. These are the Exercise, which is really the reason of being for the application, and the object which models the relationship between the exercise and the joints on which the exercise does work.
I don't know what this second object should be called. Going back to a data-centric / database implementation, this object would model the database entity that joins exercises to joint, action, movement plane, and movement symmetry. In the database schema, this is the exercise_joint_jn join table. The Exercise object composes some sort of collection of these objects. I have no idea what they should be called. In the database the table sort of has a name, but I never really thought of that as a proper name, more like a characteristic. I know that sounds odd, but again, I am very data-centric as you can clearly see.
Since I don't really know what I am doing, and I am very new to this concept, I am just going to call this object almost exactly what it is called in the database - it will be the ExerciseJointRelationship object. An Exercise object will compose zero or more ExerciseJointRelationship objects that characters how the exercise works on joints within the human body.
As far as I can see, those are the only two primary objects that I will need. In addition to that, I will need several service related objects that will return queries, retrieve data, and commit data; I will model these based on what I can remember from dozens of articles that I have read about Object Oriented Programming in ColdFusion (and most likely did not understand).
The disclaimer: I am sooo winging this. I am dominated by the understanding of data and the thought of eventual code and therefore, I am sure that the following list of objects and their structure is not all that good. But, this is a first step in learning (all feedback hugely welcome!). The objective here is NOT to get it right on the first try, but rather to jump in and just get something!
- Init() :: Exercise
- GetAlternateNames() :: string
- GetContraindications() :: string
- GetExerciseJointRelationships() :: ExerciseJointRelationship
- GetDescription() :: string
- GetName() :: string
- SetAlternateNames( string ) :: void
- SetContraindications( string ) :: void
- SetExerciseJointReleationships( ExerciseJointRelationship ) :: void
- SetDescription( string ) :: void
- SetName( string ) :: void
- Init() :: ExerciseJointRelationship
- GetExerciseID() :: int
- GetJointActionID() :: int
- GetJointActionName() :: string
- GetJointID() :: int
- GetJointName() :: string
- GetMovementPlaneID() :: int
- GetMovementPlaneName() :: string
- GetMovementSymmetryID() :: int
- GetMovementSymmetryName() :: string
- SetExerciseID( int ) :: void
- SetJointActionID( int ) :: void
- SetJointID( int ) :: void
- SetMovementPlaneID( int ) :: void
- SetMovementSymmetryID( int ) :: void
- Init( struct ) :: ExerciseGateway
- Search( [string [, ExerciseJointRelationship ]] ) :: query
- Init( struct ) :: ExerciseDAO
- Delete( Exercise ) :: Exercise
- Read( int ) :: Exercise
- Save( Exercise ) :: Exercise
- Init( struct ) :: JointGateway
- GetAll() :: query
- Init( struct ) :: JointActionGateway
- GetAll() :: query
- Init( struct ) :: MovementPlaneGateway
- GetAll() :: query
- Init( struct ) :: MovementSymmetryGateway
- GetAll() :: query
That's it for objects that I can think of, at least for now. With all the data going into and coming out of the database, of course, data validation becomes an issue. I have no idea where data validation is supposed to go, so for now, I am going to keep the validation in the business logic, as it is now; I am going to assume that any ColdFusion component passed to a data access object contains valid data.
Again, I really don't have much of a clue as to what I am doing, so don't take this as the right way to do things - that would be like the blind leading the blind. I'm just feeling my way here in the dark. Any and all feedback would be greatly appreciated.
RE: Data validation
As you have created your Exercise bean, then all of the Exercise data validation (remotely) will happen inside of this, especially as you have getters and setters for setting your bean data (which have all of your data restrictions when passing in your arguments). Any incorrectly formed data would be handled on bean creation (for Joints it would be in your Joint bean, for movements, in your movement bean). Each bean would "know" what were valid arguments for its creation, and could let the other objects "know" when they call them.
I'm pretty new to this too (just kinda got dropped into OOP programming at my new job), but it seems like you're heading in the right direction, and I'm sure you'll be changing things along the way :)
I think the project can still be used to demonstrate OO principles if you scale it up.
Let's say the exercise list can be shared between "Trainers" and "WeightLifters." A "Trainer" is a Person, and a "WeightLifter" is an Person.
The trainer wants to monitor progress of his own goals, and he wants to monitor progress of the goals of his students (that we call "WeightLifters").
Now, let's suppose a "Weightlifter", who has a lower set of objectives than the "master" trainer, has a goal of lifting 500lbs in a week.
Let's also suppose a "Trainer", who has a higher set of objectives than the newbie "Weightlifter", has a goal of lifting 1000lbs in a week.
The Trainer ALWAYS wants to always lift twice what his weightlifting student lifts.
A Trainer is a weightlifter. A weightlifter is a Person. A person doesn't lift anything, short of a backpack or in my case, a coffee cup.
Trainer [inherits] WeightLifter.
WeightLifter [inherits] Person.
Person.cfc (base class)
And the OO Principles begin...
<cfobject name="personObj" component="athelete">
<cfobject name="weightLifterObj" component="weightlifter">
<cfobject name="trainerObj" component="trainer">
A person's target goal is #personObj.personWeight#lbs. per week.<br>
A weightlifter's target goal is #weightLifterObj.personWeight + weightLifterObj.weightLifterWeight#lbs. per week.<br>
A trainer's target goal is #trainerObj.personWeight + trainerObj.weightLifterWeight + trainerObj.trainerWeight#lbs. per week.
I don't think I fully understand. When you are setting values into a instance object, are you saying that it would throw an error or something when you set the value. Data type is only a part of validation. For example, the ExerciseJointRelationship needs to hold instances of ExerciseJointRelationship, but that is just type validation. From a business logic stand point, it can only hold a MAX of one per joint ID....
I am gonna just figure this stuff out as I got... several iterations I am sure :)
I think your example would certainly help to teach inheritance, but that is really only one aspect of object oriented programming, from what I think. I don't think I can tackle all aspects at once... I will try to just make do with what I have now, and then maybe scale up later.
What you said :)
The getters and setters would, as you say, manage the data validation (numbers, "Exercise", string, etc.), and then I would have separate objects that would manage the business logic (e.g. a Joint gateway would validate whether the jointID passed to it was within the joint parameters by "getting" your Joint object and making sure the returned value was not null/0, i.e. that it fell within the valid joint range that you have set up in your dB).
The business objects "know" about themselves and "know" what they can and cannot do, so that is where I would put the check for whether what is is trying to receive is valid or not for that object.
I think I'm getting a little confused with the relationships you've got set up. From looking at "add/edit" on the live demo, each exercise has multiple joints which has movement, plane, and symmetry, so wouldn't you set up your objects something like
exercise would then be composed of joint (which would have everything about each joint returned in separate joint objects).
joint would then be what you're calling your ExerciseJointRelationship, but you'd remove the exerciseID (as the joint doesn't need to know what exercise it is being used in, just that it has action, symmetry, plane, and which joint name it is)
If you keep joint as a separate object, it can live happily along not knowing anything about the exercise object, but the exercise object would know that it was composed of joints, and would call the joint gateway to add to its joint object array. I don't know enough about the action/symmetry/plane, but if there's any other relationship between them, you may even be able to create a movement object as well and have a joint be composed of multiple movement objects.
I think what you are describing points out exactly where a HUGE road block for me comes up. In my head, I am so familiar with the database tables themselves that I see one object as the relationship between the other tables. Like, I would never create a Joint object since all I ever care about is the joint ID / name. I never want it outside the context of a query in which case I would be returning many or all of the joints.
But what you are saying, such as getting rid of the ExerciseID from joint since Join doesn't need to know which exercise it is in, this just showcases the two very different view points from which we are attacking the problem.... very interesting.
Like, what I think you are saying is that I would have a "relationship object" of some sort that would compose an instance of a Joint object, Action object, Symmetry object, and Plane object. Then, the exercise would compose a collection of these objects. ... I think this makes sense if all you care about it objects.... but the problem is, I am not there yet; I still care about Foreign Key IDs :) I don't actually want an, "Action object", I want it's ID in the database.... again, I am chained to the implementation, not to domain modelling.
This will be a long journey :)
Definitely...I'm still very much a noob on this stuff myself. Up until about 2 months ago, I was writing everything procedurally, then had to switch to OO. At first I still thought about it like the database tables, then for some reason, everything just clicked. I think I started thinking about things as objects that contained other objects, then it just started to make a little more sense (and hopefully I'm not leading you astray with my example).
What you describe with the foreign keys is just like the collection of objects. You could almost think of it like this:
You have your exercise table filled with all of your exercises.
You have a joint table filled with all of your joints.
You have a movement table filled with all of your movements.
(this next bit is not at all normalized in dB terms, but it may make thinking about objects a little easier)
You then have another temporary "joint" table that has your exerciseId, joint information, along with your movement information, etc. all contained within it. You would have created this table based upon what the user clicked when filling out your add/edit form, and the data would all be filled from the separate tables (joint, movement, etc.). These would be one joint per row, that perhaps refer to exerciseId = 1.
You would then join this on your exercise table returning back results of an exercise along with all of the joints that go along with that exercise. The temporary joint table (minus the exerciseId) would be your "joint object array". The exercise would then be composed of multiple joints. To visually see this you could use a cfoutput group="exerciseId" and you'd get one exercise (the exercise object) and each of the joint "objects" contained within that exercise. Not really the best example, but perhaps from a data perspective it's somewhat close. I still prefer to think of it as an exercise that is composed of multiple joint objects, but that's only after I got to that "ah-ha!" moment :)
Hopefully that wasn't too confusing
Thanks for the explanation. I think I see what you are saying - I am just still not at that ah-ha moment.