Data used for different purposes (i.e. different and exclusive most-common transforms/operations) will have a different form. Data should be designed to fit the transformations and constraints being applied for the problem. Two different problems imply two different data designs.
This is intuitive at a high level. Text is clearly not the best format for reading and executing an instruction stream, however it is a good format for editing and describing one. That's why we write code in text and yet it's compiled (transformed) in to something more suitable for the exclusive use (reading and executing). Even short-cycle interpreted languages are simply less monolithic versions of the same logic (e.g. small sections of modified text are transformed in to another format, like a VM code or JIT native code.) This is done because these are exclusive and independent uses of the same essential data (instruction description/stream) and different formats are better suited to the different uses, and different parts of the data are often exclusively relevant to one transform or the other.
At a high-level, the same pattern is applied to the game (e.g. data from maya, intended for general-purpose editing, is converted to a runtime format, which is intended for game rendering, which has different goals, transformations and constraints.)
You can imagine many more examples that fit this essential pattern:
Let me repeat this point: Two different problems imply two different data designs.
This is a fundamental of good data design. However, Object-Oriented Design (OOD) and Object-Oriented Programming (OOP) have perpetuated this misguided belief that there is an ideal
abstract data form where
all transformations are given equal weight*.
THIS IS GARBAGE.
- It doesn't match the reality of what's happening.
- It's not intuitive at all. (Mostly because it doesn't match any reality.)
- It's harmful (by pretty much any metric - memory, performance, complexity, etc.)
The idea that all transforms are essentially equal, and a data format is an opaque abstraction that provides the union of all information needed for all transformations equally, is just wrong. Nothing could be further from the truth, but the value of each transform, both independently and in exclusive groups, and the impact on the data design is not considered in traditional OOD. See also: Three big lies
* It doesn't matter whether or not this pattern or belief is inherent in OOD. The fact is that it is most commonly applied this way in practice. And you know it.
Take for example, this bit of code:
class AlignedBox
{
public:
Vector3 minimum;
Vector3 maximum;
bool seeded; <-- Take a moment: What do you think this is for? ...That field exists because these transformations (methods) have been included in this class:
Vector3 Test(Vector3 vertex);void Merge(const AlignedBox& box);void Merge(const Vector3& position);
You can see, for example from the Test method below that these transformations are designed to edit the AlignedBox. And that the "seeded" value is used simply as a test to see if an initial point has already been transformed by the method into extents of the box.
Vector3 AlignedBox::Test(Vector3 vertex)
{
if (!seeded)
{
seeded = true;
minimum = vertex;
maximum = vertex;
return vertex;
} if (vertex.x > maximum.x)
maximum.x = vertex.x; if (vertex.x < minimum.x)
minimum.x = vertex.x; if (vertex.y > maximum.y)
maximum.y = vertex.y; if (vertex.y < minimum.y)
minimum.y = vertex.y; if (vertex.z > maximum.z)
maximum.z = vertex.z; if (vertex.z < minimum.z)
minimum.z = vertex.z; return vertex;
}
Now without getting in to the data field seeded itself, at whether or not it's actually necessary (it's not at all - you can almost certainly know
in context whether or not you're creating a new AlignedBox. Not only that, it implies that points are being added arbitrary and individually with no consideration of
their data design. Which
in turn implies that the data design of the parent, or calling system is poor.)
Note another set of methods that exist in this class:
bool IntersectsSphere( const Vector3& pos, const f32 radius ) const; bool IntersectsBox( const AlignedBox& box ) const;Clearly these two sets of transformations are independent. The pattern should also be familiar (see diagram above). The data is "edited" by the Test and Merge methods, and is later transformed by the Intersects* methods. Certainly we may expect the AlignedBox to be edited again in the future, but in the same way that we expect code to be re-edited. These are exclusive and independent data transformations, and as such, we should expect their solutions (data forms) to be different, depending on their most common uses.
In fact if you looked at the data usage for the Intersects* methods in the system, you would very likely be able to reduce the most common pattern in to cases like the following:
And the data design you would create for solving this problem would be very different from the design you would create for solving the editing problem above. As it is with text and code.
Most people do intuitively know there is something wrong with this design. Although they may not know what. Take the Intersects* methods again, for example.
This function is completely defined in the cpp file, in that it actually does the transformation/test.
bool AlignedBox::IntersectsSphere( const Vector3& pos, const f32 radius ) const; However, can you guess what
Sphere::IntersectsAlignedBox looks like? How to handle these kinds of cases invariably leads to a confused OOD. Why?
Because the model makes no sense. The fact of the matter is that these methods do not belong in this class because this is not the class of data that they act upon. The only "class" that these transformations belong to are a class of data that match their transformation patterns (which may look like the image above, or whatever the most common context they're applied in the application happens to look like.)
But note, even in the above case you would be careful not to over-generalize the data pattern, or you would simply end up switching on every pair of data types possible in the input streams. You need to analyze the data itself to see which pairs are likely combinations and should be handled as such. It's exceedingly unlikely that all pairs are equally likely and that no similar pairs are ready to be transformed within the same latency window.
In a similar way, AlignedBox also includes the following methods:
void GetVertices(V_Vector3& vertices) const;
static void GetWireframe(const V_Vector3& vertices, V_Vector3& lineList, bool clear = true);
static void GetTriangulated(const V_Vector3& vertices, V_Vector3& triangleList, bool clear = true);Which belong to an entirely
different class of transformations! To which you would need to ask similar questions - How are the transformations applied? When are they applied? What are the latency constraints? What is the necessary throughput? etc.
While it may be said that I "cherry-picked" a particularly poorly designed example to pick on, the flaws themselves and the lack of consideration for data flow and design, are very typical. And there is a lot you can infer about the system as a whole by just looking at the data design of a single part.
Mike.