CyberArmy University | Open Source Institute | CyberArmy Intelligence & Security | CyberArmy Services & Projects

[Library Index]

[View category: Programming] [Discuss Article]

Programming Essentials: Best Practices

Article is yet to be rated
Author:      despair
Submitted:      24-Jul-2007 14:48:38
 


This article aims to cover some "best practices" of programming from a language neutral standpoint to provide both beginning and established programmers a guide to creating code that runs faster, compiles more quickly, and utilizes less resources.
Clarity of vision

The first and most important step to take when approaching a programming project is to determine, at the most basic level, what the purpose of your application will be. How you design your application and which tools you will select are dependent on your vision for the application. Clearly define the intended function of the application in broad terms (i.e. "This application will be a basic text editor"). Once you have established that, you must then list the integral features needed for an application of that type. For our basic text editor, we require:
  • The ability to accept and manipulate discrete (read keyboard) user input
  • The ability to read input from existing files
  • The ability to output data to a file
Once you have established the basic functions necessary for the type of application you have set out to create, implement these functions first, without worrying about anything else yet. This basic application may not resemble the application you envision as a finished product, but the basic functions it does have provide a stable base from which you can extend functionalities later on. It will also help to ensure that you do not lose sight of your original vision for the application.

Once these features have been implemented and are working correctly and efficiently (tips regarding correctness and efficiency will be covered later in the article), then list the features you wish to implement that are direct extensions of the base features. Building off of the basic implemented features first will help to ensure stronger, more functional code by using an already established base without the need to craft additional functions from scratch. In the long run, this will save time and produce better code.

Lastly, implement the necessary features which do not build directly off of any previously written functions. These are the features which might define your application and set it apart from others in its class, but keep in mind that these features should be essential: implementing the ability to embed video in our simple text editor would be quite novel and would certainly set it apart from other applications in its class, but would only serve to add unneeded complexity to the application with no real gain. Any new features that you code must provide real gain to the end user: it is preferable to have a slightly smaller set of well-implemented essential features than to have a larger set of essential and non-essential features that are not well-implemented. For a good example, ask yourself, "Would I have added the animated paper clip to Microsoft Office?"

Above all, remember that no one feature can become more important than the application itself. When designing an application, the whole of the application must be considered at all times. Ensure that all features integrate tightly with the application and that proper flow is maintained. Do not let your "killer feature" be the feature that kills your application. Building an application with the "new and novel features" in mind from the beginning and allowing those features to be the guiding factor in your entire development process will lead to a structurally unsound application.

Application design should be done as a pyramid:
  • Start with a broad, stable base
  • Build a rich body of features directly from the base
  • Add a capstone of "new" features
A final note regarding clarity of vision is the necessity of maintaining good source code comments. This is especially true if you will not be the only person working on the code. Even if you are the sole maintainer of the code base, leaving yourself good comments can keep you on track when resuming your project. Clearly specify each important function, as well as each function which is not readily obvious. This can save you countless headaches in the future.

Section 2: What's in your toolbox?

Programming, for all intents and purposes, is a craft. Like other crafts, such as painting, music, or carpentry, what you can create is largely dependent on not only your skills, but also the tools you have available. Even a highly skilled woodworker cannot accomplish every task in woodworking if he only owns a chainsaw.

For programming, our basic toolbox consists primarily of the following:
  • a programming language
  • a development environment
  • testing tools
First, you must determine the proper language for your application. Not all languages are created equal in every sense, and some are better suited for different applications. It is of course best to go with a language you are familiar with and use it to your best advantage, but there are many languages to choose from if you either want to learn something new or perhaps code a program in a more optimal manner. If you are skilled with a particular language and can leverage the conventions it gives you to your best advantage, then your application will be good. If you are using an unfamiliar language, the application will not necessarily be bad, but it will suffer from being wielded with a lack of expertise.

Every programming language needs a development environment. Since no programmer will write perfect code every time, it is recommended to use an optimizing compiler. Research into the compilers available for your chosen programming language can lead you to know which optimizations can be performed by which compiler, and how they will affect your code; much like programming languages, not all compilers and optimizations are created equal. Over-aggressive optimization can actually decrease performance and break things in the long run, so an understanding of the compiler you are using and the optimizations it is implementing can be quite useful. At times, it may be necessary to experiment and learn which optimizations work best for your application, as their effect will vary depending on the structure of your code. Ideally, those optimizations which do not greatly increase (or those that even reduce) the compiled code size and those which promote more efficient memory usage are the ones that you want to make use of most frequently.

