Udostępnij za pośrednictwem


Well-designed libraries

It is very rare that we come across a well-designed library every day, and even rare that we get a chance to work on them on daily basis. While it is hard to define what makes any given library "well-designed", it is rather easy to identify what is not. However, there are few exceptionally good libraries that from the moment you lay your hands on give you the feeling "…this has quality…"

Of course, there are unbeatable whole complex operating systems that were well-designed in the past and give unmatched performance even for today, which are becoming rather rare artifacts these days.

However, if you happen to work on Maya API, then more often than not you get that "…this is a good design, after all…" feeling. To see what this means, consider the following:

MStatus MDagPath::set (const MDagPath &src)

This is just a typical function in the MDagPath class that sets the DAG Path of the current object equal to the specified DAG Path. It returns an MStatus value indicating success or failure (or any other meaningful value within that context). There is nothing much complex or strange here, as this is a typical pattern in Maya API that almost every method accepts few parameters of interest and return status value indicating the success or failure of the operation. However, contrast it with the below method from the same class.

unsigned int MDagPath::length (MStatus* ReturnStatus = NULL) const

This method determines the number of DAG Nodes in the Path not including the root. However, what is of more interest for us here is not what this method does, but how it is defined. Note the MStatus now made as an optional parameter instead of return value, while the return value is position is used for a more meaningful value (the actual length, which is of more interest to the programmer in that context).

This subtle but important difference demonstrates how the library was designed to "adapt" itself to meet the priorities of the programmers/programming. MStatus values are an essential luxury provided by the API – some might need it, some may not. Thus they are made secondary citizens, always taking a less prominent place, perhaps as an optional parameter or a return value that can be always ignore when not required, while the primary concentration is given to the actual values that make more sense in the given context.

If you are wondering why this is so important, contrast this design with that of few libraries that demand status or result values to be return values always. While using them, you often find yourself saying "...huh…I have to declare a variable now to get that return value…"

A good library is one that minimizes those sighing moments.

Convenience matters apart, what is the afore-mentioned design really useful for? Can we get something more practical out of it?

Consider the following macro definition.

 #define POPULATE_PROPERTY(hWndListView, FnObj, Prop)  \
{\
    WTL::CString PropStr(OIL::ToString(FnObj.Prop()));   \
    \
    LVITEM lvi = { 0 };       \
    lvi.mask = LVIF_TEXT;  \
    lvi.iItem = MAXINT;        \
    lvi.pszText = _T(#Prop);   \
    lvi.iItem = ::SendMessage(hWndListView, LVM_INSERTITEM, 0, (LPARAM)&lvi);  \
    \
    lvi.iSubItem = 1;     \
    lvi.pszText = (LPTSTR)(LPCTSTR)PropStr;            \
    ::SendMessage(hWndListView, LVM_SETITEM, 0, (LPARAM)&lvi); \
}

Invoking the above macro with a call like below should result in the name of the method and its return value get populated in a list-view (similar to a property grid).

 void PopulateKDagNode(HWND hWndListView, MObject& obj, bool bPopulateBaseClass /*= true*/)
{
    if(bPopulateBaseClass)
        PopulateKDependencyNode(hWndListView, obj, bPopulateBaseClass);

    MFnDagNode DagNode(obj);

    POPULATE_PROPERTY(hWndListView, DagNode, parentCount);
    POPULATE_PROPERTY(hWndListView, DagNode, childCount);
    POPULATE_PROPERTY(hWndListView, DagNode, inUnderWorld);
    POPULATE_PROPERTY(hWndListView, DagNode, inModel);
    POPULATE_PROPERTY(hWndListView, DagNode, isInstanceable);
    POPULATE_PROPERTY(hWndListView, DagNode, isInstanced);
    POPULATE_PROPERTY(hWndListView, DagNode, fullPathName);
    POPULATE_PROPERTY(hWndListView, DagNode, partialPathName);
    POPULATE_PROPERTY(hWndListView, DagNode, transformationMatrix);
    POPULATE_PROPERTY(hWndListView, DagNode, isIntermediateObject);
    POPULATE_PROPERTY(hWndListView, DagNode, objectColor);
    POPULATE_PROPERTY(hWndListView, DagNode, usingObjectColor);
}

That's the complete listing of all "properties" of the given DagNode. This is introspection, which is made easy and elegant only because of the afore-discussed design. (The OIL::ToString() is a templated method from the Object Introspection Library https://sourceforge.net/projects/oil).

This is how a good designed library can make it possible to have it used for more than what it is designed for.

The introspection reminds me of one more library that has these "well designed" components in it: OpenSceneGraph (https://www.openscenegraph.org/). The Array class in it has an enum Type that identifies the type of the object being held inside it.

 class OSG_EXPORT Array : public Object
{
    public:    
        enum Type
        {
            ArrayType = 0,
            ByteArrayType     = 1,
            ShortArrayType    = 2,
            IntArrayType      = 3,
            UByteArrayType    = 4,
            ...
         }
    ...
}

While we are thinking we could have used templates, below that class we find a templated version:

 template<typename T, Array::Type ARRAYTYPE, int DataSize, int DataType>
class TemplateArray : public Array, public std::vector<T>
{
   ...
}

Further below, we find few typedefs.

typedef TemplateArray<GLfloat,Array::FloatArrayType,1,GL_FLOAT> FloatArray;

typedef TemplateArray<Vec2,Array::Vec2ArrayType,2,GL_FLOAT> Vec2Array;

In the end the array uses templates after all, but in a way that is easy for introspection. More complete details of this can be found inside the include\osg\Array header file.

One more elegant property of Maya API that we can observe is its ability to withstand mistakes (let's call it fault-tolerance). That is, we can invoke a method on a non-compatible object without bringing down the application or system. In a 3D rich world where objects that have similar properties are abounding (such as plane, surface, sphere, circle etc…) it is easy to take an object and make a method that is not suitable for that object (for e.g. trying to find the "volume" of a circle from its radius). Maya API allows this, without resulting in any adverse effects. It is very rare to see this kind of "fault tolerance" in libraries. (If you are interested in knowing how one could implement this kind of fault tolerance in one's own library, please refer to the implementation of Movie Creation Library. It uses C++ function pointers to inhibit any of its functional errors from affecting the hosting application. This allows the host application to run smooth as usual despite any errors in the movie creation.)

Of course, this doesn't mean the above discussed libraries are "the best" and cannot be made better. Nope. On the contrary, this only point to the strengths of those libraries, (while carefully ignoring weakness, if any,) that any new library designer and developers should carefully consider laying their foundations on for their own designs. Every system will have its own weaknesses – but it is those systems that have them the least win the day in the end.

Comments

  • Anonymous
    February 28, 2008
    It is very rare that we come across a well-designed library every day, and even rare that we get a chance