Prevention

 < Day Day Up > 



When we talk about preventing NIH Syndrome, there are two main goals. The first goal is to eliminate the automatic reaction to rejecting externally developed code and libraries. The second goal, which is essential to achieving the first, is to reduce the risks of using third-party code and libraries by adopting methods for handling integration and problem fixes smoothly. Once these goals are achieved, you can save development time by taking advantage of work that has already been done.

What Are Middleware and Open Source?

The best place to start is by describing what we are encouraging you to use. So far, we have been using the terms third-party libraries or external code to describe what those suffering from NIH Syndrome are trying to avoid. This was sufficient for discussing the symptoms, but to understand how to prevent decisions being made while suffering from NIH Syndrome you need to understand what decisions are affected. Boiled down to the core, we are discussing the adoption of any code written by a programmer other than yourself for use in your own code. There are, however, many forms this code can take. Each form requires slightly different handling and consideration.

The first differentiation is a common and important one: money. One choice is to go with a commercial solution often referred to as middleware, and to pay to use other developer’s code. The other choice is to use free code, be it open source available to everyone or code written by someone else within the same company that is available to members of the company freely. The cost of the product does not necessarily indicate the quality, but there is a distinguishable difference between the best commercial middleware and some college student’s freely available side project. The other big difference between commercial and free code, which we will talk about next, is the level of support that is given.

The next distinction of note is whether the source code is fully available or just the interface along with binary libraries. This has a large impact on the way the external code is integrated and maintained within your project. We will talk more about decision-making research into these and other differences shortly, but it is obvious that having the source code available offers more flexibility in integration and maintenance. Although less important, there is also a distinction between low-level libraries, high-level APIs, and application frameworks. We will go into detail about each of these as we progress through our discussion.

Technical Support

Problems will happen, but if you are properly prepared, they can be handled smoothly. This makes the level of support for third-party code an important consideration when there is a choice between different sources. There are many levels to technical support (Table 3.1), and we will go through them from least desirable to most desirable. This progression often corresponds to the expense of the third-party code and libraries, but not always. Also, note that there are other factors involved in making a decision.

Table 3.1: Levels of Technical Support

Type of Support

Description

Value

Typical Cost

No support

The library is completely your responsibility.

None

Free

Source available

Access to view and, preferably, edit the source code.

High

Varies widely

Community support

A community of people available for questions and some requests.

Moderate

Free

Developer contact

Contact with the developer for answering questions.

High

Moderate

Developer requests

The right to request bug fixes and features from the developer.

Excellent

High

The least desirable level of support is none. With this, you end up with the code as is, requiring any future modification to come from you alone. This does not mean that the code is useless, just that if there is a choice with more support that is not substantially more expensive then that is the choice to favor. You must also be careful when integrating the code into your code, particularly if the source is not available to fix problems if they occur.

The next step up in support is community-level support, which is a common form of support for open source projects. This type of support is useful primarily if the code and libraries have a large user base. Communication is achieved through a message board or mailing list, and is therefore unpredictable. However, if the community is large enough, there is a better than average chance that another member of the community has already encountered and fixed a problem you are working on. It is a good idea to check out the message board or mailing list before adopting a product with only this level of support in order to see what level of response you can expect. In addition, try to find out if the developers of the code and libraries are regular posters to the message boards or mailing lists. This is a decent level of support when the community is reasonably large.

Another step up in support is direct contact with the developer of the third-party code, which is common for commercial libraries. Optimally this would give you a line of communication to the programmers who are working directly with the code, but this is often not possible. You should at least check to make sure the technical support team consists of programmers and software engineers, since you do not just want someone reading from a script. The best asset to have is access to other programmers who have used the same developer. Question them on the responsiveness and level of support they received. Failing that, see if you can get an evaluation period with the code and come up with some questions pertinent to your project. You can then test the technical support with the questions to see if they meet your expectations.

Finally, we come to the most desirable level of support, and therefore the least common: the ability to request changes to the code or library directly from the developer. As with developer question and answer support, you should determine the level of competence of the developer. With a competent developer, this level of support provides you with extra programmers to solve the subset of problems related to the third-party code. It is necessary to go beyond this, however, and determine what support and additional implementations the developer is legally bound to deliver to you after initial purchase. Remember, we are not talking about requesting a feature for a future implementation, but a direct and timely implementation of a requested feature to meet your deadlines. Be sure both parties understand the exact obligations in order to properly assess the risks involved, and perform necessary adjustments to the schedule.

Research

The best preventative measure to take that will both reduce risk and assuage your personal worries is research. The more information you have available to make the final decision about using an external library, the more comfortable and sure you will be when you do make it. As with almost any activity in software development, there is a danger of becoming mired in continuous research. Therefore, you must strike a balance between the amount of time you spend researching versus the risk of adopting a technology and throwing it away if it does not work. Generally, you will find that this leaves you with plenty of time to perform research.

