Several months ago I was asked to lead a fairly large development project that involved efforts from multiple consultant and offshore development groups. One of the first things that I did was draft a development principles and standards document to ensure that all groups were on the same page. The following is a slightly revised version of that document. This information is not new or extremly profound; however, I thought it might be beneficial to some.
Development Principles and Standards
There are several core principles and standards that are essential to the development of any solution. These principles and standards are necessary to ensure maintainability of the solution as well as successful, cost effective implementations of future customers that wish to take advantage of the product offering.
Reusability is of prime importance as it relates to the development of the different aspects of a solution. The expectation is that all system functionality should be constructed so that new customers can be setup within the solution with no development. In addition, all functionality should be constructed in a manner that allows other systems to be able to interact with the solution with no internal system development.
Extensibility and Maintainability
There are multiple principles that should be followed to increase the extensibility and maintainability of the solution.
• Separation of Concerns (SoC) - This generally implies separation of UI, service, domain, and data access/persistence layers as well as abstraction of contracts/interfaces from implementation specifics. This allows for clear delineation of functionality and ensures that any type of updates to the solution can be accomplished quickly and with little to no unexpected ramifications.
• Single Responsibility Principle (SRP) – This principle states that each component, class, method, and/or function should only be responsible for a single piece of functionality. Another way to state this is that each distinct code block should only have a single reason to change. This reduces the potential for accidently causing an unexpected consequence when modifying a particular feature or piece of functionality.
• Don’t Repeat Yourself (DRY) – In order to make a solution extensible and maintainable, code and/or logic should not be duplicated. Failure to adhere to this principle increases both development time and the likelihood of errors.
• Self Documented Code – In order for a solution to be maintainable, it must be very easy to determine the intent of any specific piece of code. One of the best ways to accomplish this goal is to ensure that all code is as descriptive as possible by using identifier, method, function, and class names that clearly define intent.
Automated Tests and Testability
Functionality should be wrapped in automated tests. Ideally the solution would be developed using a test-first approach; however, it is acceptable to utilize a test-after approach as long as the majority of functionality is under test by the time of delivery. In addition to the “unit tests” (tests around each individual method), functional tests should be written to ensure that the overall behavior works as expected. Finally, all tests should be maintained during future development endeavors and run before “check-in” and before each deployment. These tests accomplish multiple goals:
1. They provide a form of documentation that shows the intent of the functionality and/or specific class/method.
2. They provide a quick regression testing strategy that allows confidence that any future code changes do not have unexpected, adverse ramifications.
3. A test-first approach has a general side-effect of a loosely coupled, highly cohesive, maintainable, and testable solution that greatly increases agility and significantly decreases bugs.
Consistency and Unity
The code should be consistent and unified. It should appear as though a single mind developed each bounded context. This can be accomplished by following various coding standards. It is important to point out that this relates to each bounded context and not the project as a whole. For example, any UI development should have a unified and consistent coding style. Likewise, any service/domain development should have a unified and consistent coding style (even if this style differs slightly from the UI development coding style). Conventions that promote consistency pertain to the standardization of layering, naming conventions, and consistent use of idioms. One great way to help ensure that these agreed upon standards are followed is to utilize a tool such as FxCop during development.
Versioning and Service Contract Management
In order for a system to be extensible, while continuing to support existing customers and functionality, a service contract versioning strategy must be followed. It is critical that unaffected consumers of the exposed services are not required to update/deploy changes to their systems every time that a modification/addition is implemented. This means that existing service contracts should only be modified when a non-breaking change is implemented (such as the addition of a new operation). It also implies that changes at the domain level should not automatically be propagated to the service layer (note: One strategy to prevent this issue is through the use of the DTO pattern). In the event of a required breaking-change, the service contract should be versioned using an agreed upon versioning strategy, while keeping everything as agnostic, reusable, and maintainable as possible.
The functionality exposed in the solution should be composable and expressive of agnostic logic. This means that the web services should be built as general services, which allow current business requirements to be met while allowing reusability for future consumers and/or business needs. This indicates that web services should rarely, if ever, be designed to support only a single client, consumer, or application.
The solution’s web services and functionality should be atomic. This means that all activity that occurs during the lifecycle of a service operation should be rolled back in the event of a failure. This ensures that the outcome of the operation is consistent with either a cancellation in conjunction with an error or a successful outcome with the expected response.
Fine-Granularity versus Course-Granularity
The solution should implement course-grained web services that act as a thin wrapper around a fine-grained domain model. The high level of abstraction provided by the web service interfaces allow consumers to depend on the exposed contract and receive maximum performance benefits associated with the course-grained services. The fine-grained domain model allows the solution to change whenever the business requires by providing maximum flexibility, while limiting chage to the interface(s) on which the consumers depend.
Logging and Error Handling
A logging strategy should be consistently utilized throughout the development of the overall solution. At a minimum, this strategy should include the logging of any user initiated action. Additionally, the ideal strategy should including logging at a method/function level indicating entry and exit of each specific method/function. An ideal approach to accomplishing this functionality, while still adhering to the DRY principle, is to utilize an Aspect Oriented Programming (AOP) approach (Note: A great example of a framework to help with this approach for C# development is PostSharp.) Finally, the verbosity of the logged information must be configurable during run-time. A collection of possible error codes and associated descriptions should be documented and made available for delivery to consumers. In addition, all errors should be logged in a persistent and searchable data store.