Lastly, you need testing tools. Most people would not build a house without ensuring that it is level, and very few people would buy a house with leaking pipes. Similarly, the testing tools you select will ensure that your application performs efficiently. Debuggers help you find execution flaws in your program and help you repair them. Tools which test for memory leaks will help you eliminate those (this is important even for small programs; a memory leak can be generated by a single line of code, so beware). Some programming languages (like C) have code auditing tools for testing the security of your code.

Simply compiling and executing your application and seeing that all of the features appear to execute correctly is not enough if the application can become unstable during prolonged use due to memory leaks, or if the application can easily be hijacked through an overlooked security flaw. Again, application design is a holistic process, and you must cover every angle for the application as a whole, not just each component part individually. The tools you choose to execute your design will contribute greatly to the overall solidity and efficiency of your application if you choose them wisely.

Section 3: Less is more: The elegance of simplicity

When determining how to implement a function, always take the path of least resistance - use the fewest instructions possible to accomplish the desired result. This will save valuable programming time, and promote efficiency in the application. Ask yourself, "Is it easier to make one right turn, or three left turns?" Both end up facing the same way, but obviously the latter process is more complex and time-consuming.

This is not a case of form over function, or function over form, but rather that in programming, form and function must be one. Again, application design is holistic. How you do something can actually be more important than what you do. In the "dark ages" of computing, programmers wrote tight, clean code out of pure necessity: when using machines with severely limited processing power and memory, the most efficient application always won the day. My contention is that this should still hold true today, despite the tremendous increases in processing power and memory that have occurred since the early days of computing.

Consider your general target platform: what type of machine will this application generally be run on, and what are the average specifications? If for example, you are designing an application for a system with a clock speed of 2GHz and 1GB of RAM, do not start writing your program with those specifications in mind; instead write your application such that it will run comfortably on the least amount of resources that it possibly can, and only allow yourself to move beyond those boundaries when no other option exists. By constraining yourself like this, you will write more efficient code.

Section 4: Self-Containment

In terms of efficiency, it is best to keep your application as self-contained as possible: instead of creating four processes that communicate not only with one another but also with the same set of outside libraries, try to condense the application into fewer processes communicating with the same outside libraries. Four processes accessing the same outside library and speaking with each other create more unnecessary channels of communication than a single application process accessing that same outside library and sharing information within itself. One way to do this is to create internal modules for your application and pass values between the modules internally, rather than loading external application processes.

For example: Four application processes accessing the same outside library and communicating with each other create six additional outside channels of communication.

n(n-1)/2

This formula is known as the Group Intercommunication Formula (1). The top part of the formula comes from the fact that n total processes communicate with n-1 processes (to exclude itself). The division by two is due to the lack of hierarchy between the processes communicating. If a hierarchy existed between the processes, the formula would be simply be n(n-1), because communication could only function in one direction (i.e. process "a" can talk to process "b", but not the other way around). As this process can occur two ways between the processes, this gives rise to the need for the division by two.

4(4-1)/2 = 6

While condensing these four external processes into two processes using internal modules which share information from the same outside library reduces this number significantly (to 1 additional external communications channel).

2(2-1)/2 = 1

A single application process will have no additional external forms of communication. When expressed in terms of the previous formula, it would technically yield an "undefined" result, due to the division by zero. This illustrates that this is not applicable to single application processes and is therefore preferred when possible.

The fewer the processes, the fewer the communications channels required to complete any given action outside of the program, and thus the smaller the memory overhead for your application. This also improves memory management in dynamic memory management applications. Memory management is done for one application process in a single memory space rather than for a greater number of application processes residing in unique memory spaces passing information between one another. In some instances, the presence of multiple memory spaces which pass shared values between one another can cause erratic behavior in the application due to memory spaces being deallocated for one process when they are still storing information needed by another process, or having values which are stored in one memory space being overwritten with a value which may or may not make sense to another application process reading from that memory space.

Section 5: Design philosophies: UNIX versus MIT

While there are several programming design philosophies, I will cover the two most prominent philosophies here: the UNIX design philosophy (2) (also called "New Jersey Style" and "Worse is better") and the MIT approach (3) (also known as "The Right Thing"). The two design approaches take an opposite stance and produce quite different software. Neither can be said to be a "correct" or "incorrect" way of doing things in general terms of software development, as each design philosophy has its place depending on a number of factors. Finding the one that suits your purpose is largely dependent on your individual style and the needs of your application.

"Worse is better":

Simplicity - the design must be simple, both in implementation and interface. It is more important for the implementation to be simple than the interface. Simplicity is the most important consideration in a design.

Correctness - the design must be correct in all observable aspects. It is slightly better to be simple than correct.

Consistency - the design must not be overly inconsistent. Consistency can be sacrificed for simplicity in some cases, but it is better to drop those parts of the design that deal with less common circumstances than to introduce either complexity or inconsistency.

