Silverlight Best Practices: Modular Code
In this series, you’ve learned about best practices for developing Silverlight LOB applications. LOB applications share specific qualities that make it ideal to build them in a modular fashion. They are often engineered by multiple programmers and span teams between design, development, and testing. Some applications can grow large and quite complex. LOB applications often follow aggressive release cycles to keep up with customer demand and require the ability to easily extend the application and provide customer-specific plug-ins and extensions.
All of these features can be effectively addressed using a modular design in your Silverlight code. Modular development is breaking a software application into smaller, interchangeable components. Modules are focused on specific functions and contain all of the code, data, and logic necessary to accomplish that function. Modules often interact with other modules through interfaces, events, and messages. A well-designed module should have minimal dependency on other modules in the system.
Silverlight is ideal for modular code because it contains all of the features necessary to produce a flexible, extensible, and modular system. Namespaces provide scope and context for modules. Data-binding coupled with the MVVM pattern allows for separation of presentation logic from the rest of the application to facilitate a parallel workflow between designers and developers. The Managed Extensibility Framework (MEF) is part of the runtime that supplies services for inversion of control, discovery, lifetime management, and extensibility. Through MEF, you can also facilitate runtime extensibility by exposing a plug-in model and dynamically loading new modules into the application.
Before you can take advantage of any of these features, it’s important that your code is architected to support them. The first and most basic principle for writing modular code is to follow principles and guidelines for keeping your code decoupled. The S.O.L.I.D. design patterns help you manage this within the code base.
Follow SOLID Software Design Patterns
The SOLID principles were introduced by Robert C. Martin. The principles cover guidelines for object-oriented design. These guidelines help you determine how to separate your code between classes, namespaces, and modules within the system. The following sections give a brief introduction to the principles and what they mean.
Single Responsibility
The single responsibility states that a class should only focus on one thing. For example, an application that loads and parses a CSV file should not contain one class with the responsibility for loading the file and parsing it. Instead, one class should have the responsibility of loading the file, while another class has the responsibility of parsing a CSV stream into its components. Following this principle encourages parallel development because multiple developers typically won’t end up in the same class at the same time. It also eases the burden of testing because there is only the primary responsibility to test for.
Open/Closed Principle
A good class is open for extension. If you need to extend the behavior, you should be able to do so by creating a derived class rather than changing the class itself. The class should be closed to modification so that the only reason to modify it internally is to fix a defect. This allows third-parties to extend the functionality of a module without breaking the module or changing it in a way that might impact other modules that depend on it.
Liskov Substitution Principle
When you do extend a class, the derived class should be substitutable for the base class. In other words, you should be able to cast a derived class to the base class and have it perform exactly as expected. For example, you may be tempted to create a Rectangle class with a length and width and derive from the Rectangle to create a Square that forces the length and width to be the same. This would violate the principle because when you cast the Square to the Rectangle, it will not behave correctly because the rectangle should allow a different width and height. To honor the principle, create a base Quadrilateral class where the length and width are read-only. The Rectangle can derive from this and allow the user to set the length and width, while the Square could override the same class and provide a simple method to set the length of a side. In both cases, the base Quadrilateral class would now perform as expected without compromising the functionality of the derived class.
Interface Segregation
Interfaces should be client-specific. A great example of this from the framework is the implementation of the IComparable and IEquatable interfaces. Most value types (integers, dates, decimals, etc.) are both comparable (they can be sorted into a list) and honor equivalency (you can determine when one value is equivalent to another). As a client dealing with a generic value type, you will likely be interested in only one function or the other. Instead of providing a general interface that specifies methods for comparison and equality, the framework segregates the interface for the specific functionality. If you need to perform a comparison, you can use the IComparable interface without knowledge of any other interfaces the target type might implement. This allows for a simpler design and reduces dependencies between clients that are consuming services on a type.
Dependency Injection/Inversion of Control
The dependency injection principle reinforces the single responsibility of a class. A class that requires logging may hold a reference as an interface to the logger. If the class creates an instance of the logger, it has additional responsibility for creating the logger and a direct dependency on that implementation. There is no longer any flexibility to change what the logging mechanism is without modifying the class, which would violate the open/closed principle. Instead, the class should expose the interface but invert control by allowing some other mechanism to provide the implementation. The dependency is then injected by the framework.
To learn more about this principle, read my Informit article “Inversion of Control with the Managed Extensibility Framework.”
Together, these SOLID principles are powerful guidelines for architecting decoupled and modular software applications.
Use Namespaces Appropriately
Namespaces provide logical scope and context for classes and types. They are useful to avoid collisions between types defined across components and between third-party providers. Without namespaces, all logging mechanisms might have the same type of Logger. Namespaces allow companies to add additional context and brand the type so that you can use a Jeremy.Logger or an InformIT.Logger and avoid a collision between the type names.
Namespaces also help segregate your internal code. Companies often use namespaces to differentiate core framework and scaffolding components from product-specific ones, and to designate modules within those applications. A very common convention that follows the .NET Framework and should be specific enough to use across your enterprise is to use this namespace template:
Company.Product.Module.Folder
For example, if I am designing a feed reader with a search module, I would place my view models in the following namespace:
JeremyLikness.FeedReader.Search.ViewModels
The key for your organization is to define a standard and stick with it so that it is consistent across the enterprise. Once you have your namespaces defined, it is easier to design your projects and assemblies. One common mistake I see (and have been guilty of myself) is to create a project for each module right away. While reducing dependencies and decoupling the application is important, there is a trade-off with complexity when you add projects. This leads to additional compile time and overhead when building the application.
If you follow a solid namespace convention, it is easy to refactor the solution into separate projects when the main project becomes too large or requires the separation. For example, you might start with a single projected called “JeremyLikness.FeedReader” with folders named “Search” and “Display” that have their own subfolders. If the project becomes too large, you can create a new project called “JeremyLikness.FeedReader.Search” and move all of the search items into that project. The namespace will be preserved and the only refactoring necessary will be to add the appropriate references and rebuild the application.
Keep it Design-Friendly with the MVVM Pattern
The MVVM pattern was covered in a previous article. One important benefit that using this pattern provides is a clean separate of design and development. The design for an application can be a lengthy process and it may change often. Traditional workflows often provide time at the beginning of the project to focus on design, and delay development until the design has been finalized. Silverlight, data-binding, and the MVVM pattern allow a parallel workflow that is far more efficient.
I worked on a large project called Looking Glass. The project called for nearly a dozen designers who produced the design over several months, moving from an initial set of wireframes to static “comps” followed by actual design assets provided in Illustrator files. The development team was also busy building the functionality of the application and the timeline required that both processes happened in parallel.
Using the MVVM pattern allowed the team to define a set of contracts up front. While the design was flexible, it was agreed that certain screens would exist and have a specific set of information. The design team focused on the presentation of the information and transition between screens while the development team built the business logic and other functionality required to obtain the information. The contract was defined by the view models that were created. Initially, “design-time” view models were built that provided sample data for the designers to work with, while the development team built the “run-time” view models that interfaced with web services and pulled in the real-time data from the backend servers.
Using MVVM allowed the design process and development process to move in parallel over the first few months. Developers used unit tests to validate their effort until the design team was finished. Once the design was complete, the XAML integration team used the design assets to build XAML based on the contracts provided by the design-time view models. The last step was simply to bind the XAML to the runtime view models created by the development team and the design ultimately merged with the development to produce a final work product. The calendar time for the project was cut almost in half by allowing the parallel design and development to take place.
Leverage the Managed Extensibility Framework
One challenge with creating modular code is discovering where implementations exist and integrating those with the main application. Fortunately, the Managed Extensibility Framework (MEF) is a built-in part of both the .NET and Silverlight frameworks that provides the services needed to pull all of the modules together. MEF provides several key services, including the following.
Extensibility
MEF enables your application to be extended through plug-ins. Specifically with Silverlight, additional modules can be downloaded at runtime and integrated with the project. MEF gives you the ability to define contracts for extension points to implement and even fire actions once the extensions are loaded.
Discovery
When following the SOLID principles listed earlier, one challenge with dependency injection and inversion of control is mapping the implementation to the control. MEF handles the inversion of control by taking responsibility for discovering implementations and making them available to classes that need them. You specify the requirement, and MEF supplies the solution if it is available within the project.
Lifetime Management
It is often important to determine the lifetime of a class when it is created for an application. For example, a short-lived helper class used by a view might be created temporarily for that view and destroyed when the view is no longer needed. A logging mechanism, however, might require a single class to be created and reused throughout the application; this is referred to as a Singleton. The Singleton pattern describes several ways to modify a class to force it to provide only a single instance, but this involves manipulating the class itself just to change its lifetime. MEF allows you to create the class ignorant of lifetime and specify how it should be shared or not shared as part of the contract. This makes it far easier to test as well as locate objects in your applications.
Metadata
The final functionality MEF provides is the ability to provide metadata about a module. This allows your application to make decisions about what modules to load and interface with. An application running in trial mode might interrogate “paid version” modules and reject those because the user has not yet upgraded to a paid account. If the system is offline, you can reject modules that require Internet connectivity. This is a very powerful feature that is used extensively in modular applications, especially ones that follow the MVVM pattern.
Load Modules on Demand
A final benefit of creating modular applications is the ability to load modules on demand. An extremely large software application might contain dozens of modules, while a particular user only has access to a smaller subset and may only use one or two in a given session. Packaging the modules in separate XAP files enables you to load the modules dynamically, as well as add new modules after the application is released to extend functionality.
There are three common module-loading scenarios:
- The first is just for speed of the application: instead of pulling down a large package, the user can pull down a lightweight package that displays a splash screen or perhaps a slide show while downloading the other required modules.
- The second scenario is to filter the modules available to the user based on his login and only download the modules they should have access to.
- Finally, the plug-in model involves looking in a well-known place for additional modules to extend the application with and loading them as they become available.
You can easily integrate dynamic XAP files using the Managed Extensibility Framework. To learn more, check out my LiveLessons video, “Fundamentals of the Managed Extensibility Framework (MEF).”
Final Thoughts
Building modular applications enables parallel workflows so that you can scale the development and design teams without conflict. It is easier to fix defects, test, maintain, and extend modular applications. The combination of MVVM and MEF makes it easy to build modular Silverlight LOB applications using the well-established SOLID principles for object-oriented design.