Congratulations! You have just circumnavigated the programming language / type system globe, arriving on the other side of the world from whence you departed. You have just landed on the border of dynamic language / prototype-based object land!
Many dynamic languages (e.g. JavaScript, PHP, Python) allow one to extend or change object properties at runtime.
The extreme form of this is a prototype based language like Self or JavaScript. They
do not have classes, strictly speaking. You can do things that look like class-based, object-oriented programming with inheritance, but the rules are greatly relaxed compared to more sharply-defined, class-based languages like Java and C#.
Langauges like PHP and Python live in the middle ground. They have regular, idiomatic class-based systems. But object attributes can be added, changed, or deleted at runtime--albeit with some restrictions (like "except for built-in types") that you don't find in JavaScript.
The big tradeoff for this dynamism is performance. Forget how strongly or weakly typed the language is, or how well it can be compiled down to machine code. Dynamic objects must be represented as flexible maps/dictionaries, rather than simple structs. This adds overhead to every object access. Some programs go to great lengths to reduce this overhead (e.g. with phantom kwarg assignment and slot-based classes in Python), but the extra overhead is usually just par for the course and the price of admission.
Getting back to your design, you're grafting the ability to have dynamic properties onto a subset of your classes. A Product
can have variable attributes; presumably an Invoice
or an Order
would and could not. It's not a bad way to go. It gives you the flexibility to have variation where you need it, while remaining in a strict, disciplined language and type system. On the down side, you are responsible for managing those flexible properties, and you'll probably have to do so through mechanisms that look slightly different from more native attributes. p.prop('tensile_strength')
rather than p.tensile_strength
, for instance, and p.set_prop('tensile_strength', 104.4)
rather than p.tensile_strength = 104.4
. But I've worked with and built many programs in Pascal, Ada, C, Java and even dynamic languages that used exactly such getter-setter access for non-standard attribute types; the approach is clearly workable.
By the by, this tension between static types and a highly varied world is extremely common. An analogous problem is often seen when designing database schema, especially for relational and pre-relational data stores. Sometimes it's dealt with by creating "super-rows" that contain enough flexibility to contain or define the union of all imagined variations, then stuffing any data that comes along into those fields. The WordPress wp_posts
table, for example, has fields like comment_count
, ping_status
, post_parent
and post_date_gmt
that are only interesting under some circumstances, and that in practice often go blank. Another approach is a very spare, normalized table like wp_options
, much like your Property
class. While it requires more explicit management, items in it are rarely blank. Object-oriented and document databases (e.g. MongoDB) often have an easier time dealing with changing options, because they can create and set attributes pretty much at will.