Source code, basically, is
the materialization of an idea, and the more complex the idea is the
more you see an explosion of possible ways to materialize it.
When
in the process of materializing an idea (coding), multiple competing
constraints emerge and try to influence the final materialized form.
Examples
of constraints are performance, readability, maintainability,
extensibility, time, elegance, typing economization and probably others
as well.
This happens even when writing something as simple as the declaration of the function.
inline?
virtual? pass arguments by reference? which arguments to pass? how to
name the function, how to name the arguments, implement inside the
header file?
each of those decisions can satisfy or break
constraints and there is most often no way to satisfy all of them, and
all we wanted is to declare a simple function.
One might
thank that choosing which arguments to pass is obvious, but many times
it is not, there are many variations and optional arguments that are
handled and the decision has to be made to code many 'interfaces' to
the function accepting slightly different arguments, this might satisfy
readability constraints, but violate maintainability ones as I will in
some examples at the end.
Moreover, the more the idea materializes, the more
important 'piping' becomes, and the exact declarations start to become
a big role, since nice 'piping' is also a very valid constraint. This
means that it is not even possible to design beforehand a 'perfect'
materialization, because the constraints we mention operate on the most
microscopic level, and this would mean that the perfect design would
need to go down to the lowest level, but then the design is the same as
the actual coding, which beats the purpose of a high level design.
So
all these decisions have to happen at the time of materialization
(coding) and as soon as one form is chosen, some constraints are
violated, this cannot be avoided, following the great wisdom of
Japanese Anime Black Lagoon II: 'when you choose, you loose
something', this is in fact unavoidable.
It is a really
annoying fact, the better one wants to satisfy all constraints the more
impossible it gets. This is a usual problem manifesting itself
everywhere really, science, philosophy, every day life, everywhere
really. One might say that achieving a balanced form which satisfies
most of the chosen constraints for the current context is optimal, and
yes this is what we usually strive for, but it is still, a compromise
and we still 'loose something'.
The
important thing however when it comes to coding is to be AWARE of the
chosen constraints and of any compromises that were made, and the
consequences of all the forms that were chosen.
Awareness puts us in control instead of being unknowingly controlled by
constraints sitting in our sub-conscious mind
(readability freak, micro-optimization freak, code elegance freak, ...)
and allows us to walk through the mine field with a headlight and a
baseball bat, instead of blindly stepping on the mines totally unarmed.
In
the examples, and yes they are almost ridiculous, I chose very simple
functions, and one might think that this is exaggerated, but one can
notice that at every example decision some constraint were broken in
the favor or others and hence a compromise was made.
Example1:
1. float dist(const Vector3 &a, const Vector3 &b);
nice and short, however, 'distance' is more precise, more readable, but needs more typing ...
2. float distance(const Vector3 &a, const Vector3 &b); //more readable
but it is more performant to determine the squared distance and it is all whats needed in many cases
float distanceSquared(const Vector3 &a, const Vector3 &b);
this is too much typing
float distSq(const Vector3 &a, const Vector3 &b);
this is too cryptic
float distanceSq(const Vector3 &a, const Vector3 &b);
but how to implement this?
inline float distance(const Vector3 &a, const Vector3 &b) { return sqrtf(distanceSq(a, b); }
but makes the header file look dirty, let the compiler do the inline optimization for us
float distanceSq(const Vector3 &a, const Vector3 &b); //implemented inside .cpp
Example2:
1. float distance(const Vector3& segA, const Vector3& segB, const Vector3& pt);
segA? what's that? too cryptic
2. float distance(const Vector3& segmentPointA, const Vector3& segmentPointB, const Vector3& point);
that cost more typing, but ok, now I have an Idea why don't we write it like this?
3. float distance(const Segment& segment, const Vector3& point);
let's make this cleaner and move it into the Segment class.
3. float Segment::distance(const Vector3& point);
but sometimes the code needs to compute the distance directly from 2 points, without having a segment,
but we still want this version, saves typing when dealing with segments.
4. inline float distance(const Segment& segment, const Vector3& point) { ... }
or
inline float Segment::distance(const Segment& segment, const Vector3& point) { ... }
now we need that squared version again
5. float distanceSq(const Vector3& segmentPointA, const Vector3& segmentPointB, const Vector3& point);
but now we need to maintain 2 functions that do the same because of decision 3, so now we need to add
6. inline float distanceSq(const Segment& segment, const Vector3& point) { ... }
that's all!!, but no wait, many times, we need to extract the closest point at the same time, ok, add
5. float distanceSq(const Vector3& segmentPointA, const
Vector3& segmentPointB, const Vector3& point, ector3&
closestPt);
but wait, we can merge the 2 to have less maintenance into
6. float distanceSq(const Vector3& segmentPointA, const
Vector3& segmentPointB, const Vector3& point, Vector3* pClosestPt = NULL);
but that costs the callers that don't need to calculate the closest point some performance.
doesn't matter, this is good enough.
there is one more thing though sometimes all we need is the resulting interpolation value 'u',
specially when using this implementation:
float u = ((point.x - segmentA.x)*(segmentB.x - segmentA.x) + (point.y
- segmentA.y)*(segmentB.y - segmentA.y) + (point.z -
segmentA.z)*(segmentB.z - segmentA.z))
/ ((segmentB.x - segmentA.x)*(segmentB.x - segmentA.x) +
(segmentB.y - segmentA.y)*(segmentB.y - segmentA.y) + (segmentB.z -
segmentA.z)*(segmentB.z - segmentA.z));
interpolate using u
no need to calculate the exact closest point... ok
7. float distanceSq(const Vector3& segmentPointA, const
Vector3& segmentPointB, const Vector3& point, float& u);
or maybe
8. float distanceSq(const Vector3& segmentPointA, const
Vector3& segmentPointB, const Vector3& point, float* pU = NULL);
or even
8. float distanceSq(const Vector3& segmentPointA, const
Vector3& segmentPointB, const Vector3& point, float* pU = NULL, Vector3* pClosestPt = NULL);
nah, too much checking NULL pointers, so better write many versions, but that's more annoying to maintain
9. ......................you get the point, and those are only declaration of simple functions................
Materialization of a Mine Field
April 17, 2008, 1:40 pm
Page :
1