Completeness - the design must cover as many important situations as is practical. All reasonably expected cases should be covered. Completeness can be sacrificed in favor of any other quality. In fact, completeness must be sacrificed whenever implementation simplicity is jeopardized. Consistency can be sacrificed to achieve completeness if simplicity is retained; especially worthless is consistency of interface.

As long as the initial program is basically good, it will take much less time and effort to implement initially, and it will be easier to adapt to new situations, such as porting software to new machines. A program designed using this philosophy will spread rapidly, long before a program developed using the MIT approach has been developed and deployed. Once it has spread, there will be pressure to improve its functionality, but users have already been conditioned to accept "worse" rather than the "right thing". "Therefore, the worse-is-better software first will gain acceptance, second will condition its users to expect less, and third will be improved to a point that is almost the right thing."

This philosophy is good if you wish to see a wide acceptance of your software, especially if you produce a workable product that can be patched and improved along the way. There is a deep bias against this type of software development, as it tends to initially produce what can be considered "inferior software." However, if your application needs rapid, widespread acceptance, is correct and consistent enough to be accepted by users with minimal effort, can be adequately updated to provide correctness and consistency, and needs to be highly portable to other architectures and environments, this may be the choice for you.

This is the software equivalent to the "slightly used" AMC Gremlin. It may be ugly, it may need work, and you may have to come up with some interesting ways to keep it running, but it may last half a million miles on the road before you have to find another one, and no two will ever drive the same. Ask yourself, "Is the requirement for user adaptability, ingenuity, and an inconsistent experience worth wide acceptance and long application life?"

MIT ("The Right Thing"):

Simplicity - the design must be simple, both in implementation and interface. It is more important for the interface to be simple than the implementation.

Correctness - the design must be correct in all observable aspects. Incorrectness is simply not allowed.

Consistency - the design must not be inconsistent. A design is allowed to be slightly less simple and less complete to avoid inconsistency. Consistency is as important as correctness.

Completeness - the design must cover as many important situations as is practical. All reasonably expected cases must be covered. Simplicity is not allowed to overly reduce completeness.

This philosophy is favored if you are producing a mission critical application which must be correct. If rapid deployment and rapid public acceptance are not your initial goal (which may or may not reduce long term acceptance and application lifetime), and you do not need your application to be ported to different architectures or environments easily, then this is certainly the favored approach. The MIT philosophy simply produces better code, initially and in the long-run.

It is the software equivalent to buying a new Rolls Royce. It is shiny, well made, and will also last half a million miles on the road. Lots of work goes into this vehicle, and as a result, there is associated expense and limited availability. Nothing will have to be "jury rigged" because it was made correctly the first time, and any two vehicles of this type will offer exceptional driving experiences. Ask yourself, "Who doesn't want a Rolls Royce?"

As stated previously, other design philosophies exist, and it is up to you to determine which philosophy meets your personal style and your desires for your application. The articles cited in the notes will serve as a good jumping-off point for researching design philosophy.

Conclusion

Keeping the above guidelines in mind from start to finish during application development will help to ensure that you produce the highest quality output for the application you envision. Start by outlining your basic functionality, building additional features to extend that functionality, and then cap things off with innovative and necessary features. Remember to maintain clear comments and documentation to keep your project running smoothly and to maintain consistency.

Select the best toolbox for your particular application and leverage every aspect of your chosen toolbox to your best advantage. During design, always favor the approach which will accomplish the same end result in the fewest steps possible; this will lead to smaller, more efficient applications. Avoid "shortcuts" which are designed to save programming time, because they will translate into time or functionality lost by the end user, and lost end user time is much worse for an application than unsaved programming time.

Self-containment produces stable code, which in turns yields more efficient results. Condense code using modules rather than external processes where applicable. Choose the design philosophy which best suits you and your application and apply it consistently to your design. All of these things will help to ensure that you produce strong code with high functionality and stability in a timely manner.

Notes:

(1) This formula is referenced in "The Mythical Man Month" by Fred Brooks to describe the difficulty in adding additional developers to a project which is behind, and how such a thing can actually produce a negative work output compared to the previous number of developers. Both the formula and the article on the essay can be found on Wikipedia by searching for The Mythical Man Month.

(2) "UNIX Philosophy" Accessed July 30, 2007.

(3)"The MIT Approach" from "Worse is Better" Accessed July 30, 2007.

Tr. despair
CyberArmy Academy Publications

This article was originally published by CyberArmy.net in the CyberArmy Library.

You must be logged in to vote on an article

About Us | Privacy Policy | Mission Statement | Help