So, what should you research? Technical support, as we just discussed, is perhaps one of the most important considerations, but certainly not the only one. Sharing the top status with technical support is the applicability of the code or library to your project. Before you can properly research this, you must determine the functionality that you require. Look over your current project plan and group similar technologies together. You might end up with some technologies in multiple groups, which is fine in the research stage. Just remember not to use more than one of them in the final implementation. For each of these groups, make a detailed list of needed functionality, and then go back over it and prioritize the list. Next, determine the functionality most likely to change and put that on a separate list. At this point, you are ready to look at the features of the code or library you are considering adopting. There usually exists a feature list, and even better, sometimes the interface is published. Compare this and mark any missing or vague items on your list. Follow up with an e-mail to the developer asking about these points, which is also a good test of their responsiveness. If the functionality meets your needs or is close enough that you can adapt to it, you should add it to your possible solutions for that problem. Step away from it for a short period before coming to a final decision. In addition, there are a few more considerations to add to that final decision.

The next item to check is whether you can obtain the full source code or only the interface and binary libraries. If you obtain the source code, then even if technical support fails you still have options available to fix the problem yourself. Without the source code, you might still be able to find a workaround, but it will not be as easy. With the source code, it is not only much easier to find a workaround, but you can also change the source when an external workaround is not available. Another advantage occurs if you can obtain at least part of the source code during evaluation. This is another indicator as to the technical experience of the developer. In summary, the lower the support level the more you need the source code, but even with the highest support level there are still benefits to having the source code.

start sidebar
Go to the Source

One project was using network code from another party when they ran into a problem. One of the library functions was leaking memory, thereby causing the application to fail after only a few calls to that function. After a day of useless investigation, one of the programmers decided to ask if the source code could be obtained. It was provided readily and, within an hour, the problem was located. Another hour was all it took to inform the original developer and update the library. The lesson taken from this is to obtain the source code with the library, if it is available. This can save you valuable time if problems occur.

end sidebar

Finally, you will also want to look at the competence of the developer from an overall perspective. The best approach to this is speaking with customers who have used the code or library directly. While it is also useful to read message boards and reviews, nothing beats a direct one-on-one conversation. Do not be afraid to ask the developer for references to others using their product, but try to find some on your own as well as the developer choices might be biased. A large user base is a usually a good sign, and it also makes finding other users to talk with much easier. Additionally, if the developer has been around for a considerable amount of time they are likely to be more experienced as well as have a larger user base.

After investigating all the relevant information, you should weight it according to the requirements of your project. At this point, you should also be able to assess the risks involved and the corresponding rewards for taking these risks. Put this all together and make a final decision.

Flavors: Types of Reusable Code

It is important to understand the different types, or flavors, of reusable code that you will encounter. Each type has its own particular uses and corresponding advantages and disadvantages. What follows are the most common types, but do not be surprised if you encounter others on occasion.

Snippets

Code snippets are small pieces of code that are useful but not easily categorized and placed into a library. These tend to come from one of several main sources and are usually free. For example, you might have written a small template that deletes and sets a pointer to NULL in C++:

   template <typename t_Type> void delete_and_null(t_Type &io_pointer)    {       delete io_pointer;       io_pointer = 0;    }    

This does not really fit in any library that you have created, so you just bring the small code snippet over to your next project. This might continue through several projects.

Another source of code snippets is fellow programmers. These snippets are generally passed around when one programmer hears another talking about how to solve a problem. The programmer might then offer a small code snippet that he used to solve the same problem.

Finally, there are a couple of public sources for code snippets. Magazine and Web articles will have some useful code snippets. There are also online databases, such as www.sourcebank.com, that have searchable repositories of code snippets. Both these sources are worth checking if you are looking for a small piece of code. Be sure to check if any copyrights or licensing applies, but because of the small size, these snippets do not usually have these requirements.

Because code snippets are small single purpose chunks of code, they are generally easy to integrate and remove when necessary. This makes them a very low risk for reuse. The one caveat is that they should still be well documented in the project documentation to avoid duplication of functionality by other team members. Look at the CAP Epidemic to learn more about this danger and how to avoid it.

Standard Libraries

When we talk about using external code, we are not referring to higher-level languages. However, we are referring to the standard libraries that are available for those languages. As with code snippets, these are usually free and place no restrictions on their use. Even better than code snippets, they tend to have a larger user base from the language community for testing, debugging, and feature requests. This makes them robust libraries that are generally useful for practical problems. Because of their availability and large community support, it is also easy to transfer code written using the standard libraries between projects, since the other project can be expected to use the standard library. Another advantage with many of the standard libraries is the tendency for their creators to be experts in the particular language for which the library is designed. Disadvantages tend to come in the support area since there is generally no dedicated support team, but the community support is very good and with all the other advantages makes adopting standard libraries a must for most languages.

Other Libraries

Third-party libraries that are not part of the standard language are where most decisions regarding using external code will have to be made. These range greatly in purpose, quality, cost, and support. This is where you must perform diligent research in order to decide if you want to adopt a library for use with your project. The common format is a set of interfaces to an internal implementation of the libraries’ function. Depending on the provider, you might or might not have the source code for the implementation. It is generally better if you can obtain it, but not required.

