I encountered this on some stuff I was doing. My general conclusion was that whenever I felt like I needed a class with a bunch of fields I was doing it wrong. Here's my thought process:
Problem:
- I need a large number of fields to hold this data
- All these fields require huge amounts of boilerplate
Solution:
- use reflection to reduce the boilerplate < "you are here"
- Use metadata to specify how the fields should be used
New Problems:
- Reflection is difficult to understand when someone new looks at the code
- Once you go meta enough to eliminate more boilerplate, the fields often have no mention in the code except through the metadata--why are they fields?
- Specifying the metadata in code becomes bulky quite quickly (easiest way is a string array, by the way)
Solution: Start storing data in a collection and Specify metadata in an external data file
New problem: Errors become hard to find
Be meticulous about error checking and very explicit with your error messages. Make sure any other programmers that might use your code read the error messages. Attempt to indicate when metadata is missing or wrong and how the programmer should update the metdata--include the location of the metadata file.
Problem: No type safety
Yeah, this became somewhat annoying at times. I ended up including type information in the metadata so that if someone put the wrong value in a field, it could be detected--essentially this moves type safety from build time to run time which was fine in my case.
Problem: The metadata is needed repeatedly throughout the life of the object
Rather than looking it up by name every time it's used, I'd parse the metadata at the beginning and put it in an object (call it an IntHolder). This holder would end up in the hashtable and it would contain the current value as well as a reference to the parsed metadata.
Example
Here's what my metadata would end up for one field of a style sheet:
FieldName: Age
FieldType Integer
ScreenBinding: AgeTextField
DBBinding: User.age
Validation: IntRange(0, 120); "Age is out of range"
The field name might be how it is displayed to the user or just for use in your program. In general, you shouldn't need to directly manipulate this type of data by name--but of course you do sometimes.
When you do need to use, use getInt("Age") and setInt("Age", 12) instead of getAge() and setAge(12)--slightly more verbose but not really a problem.
If it is a problem, you can make getAge/setAge helper methods and you never need to know it's not a field, but that does start piling on the boilerplate again.
FieldType: specifies how it's stored and lets you implement type checking.
ScreenBinding and DBBinding are used to copy the value into and out of other systems. I also used this type of mechanism to transfer the data from server to client and back.
The fun one is Validation. When pulling data off the screen it can be passed to a validator in a very progmatic way. The same validator can be used before committing to the DB.
Validators can do a lot more than that, they can act as triggers (if a value changes, do this) or actions (when user submits a screen, do this). These are a simple objects with the ability to take a value (through an interface)--they can be as flexible or powerful as you like but are not tied directly to any object except through the meta-data.
The only problem with this approach is you have to be the type of programmer that enjoys writing fixtures instead of error-prone boilerplate. (Yes, I find that the amount of time is about equal, but I tend to get really slow when I have to implement boilerplate)
After doing this a few times I really love the pattern, but it gets difficult to implement. Next time I do it I'm going to try to make it into a library of some type.