What are Dependencies?A dependency is any logical or physical object upon which program code depends on for proper execution. So long as a dependency object remains unchanged, it is predictable, and provides a stable, reliable foundation upon which more complex programs may be built.
However, dependency objects are programs, too. They must adapt to change and demands. When dependencies are modified, their new behavior may adversely affect the function of programs that rely on them. These issues may cascade through several layers of dependent assemblies, and then ultimately manifest in an unexpected area, that does not directly interface with the dependency object. Tracing this sort of bug is not always easy, and can be easily prevented.
We have all worked with that chunk of code that everyone dreads changing, due to the horrors of complexity and/or obscure coding practices. Dependency object must remain highly maintainable, and must have proper implementation of interfaces, to mask internal changes. Change turnaround time is most often hindered by the morass of an inordinately large and/or complex method or subroutine.
The culprit second to complexity in maintenance horrors is poor documentation. Code should be self-describing, and should include comments, besides. (I don't care how self-describing code is -- always include comments describing the actions about to take place! This helps to quickly isolate problems in the future, and probably save your hide from being tanned.) Documentation should be externally visible. For example, Visual Studio's XML Intellisense comments are a very good source of documentation that may be extrapolated and compiled into a variety of reference methods.
The cost of maintenance may quickly become prohibitive. Changes to dependency objects result in adapting dependent code and regression testing. An estimated dollar amount of required programmer hours is often quite unattractive.
Public interfaces must not change. The worst problems arise when an interface changes, requiring all dependents to be modified accordingly. This does not downplay the issue of changing the behavior of the code. When functions are changed to return different data, consumers of that data may not be able to handle the change.
- Version Control
Administrators of version control software must be aware of changes, and should already have planed for dependency change, in advance. Static "snapshots" of a logical dependency (assembly / library) must be used for development and deployment. otherwise, ongoing changes to the dependency will exponentially increase production delays, through the chain of dependencies.
Along with version control, the testability of the dependency must be kept in check and planned in advance. When a dependency object changes, a full bout of regression testing is in order. Regression tests must start by testing the most fundamental portions of the program, which are the dependency objects. Therefore, the object must remain highly testable. Of course, the dependency itself must be testable, before being released for integration in other products.
An equally important aspect of testing is how to mock a dependency object, when performing unit tests. Using the actual dependency object for testing is extremely unreliable, and should be replaced with a predictable mock. (See the dependency injection section, below.)
Types of Dependencies
Assembly / Library / Package DependenciesWhen a program relies on another compiled set of code, this is known as an assembly or library dependency. The dependent program relies on the external assembly to perform functions not included in the program itself. This centralizes common functions in a distributable assembly, for use in various other programs, and is a general precept of OOP.
Data access layer assemblies are commonly shared among program applications that access common data, from external resources. It makes sense to create a common assembly that performs the actions of data connections, retrieval, formatting, etc. Doing so creates a single point of maintenance, making bug fixes faster. However, this also means that a change in the one assembly can easily break many programs.
Interface DependenciesOne of the chief purposes of interfaces is to provide consistency. This is of particular import when dealing with application program interfaces (API) and wrappers. These interchanges depend on consistent interface structure, for proper operation. APIs are especially susceptible to interface changes, which will cause applications developed by all consumers of the API to fail. Imagine what would happen if the API for Cisco router network management interfaces were to unexpectedly change.
Class DependenciesClasses are prevalent throughout OOP. As result, there are many ways to determine the accessibility of any one class (public, private, external, internal, protected, etc.) and its state behavior (static, etc.). Changing the accessibility of a class from Public to Internal may not initially present any problems, even in testing, but will manifest when the class is a dependency of an external assembly.
Class members must also be taken into consideration. Changing the accessibility of a property from public to private will shut off access, causing issues in a dependent class or external assembly.
Inheritance will likely expose dependency issues rather quickly. Derived objects may or may not suffer catastrophic failure, depending on the architecture of the base object. If so, the code will simply fail to compile. The trouble is that when the dependency is out of your control (i.e. third party API), there is nothing you can do, but adapt to the change. You do not want to be the one forcing change upon everyone else, if you can help it, unless you want people cursing your name and coming at you with pitchforks and torches... and possibly a rope or two.
Physical DependenciesThere are several physical dependencies to consider:
The state of the computer may change. If the application requires drive letter "D:\" be present, what happens when that drive is removed? What is it is irrelevant, because the operating system does not use drive letters?
Many programs retrieve data from databases, file server, and other resources. Each of these machines, including each network segment and signaling device in between, represent a dependency prone to failure. Programs should gracefully handle network disconnects and missing resources. Hard-coding anything is a terrible thing to do, especially network resources, such as server and other machine names.
- Web Services
Like network resources, Web service URLs are prone to change. Don't hard-code these!
We have all seen applications that communicate with Internet resources. I'm sure you have also seen one that simply hangs until that resource is found, particularly on mobile devices. Applications must gracefully handle Internet resources by assuming they will not always be available, and to not prevent the user from interfacing with the application while resources are found.
- Logical Structures
XML is a versatile mode of transporting data in logical structure. However, the structure map is expected to remain constant, so consumers know what data may be provided, and how to locate data of interest. SOAP is a more detailed means of transporting data in logical structures, and is more complex than XML. With increased complexity comes increased sensitivity of dependent change.
Dependency InjectionA method to work around dependency issues is to employ a dependency injection framework. This simply uses service requests, in place of the dependency object. When the dependency is needed, the service is called, and the injector passes back an object. This is extremely useful for testing, as the injector can either pass back the actual dependency object, or a mock object that emits predefined, predictable data. Dependency injection simply leverages OOP principles, to move objects one step away from the consumer, allowing decision logic to take place in between.
Managing Assemblies with Version ControlUltimately, dependency objects, including injection frameworks themselves, must be properly managed via version / revision control. Any code dependencies (assemblies / libraries / packages / etc.) must be copied and attached to each release, spiral, sprint, phase, etc. of a product's lifetime. Each branch of the source code should contain a "packages" or otherwise named directory that contains the .dll, .exe, .jar, etc. files upon which that specific version of the product depends. This means you must do two things, to properly manage production via version control: 1) properly use branching, and 2) properly use branching. Yeah, you read right.
The purpose of freezing code dependencies is to both minimize impact on development time when developing hot-fixes for bugs, and for historical reference. If a version of the program from five years ago required a hot-fix, and the dependency code was not preserved, it is highly unlikely the exact version of the object will be found, if at all. Most of the time, adapting the entire program to a current version of the dependency object causes more bugs and problems than are corrected. Besides, the QA staff will loathe you, and stock for pitchforks and torches will be on the rise.