Properly incorporating the library into your development process is very important. Make sure the documentation for the library is available to the entire team. A good way to do this is have an internal documentation Web site with both the project documentation and documentation for all external libraries and tools. Additionally, the libraries should be added to the build process transparently, or in other words, without making it more complicated. This is easiest if you have one person responsible for integrating libraries into the build process. Once this is accomplished, you can begin to use the library within the project code. Later we will talk about safe ways of using library code within your code base that minimizes the risk of later problems.

Frameworks

Frameworks differ from libraries primarily in where they integrate into program flow. Unlike a library, which your code calls, a framework calls your code. If you do decide to use a framework for your application, chances are that you will only use one framework for the main project. However, some user interface components, such as dialog boxes, might act as miniature frameworks by using callbacks into your code. This means that you can think of the user interface as a framework as well. Many times, the main framework and the user interface framework are the same framework.

Unlike libraries, frameworks are tightly integrated and often hard to remove. They also require early adoption to be of use. Frameworks should be given the most research time, as they can cause the most problems if you change your mind. Because you have less control over the flow of the application with an external framework, it is more important than ever to obtain either source code or the highest level of technical support.

start sidebar
Microsoft Foundation Classes

One very common framework is Microsoft Foundation Classes (MFC), which has been used in many applications. The MFC framework uses a Document/View structure for the majority of its applications. This breaks the application into separate data and user interface modules. With the help of a project creation tool in Microsoft Visual Studio, the basic classes are created with slots for the user to add application-specific code to each of these modules. The framework handles many common tasks with only minimal implementation required by the programmer using it. A plethora of other supporting classes is provided to aid in the creation of the application.

While MFC is great for certain types of application, particularly small tools requiring user interfaces, it does show some of the common limitations of frameworks. MFC removes the need to implement common tasks, but if small changes are required to these common tasks, it is often difficult to achieve them without considerable work. Sometimes it is even necessary to re-implement certain parts of the framework to achieve these minor modifications.

Another difficulty encountered with frameworks is in integrating them later in development. A project had created an editor tool for editing application-specific configuration files. The tool was originally implemented with the standard Windows libraries, but a suggestion was made to update it using MFC to make new feature implementation easier. Unfortunately, the amount of work that would have been required to update the current tool would have been as much or more than writing a new tool from scratch. This course of action was therefore abandoned.

end sidebar

In the case of frameworks, often the user must adapt to the framework more so than adapting the framework for the user’s specific purposes. To make this clearer, let us look at one such framework that is a choice available for doing development for cell phone applications. This framework is an essential part of the Java2 Micro Edition (J2ME), and provides both an application and user interface framework. Let us start with a discussion of the application framework. In order to create a J2ME application, you must derive at least one class from MIDlet and implement three abstract protected methods. This means that your class must have the following signature:

public class MIDletImplementation extends MIDlet { protected void startApp() throws MIDletStateChangeException; protected void pauseApp(); protected void destroyApp(boolean b) throws MIDletStateChangeException; } 

Of course, for the application to actually work, the methods must be implemented. Now the environment that this application is run on, be it an emulator or a cell phone, makes calls to the three functions according to a certain set of rules. To initiate the application, startApp is called first. After that, the framework might call pauseApp or destroyApp at any time. If the application is put into the background for some reason, pauseApp will be called. When the application is then subsequently brought into the foreground, startApp will be called again. Notice that the framework leaves it up to the implementer to determine if startApp was called for the first time or to reactivate from a paused state. When the application is terminated from a source external to the application, destroyApp is responsible for handling this. The argument provided to this function gives the implementer the opportunity to prevent the destruction in some cases. Again, the implementer must adapt to both cases.

Now let us look at a sample application, which is somewhat contrived for the purposes of simplicity:

public class MIDletImplementation extends MIDlet     implements CommandListener, Runnable {     protected void startApp()         throws MIDletStateChangeException {         List list = new List("Menu", List.IMPLICIT);         list.append("Start", null);         list.append("About", null);         list.append("Exit", null);         list.setCommandListener(this);         Display.getDisplay(this).setCurrent(list);     }     protected void pauseApp() {         Display.getDisplay(this).setCurrent(null);     }     protected void destroyApp(boolean b)         throws MIDletStateChangeException {     }     public void commandAction(Command command,         Displayable displayable) {         switch(             ((List)displayable).getSelectedIndex()         ) {             case 0:                 new Thread(this).start();                 break;             case 1:                 Display.getDisplay(                     this).setCurrent(                         new Alert("MIDlet Example"),                         displayable);                 break;             case 2:                 try {                     destroyApp(true);                 } catch(                     MIDletStateChangeException e                 ) {                     System.out.println(                         e.toString());                 }                 notifyDestroyed();                 break;         }     }     public void run() {         Alert alert = new Alert("Running...");         // 1000 milliseconds = 1 second         alert.setTimeout(1000);         Display.getDisplay(this).setCurrent(             alert,             Display.getDisplay(this).getCurrent());     } }

This is a complete application, but you might have noticed that commandAction and run are never called. This is because they both represent calls made by the J2ME framework rather than by the implementer. To understand how these are used, let us look at the following line first:

list.setCommandListener(this); 

This tells the framework that this object will be handling actions taken by the user on the user interface element list. This means that the display and some interactions with the user interface are handled by the framework, leaving the implementer with control only when certain actions are initiated that cause a call to commandAction.

The run method is eventually called after the following line is executed:

new Thread(this).start();

Although the implementer has more control over this, the exact timing of the call is controlled by the framework scheduler, or in some cases, the framework might defer to a hardware scheduler. As you can see from this example, the implementer must respond to requests from the framework to handle application-specific functionality. In addition, the user can only exert a certain amount of control over how the framework behaves. Frameworks can be very useful, but it is important to know their limitations even more so than for third-party libraries.

Active Libraries

Although optimization should not be a major concern until it is necessary to optimize, there is certainly no reason not to use a library with better performance if it offers the same features and ease of use that another, slower library offers. Since you are unlikely to modify or maintain the external library yourself, the concerns about Premature Optimization are not as justified when determining which library to use. Because of this fact, active libraries are a new concept that can be useful to understand when choosing a third-party library. However, do not use this line of reasoning to discount third-party libraries altogether, only to choose between different options.

Active libraries are part of the concept of generative programming, which is discussed in more detail in Chapter 2, “CAP Epidemic.” Here we are concerned only with the active libraries themselves. An active library is essentially a code generator rather than a fixed code library. This means that an active library generates code specific to the feature requests for an application at compile and link time, rather than determining what code to execute at run time. This provides several optimization advantages, including better performance due to reduced need for conditionals and often less memory due to the inclusion of only code that is necessary for the final application. Assuming the active library provides the same features, they are the preferred choice over fixed code libraries. In fact, because of the reduced code size of the final application-specific library, active libraries can provide more functionality without the associated risk of an overly bloated binary library.

While much of generative programming represents technology that still has a considerable amount of time to mature, active libraries can be implemented in some languages with current techniques. In particular, C++ templates offer a method of creating active libraries that require no extensions beyond the language standard. Templates can be used to execute algorithms at compile time through a variety of techniques that fall under the moniker of template meta-programming. Excluding the limitations of the compiler, template specialization allows the use of conditional compilation and looping code generation through recursion. This forms a Turing complete language just like C++ that in principal can perform any algorithm, except templates can do this at compile time. Due to compiler limitations and the higher complexity of writing templates, this ideal cannot be fully realized. Nevertheless, enough is possible to allow the creation of template libraries that generate code at compile time, making them active libraries as well. Therefore, if you are evaluating libraries, look on template meta-programming as providing an advantage over run-time libraries. If you are writing a library, consider using template meta-programming over conditionals to capture feature variations that only need to be distinguished at compile time and not run time.

To help show the flexibility that active libraries can provide, here is the definition of a debugging flag class that could be part of just such a library:

   /**   Generic policy-based debug flag template class.     *   This is a customizable flag intended for  *   use during debugging. A disabled version that  *   always returns false is provided for     *   release builds.  A decent optimizing compiler  *   will eliminate code that is surround by the  *   false statement in a release build.     *     *   @param   init_policy   - Initialization  *            policy class type.     *   @param   assign_policy   - Assignment policy  *            class type.     *   @param   storage_type   - Storage type.     *     *   @par Initialization policy must provide:     *   @li      typedef <I>type</I> type;<BR>     *         This is the argument type, so make  *         it a const reference if it is a  *         complex type and performance     *         is a concern.     *   @li      static storage_type  *         convert_to_storage_type(type);     *     *   @par Assignment policy must provide:     *   @li      typedef <I>type</I> type;<BR>     *         This is the argument type, so make  *         it a const reference if it is a  *         complex type and performance     *         is a concern.     *   @li      static storage_type  *         convert_to_storage_type(type);     *     *   @par Storage type must support:     *   @li      conversion to boolean     */    template <       class init_policy      = boolean_flag_policy,       class assign_policy      = boolean_flag_policy,       typename storage_type   = const bool    >    class flag    {       // ...    };

This class is meant to represent a boolean flag that can be initialized in a customizable manner. To understand how this works, let us look at the init_policy and assign_policy. These two template parameters can take any class that follows the interface guidelines specified in the comments. This allows the initialization function for this class to be written as follows:

   /** Create new flag.     *   @param   i_initialState - Value to be  *            converted to initial state.     */    explicit flag(init_type i_initialState)       : m_state( init_policy::convert_to_storage_type( i_initialState))    {    }

Because the initialization is provided as a template parameter, no object creation or branching is required at run time. This amounts to the same effect as if a code generator had created the required code based on a feature description provided by the user. By predefining the most common policies, the library can offer easy generation of different flags by specifying the features required as template parameters. For example, the simplest policy is to emulate a standard boolean assignment:

      /// Default boolean policy for use with /// debug flag.       struct boolean_flag_policy       {          /// Policy type used by template.          typedef bool type;          /** Conversion function from policy type  *  to storage type. In this case there  *  is no conversion so the value is just           *  passed along.           *  @param   i_value - Value to convert  *            to boolean.           *   @return   Boolean result of conversion.           */          static inline bool convert_to_storage_type(type i_value)          {             return(i_value);          }       };

This is just a small sample of the flexibility that an active library can provide without sacrificing flexibility or performance.

Expanding the Selection with .NET

Programmers tend to favor a particular language for their development purposes. Reasons vary greatly, from the language being the first they learned to a careful evaluation of their needs and programming style. For many, their adherence to a particular language is almost religious in nature, with their own little wars occurring on the message boards and at conferences. Despite this fervor, these programmers have many valid reasons for using one language or another. The real problem is that these languages do not communicate well, and what little communication that does exists must be generated from scratch for each language combination. This means that libraries written in one language are useless to another language, and although not directly caused by NIH Syndrome, it leads to the duplication of effort that is symptomatic of NIH Syndrome.

The introduction of .NET by Microsoft offers a solution to this problem. While it will probably not put an end to the language wars, it does make available a peaceful channel of trade between the warring parties. A library written in any .NET-compliant language can be used by any other .NET-compliant language, with all the work concentrated in making the language .NET compliant. There is no need for custom programming to integrate with each individual language. This, along with a well-formed object model, allows sharing between languages with ease previously unavailable.

The details of .NET require a book in their own right; in fact, many already exist, so we will only take on the task of how this affects NIH Syndrome. The most important impact is the expansion of available third-party libraries that are coming into existence or being updated to meet compliance with the .NET standards. This should provide an unprecedented level of code reuse and third-party library support, but there are still some roadblocks to this new technology. Once again, NIH Syndrome rears its ugly head as many programmers reject .NET technology out of fear.

What are these fears, and how realistic are they? There are two primary fears, both of which are overemphasized by most programmers. The usual suspect is here in the form of performance concerns, and as before, the same rules of avoiding premature optimization apply. In fact, the opposite is true, as .NET can provide a clear development advantage in this area that was previously not available. The application can be written in a safer and easier development language with the corresponding increase in development time, then particular modules that represent performance bottlenecks can be rewritten in a more performance critical but harder to use language to meet performance goals. This is facilitated by .NET because interfacing the two languages is relatively easy.

The other common fear is that .NET reduces all compliant languages to a single common denominator, essential forcing every language into one. This is a misunderstanding of .NET’s purpose that dissuades many from its use. The real purpose of .NET is to provide a common interface between languages that allows the communication of higher-level constructs such as objects to survive. Behind this interface, the language might exist as it always has and take advantage of any constructs unique to that language. Additionally, some constructs, such as multiple inheritance, can be mapped to the .NET language model with only minor concessions. The requirements of .NET are only that the accessible pieces of the language be mapped by the compiler to the .NET model. Thus, languages such as Eiffel remain largely intact even after integration into the .NET environment.

As with any technology that interfaces between different models, there is overhead to this connection. However, as with other performance concerns, this is unlikely to be problematic in more than a few instances. These instances can then be handled at the end of the project in the optimization phase. This will usually involve the shifting of some responsibility between two different languages and should not present a real problem.

The final and most valid concern about adopting multiple languages lies in support. This can be handled in several ways. A team of individuals working on closely related aspects of a project should stick to one language, as their code is likely to intersect often. If the entire team is just a few people, the entire project code base should stick to one language. However, on large teams that are broken into smaller self-contained teams, it is possible for the teams to develop in different languages as long as the interface and responsibilities are clearly defined. No matter what the size of the team, third-party libraries from any language should be considered. However, the one caveat is the need to more carefully consider support for these libraries. In this case, even if your team can obtain the source code, it must have members who understand the source language; otherwise, it is necessary to look for an increased level of support in case of problems.

Overall, the introduction of .NET should expand the range of libraries available across the software community. However, there is another concern shared by many in the software community, particularly open source developers. Because the .NET technology is proprietary to Microsoft, there is the fear of the level of control that will be available in the future. Before we analyze this fear, it is important to understand some issues about .NET and how these relate to NIH Syndrome. For Microsoft, the .NET initiative represents a large number of changes including corporate branding. We are, however, really only concerned with the portion that facilitates sharing of object libraries between different programming languages.

This communication between programming languages, and the ability of compiler writers to adapt to .NET, is made possible by a set of open standards called the Common Language Infrastructure (CLI). For a language to interact with .NET, it must support a mapping to the Common Language Specification (CLS) and .NET object model. This is achieved by an intermediate language (IL), which can then be run on an interpreter or virtual machine (VM). The IL might alternatively be compiled either ahead of time or with a JIT (Just In Time) compiler.

We can now return to our concern about the proprietary nature of this technology. Because a valid .NET language must compile to the IL, which follows an open specification, then it is possible to create a VM and JIT compiler that can use the IL. Just such a project is currently in progress under the moniker of Mono sponsored by the open source developer Ximian. Mono will be open source and therefore free of any charges or proprietary information. In addition, due to the open nature of the IL, any language can be modified to produce IL code and therefore integrate with either .NET or Mono.

The end advice is to conquer your fears about language interoperability and begin to seriously consider if your project could benefit from the .NET framework. As with every new technology, you must analyze the risks and rewards, but you can expect to see future work to continue in the direction of language interoperability.

Strategy Pattern Revisited

When we looked at preventing premature optimization, we discussed the Strategy Design Pattern [GoF95]. To recap, the basic idea of the Strategy Pattern is to abstract the interface to an algorithm so that multiple algorithms for the same functionality can be encapsulated in interchangeable objects. This pattern is described in detail in the Design Pattern book by the Gang of Four, so here we will just examine how it applies to reducing the risks of adopting third-party code and libraries.

When adopting any library algorithm, it is a good idea to encapsulate the algorithm in an object that follows the Strategy Design Pattern. By doing this, changing to a different library’s implementation or your own implementation of the algorithm is an easy transition. The important part of this approach is to carefully consider the public interface of the Strategy so as not to expose any library-specific details that will make transitioning difficult.

To accomplish this, it might be necessary to convert objects and values back and forth from your project’s representation. This is a good idea in any case, as a common representation for objects across the project is important to communication and sharing of code and responsibilities. For example, the only time you should expect to see more than one representation of a three-dimensional vector is when interacting with an external library. By using the strategy pattern, and the other patterns we will talk about shortly, you can minimize the areas of the code where this occurs. As always, the conversion process will lead to concerns about performance. For reassurance about why this is not a concern, see Chapter 1. In short, when the time comes to optimize, you will be able to sacrifice this conversion if and only if it is necessary for performance, because the code base will be stable and therefore changes will not cause development hassles.

 CD-ROM  For common conversion operations, it is vital to provide an automated method for converting and handling the different object types of the library and project code. This will encourage the consistent use of conversion by removing the tedium of continually implementing the same conversion. In addition, the presence of the library’s object type can be reduced to only direct interactions of the library. To better understand what this means, let us look at an example of two different vector types that might be encountered on a project using another developer’s library. The full C++ code for this example is included on the companion CD-ROM in Source/Examples/Chapter3/convert.h and Source/Examples/Chapter3/convert.cpp. Here is the vector class that the project is using:

   class t_Vector    {    public:       t_Vector()          : m_x(0.0f), m_y(0.0f), m_z(0.0f)       { }       t_Vector(float i_x, float i_y, float i_z)          : m_x(i_x), m_y(i_y), m_z(i_z)       { }       float m_X() const { return(m_x); }       float m_Y() const { return(m_y); }       float m_Z() const { return(m_z); }    private:       float m_x, m_y, m_z;    }; 

Here is the vector class used by a third-party library:

   class t_NIHVector    {    public:       t_NIHVector()          : m_x(0.0), m_y(0.0), m_z(0.0), m_w(0.0)       { }       t_NIHVector(double i_x, double i_y, double i_z, double i_w) : m_x(i_x), m_y(i_y), m_z(i_z), m_w(i_w)       { }       double m_X() const { return(m_x); }       double m_Y() const { return(m_y); }       double m_Z() const { return(m_z); }       double m_W() const { return(m_w); }    private:       double m_x, m_y, m_z, m_w;    };

Notice that there are two major differences between the two vector classes. The project class uses float, whereas the library uses double and the library vector contains the extra w coordinate. To automate the conversion process, we need an object that can be assigned to from both types and then used as both types:

   class t_NIHVectorProxy    {    public:       explicit t_NIHVectorProxy( const t_Vector &i_vector) : m_vector( i_vector.m_X(), i_vector.m_Y(), i_vector.m_Z(), 1.0)       { }       explicit t_NIHVectorProxy( const t_NIHVector &i_vector) : m_vector(i_vector)       { }       t_Vector m_Vector() const    {          return(t_Vector(             static_cast<float>(m_vector.m_X()),             static_cast<float>(m_vector.m_Y()),             static_cast<float>(m_vector.m_Z())             ));    }       t_NIHVector m_NIHVector() const { return(m_vector); }       const t_NIHVector &m_ConstNIHVector() const { return(m_vector); }    private:       t_NIHVector m_vector;    };

Notice that we store the vector as the more complex of the two types. This is to prevent loss of the extra information contained in the w coordinate. Even with this precaution, it is still easy to accidentally lose this information when performing conversions. An alternative method, known as the Adapter Pattern, exists that can further reduce this possibility, but even it cannot eliminate this danger. Therefore, it is more important than ever to comment conversion and adapter classes. We will look more at the Adapter Pattern later; for now, let us look at how we could use our conversion helper. First, let us imagine that we have two library functions for getting and setting a location as follows:

   t_NIHVector m_GetLocation();    void m_SetLocation(const t_NIHVector &i_vector); 

In addition, we need a function in our project with which we can add two vectors:

   t_Vector m_VectorAddition( const t_Vector &i_lhs, const t_Vector &i_rhs)    {       return(t_Vector(          i_lhs.m_X() + i_rhs.m_X(),          i_lhs.m_Y() + i_rhs.m_Y(),          i_lhs.m_Z() + i_rhs.m_Z()          ));    }

Now we can write a function that moves the library location by a delta vector:

   t_Vector m_Move(const t_Vector &i_delta)    {       t_NIHVectorProxy l_location(m_GetLocation());       t_NIHVectorProxy l_newLocation( m_VectorAddition( l_location.m_Vector(), i_delta));       m_SetLocation( l_newLocation.m_NIHVector());       return(l_newLocation.m_Vector());    }

Notice that the m_VectorAddition function returns a new object of type vector, which therefore cannot have retained the w coordinate of the library location vector. In this instance, we know that the w coordinate is always 1.0, but if we could not guarantee this, we would have to find another method for adding the vectors or saving the w coordinate. This illustrates the dangers of mixing types and why they must be isolated as much as possible. To this end, let us look at some other useful techniques for containing the library interactions.

Adapters, Bridges, and Façades

While the Strategy Design Pattern works well for isolating individual third-party algorithms, you might be adopting a more full-featured set of code and libraries that represents more than just an algorithm. Fortunately, other useful design patterns can be used to isolate these larger concerns from the project code to assist in case of the necessity for changing libraries. As with the Strategy Pattern, the Adapter, Bridge, and Façade Patterns can be investigated in detail in the Gang of Four’s Design Patterns book. Here we will discuss the specifics of their use in providing a layer of protection from external code.

Bridge Pattern

We start with the Bridge Pattern, which is meant to separate an implementation from the corresponding abstraction that it represents. This applies perfectly to the use of external libraries, where you have an existing implementation with the possibility of changing to a different implementation if problems arise.

To create a bridge, you must examine the library and determine what functionality your project is planning to use. With this in mind, construct an interface that conforms to the style of your project. The key concept to apply while constructing this interface is making it minimal but complete. Do not add a lot of helper functions or access to unrelated parts of the library. These can be added later at a different layer or through a different bridge, and will only confuse the use and maintenance of the abstraction you are initially creating. On the opposite side of the coin, you should try to anticipate any reasonably minor changes and likely functionality that is related to the parts of the library being used. Creating the interface is a balancing act between these two opposites that is unique for each new project and library.

With the interface designed, the bridge can be connected to the library and placed into the project for use. Figure 3.1 shows the relationship of the library to the project using the Bridge Pattern. The library implementation should be private and remain inaccessible except from the bridge or bridges for that library. With most languages, this must be accomplished with project conventions since it is difficult to restrict access to a library meant for reuse.

click to expand
Figure 3.1: A bridge between the project style interface and the libraries is made through a private interface that provides uniform access to each library’s wrapper.

An additional advantage is that the implementation can vary even at run time, allowing experimentation with different libraries for evaluation purposes. However, as with all abstractions there is a performance penalty implicit in the redirection necessary to create the bridge. This should only be a concern at the end of development in the optimization phase, and at this time, the bridge can be removed without the problematic consequences of not creating it early in the project lifecycle or removing it too early.

Adapter Pattern

In any object-oriented language, there is a good chance that a library will return an object with information you need to save, access, and pass back to the library. If your project does not already have planned an object of this type, you might consider adopting the library's implementation. However, if you already have an object of a related type, it is not wise to have two related but differing representations in the project code base. A common example is a vector or matrix representation, which has been redeveloped more times than many of us would like to hear. You therefore must come up with a way for your project to interact with its type and the library to interact with its type seamlessly.

One method for accomplishing this, which was mentioned earlier, is to convert objects from the library to objects in your project. However, this approach is not sufficient in all cases. Suppose, for instance, that the library representation contains an extra bit of information that your type is not capable of representing. To solve this, you could create a container that holds your type, converted from the library type, and the extra information, but this becomes cumbersome to manipulate and track.

A better solution is to use the Adapter Pattern to wrap the library object in an interface that is shared with your object type. You can then present an object that acts like your type while maintaining any internal data specific to the library implementation. This also facilitates changing library implementations as you already have the adapter interface available and only need to wrap the new libraries object in the same interface.

To better show the tradeoffs involved, let us look at an alternative solution to the vector conversion that was mentioned in our discussion of the Strategy Pattern. For this example, we extend the functionality of the vector class slightly to allow assignment:

   class t_Vector    {    public:       t_Vector()          : m_x(0.0f), m_y(0.0f), m_z(0.0f)       { }       t_Vector(float i_x, float i_y, float i_z)          : m_x(i_x), m_y(i_y), m_z(i_z)       { }       float m_X(float i_x) { return(m_x = i_x); }       float m_X() const { return(m_x); }       float m_Y(float i_y) { return(m_y = i_y); }       float m_Y() const { return(m_y); }       float m_Z(float i_z) { return(m_z = i_z); }       float m_Z() const { return(m_z); }    private:       float m_x, m_y, m_z;    };

As well as the library vector class:

   class t_NIHVector    {    public:       t_NIHVector()          : m_x(0.0), m_y(0.0), m_z(0.0), m_w(0.0)       { }       t_NIHVector(double i_x, double i_y, double i_z, double i_w) : m_x(i_x), m_y(i_y), m_z(i_z), m_w(i_w)       { }       double m_X(double i_x) { return(m_x = i_x); }       double m_X() const { return(m_x); }       double m_Y(double i_y) { return(m_y = i_y); }       double m_Y() const { return(m_y); }       double m_Z(double i_z) { return(m_z = i_z); }       double m_Z() const { return(m_z); }       double m_W(double i_w) { return(m_w = i_w); }       double m_W() const { return(m_w); }    private:       double m_x, m_y, m_z, m_w;    };

Now to implement the Adapter Pattern, we must extract an interface from the project vector class:

   class i_Vector    {    public:       virtual float m_X(float i_x) = 0;       virtual float m_X() const = 0;       virtual float m_Y(float i_y) = 0;       virtual float m_Y() const = 0;       virtual float m_Z(float i_z) = 0;       virtual float m_Z() const = 0;    };

This interface will be used when possible in the project code instead of the actual vector class. For example, we now implement the vector addition as follows:

   void m_VectorAddition( i_Vector &io_lhs,       const i_Vector &i_rhs)    {       io_lhs.m_X(io_lhs.m_X() + i_rhs.m_X());       io_lhs.m_Y(io_lhs.m_Y() + i_rhs.m_Y());       io_lhs.m_Z(io_lhs.m_Z() + i_rhs.m_Z());    }

Notice the necessary difference is that we no longer want to create a new vector if possible. This is because the new vector could be a different type than the vector we are trying to update. There are ways to handle even this by using the Abstract Factory Pattern, but for now, let us just use the method presented here. The good news is that larger objects, which are good candidates for the Adapter Pattern, generally do not get instantiated as often, so they avoid this problem.

As you can see, things can become somewhat complicated when restricted to using interfaces. Using the Adapter Pattern can have wider reaching consequences if you are not already using interfaces for the project classes involved. This is the main disadvantage of using the Adapter Pattern, and the only tradeoff that will have a major impact on your decision to use conversion or adaptation.

Moving on, now we can derive the concrete vector class from this:

   class t_Vector : public i_Vector    {       // ...    };

The most important step comes next, as we make a wrapper for the library vector that implements the project vector interface:

   class t_NIHVectorWrapper : public i_Vector    {    public:       explicit t_NIHVectorWrapper( const t_NIHVector &i_vector) : m_vector(i_vector)       { }       t_NIHVector m_NIHVector() const { return(m_vector); }       float m_X(float i_x)       {          return( static_cast<float>( m_vector.m_X( static_cast<double>(i_x))));       }       float m_X() const { return( static_cast<float>( m_vector.m_X())); }       float m_Y(float i_y)       {          return( static_cast<float>( m_vector.m_Y( static_cast<double>(i_y))));       }       float m_Y() const { return( static_cast<float>( m_vector.m_Y())); }       float m_Z(float i_z)       {          return( static_cast<float>( m_vector.m_Z( static_cast<double>(i_z))));       }       float m_Z() const { return( static_cast<float>( m_vector.m_Z())); }    private:       t_NIHVector m_vector;    };

This wrapper contains the library vector, thus preserving any extra information. Finally, we rewrite the m_Move function to use our new functionality:

   t_Vector m_Move(const i_Vector &i_delta)    {       t_NIHVectorWrapper l_location(m_GetLocation());       m_VectorAddition(l_location, i_delta);       m_SetLocation(l_location.m_NIHVector());       return( t_Vector( l_location.m_X(), l_location.m_Y(), l_location.m_Z()));    }

At this point, the Bridge and Adapter Patterns probably sound nearly identical. In many ways, they represent a similar approach to the problem of hiding the library implementation, but there is an important difference. In the case of the Bridge Pattern, the interface you are developing for abstraction comes directly from your use of the library implementation. The Adapter Pattern, on the other hand, is meant to adapt a library object to an existing interface already designed for your project.

Façade Pattern

So far, we have talked about abstracting specific functionality for varying the implementation from the abstraction. We can, and should, take this one step further by using the Façade Pattern. This pattern provides a unified interface to a collection of subsystems that make up a coherent module. For example, the rendering system of an application could be composed of several different subsystems with differing interfaces. By placing a façade upon the rendering subsystems, the rest of the application deals with a single interface.

The true advantage of this for external code and library use comes when a portion of the library in a system is lacking in support for a necessary subsystem. With the façade in place, it is easier to add a separate library into the mix without providing access to the library that would result in confusion about which library is responsible for what functionality. This reduces the risk of confusion by ensuring that only a few people or a single person is exposed to the problems of combining multiple implementations.

The façade should be a second layer of abstraction for external libraries, with the Bridge Pattern forming the first layer of abstraction. This provides the maximum flexibility for adapting to changing requirements and unexpected problems with the third-party code. This also allows you to get the most out of external libraries without the fear of everything depending on them. Even if one portion of the library fails, it is isolated and can be replaced individually with no effect on the rest of the library or the project code.

All these patterns and other preventative techniques aim to reduce the risk involved in using code and libraries from other programmers and developers. With reduced risk comes reduced fear, and this removes the main force behind NIH Syndrome. Even if you do not possess the usual fear of using others’ code, these techniques can benefit you and protect from the inevitable unexpected problems that arise in software development.



 < Day Day Up > 



Preventative Programming Techniques. Avoid and Correct Common Mistakes
Preventative Programming Techniques: Avoid and Correct Common Mistakes (Charles River Media Programming)
ISBN: 1584502576
EAN: 2147483647
Year: 2002
Pages: 121
Authors: Brian Hawkins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net