In-Depth
An Architecture for Your Mid-Size Apps
Learn how you can architect a structurally sound system that will grow over time and accommodate changing requirements.
- By Rob Keefer
- June 19, 2006
Software architecture was a big topic at last week's Tech-Ed conference. It's fun to think and talk about infrastructure and enterprise-wide architectures, but at the end of the day the typical software developer is building a system to support a business function, not an entire business. So, keeping in mind a small development team (say up to 10 developers) that is building a mid-size application (up to 100,000 lines of code), let's consider a good architecture that will support evolving business requirements, yet remain structurally sound.
An architecture should lend itself to being developed as a skeletal system containing the major communication paths throughout the system. At first the system will have little functionality. This skeletal system will grow incrementally over time to fulfill the requirements of the system.
A typical business system comprises a user interface (Web or desktop), business logic, and a database (see Figure 1). Software architects encourage the use of an N-tier architecture to construct such a system. Quite often, though, the discussion focuses on ensuring that the interfaces are descriptive and well-defined. The code itself is treated as inconsequential. So, let's look at some C# code snippets that illustrate what makes a system like this work.
User Interface (UI) Layer
The UI layer should be as thin and lightweight as possible. The primary responsibility of this layer is to display the user interface and interact with the user. Other components of the architecture should create data formats, table population, and lists, leaving this layer simply to present the required information.
Consider date formatting as an example. By allowing the Controller layer to format the date, the UI can present the most usable form of the date, but also enable the system to store the date in a form most useful for the rest of the system.
Another example of keeping the UI as thin as possible is a Grid control. In a Microsoft .NET application, it would be a good practice to build a DataSet object in the Controller layer, and use the DataSet in the Grid control. This method isolates the assembly of the data from the presentation of the data.
In this code snippet, the updateDataSet
method is called to refresh a Grid control that presents a list of books:
private void updateDataSet() {
bookListGrid.DataSource = _controller.GetDataSet();
bookListGrid.DataMember = "BookTable";
bookListGrid.CurrentRowIndex = (0);
}
The Windows Form that contains the bookListGrid
control would also instantiate a controller object (_controller
), and communicate with the rest of the system through this object.
Note that Visual Studio 2005 has a new feature that allows you to bind directly to a Value Object. This can come in handy as long as the heavy lifting for the object is done in the lower layers of the system. When you push the intense processing further down into the architecture, the UI layer stays as thin as possible, making it much easier to test. Unit tests for normal processing are much easier to write and maintain than tests for a UI. So to ensure a stable system, you should put the heavy processing in the architecture's lower layers.
Controller Layer
The Controller layer processes requests from the UI. The primary purpose of this layer often is to delegate processing to the Business layer. However, the Controller can also perform the important task of isolating the presentation of information from the manipulation of data.
Continuing with the DataSet example, the UI layer might use a DataSet to represent a table of data. DataSets are often difficult to manipulate, so the lower levels of the architecture might prefer arrays or maps. In this case, the Controller can unwrap the DataSet from the UI and repackage the data for the rest of the system to use, and vice versa.
In this code snippet, the Controller wraps up the data into a DataSet to pass data up to the UI:
public DataSet GetDataSet() {
DataSet returnDataSet = new DataSet(BOOK_DATA_SET);
DataTable newTable = returnDataSet.Tables.Add(BOOK_TABLE_NAME);
newTable.Columns.Add(BOOK_ID_COLUMN, typeof(string));
newTable.Columns.Add(BOOK_TITLE_COLUMN, typeof(string));
newTable.Columns.Add(AUTHOR_NAME_COLUMN, typeof(string));
DataRow newRow;
// populate the data set
foreach (BookValueObject valueObject
in _business.getBookList())
{
newRow = newTable.NewRow();
newRow[BOOK_ID_COLUMN] = valueObject.BookId;
newRow[AUTHOR_NAME_COLUMN] = valueObject.AuthorLastName + ", " +
valueObject.AuthorLastName;
newRow[BOOK_TITLE_COLUMN] = valueObject.BookTitle;
newTable.Rows.Add(newRow);
}
return returnDataSet;
}
Business Layer
The Business layer performs the system's most important processing, and as such is considered the workhorse of the system. The Controller sends processing requests to the Business layer, which contains the system's complex logic and data manipulation. The Business layer relies on a simple interface provided by the Data Access layer to access the data sources used by the system.
While the Business layer performs the primary processing of the system, sometimes it simply serves as a pass-through layer. In this example, the system simply needs to get data from the database, so the Business layer code is simple:
public List getBookList()
{
IBookDao bookDao = DaoFactory.getBookDao();
return bookDao.getAllBooks();
}
The Business layer instantiates an IBookDao
interface and calls the getBookDao
method to retrieve the book list. The use of a factory method isolates the Business layer from the Data Access layer. This enables the use of more than one data access method without requiring a change in the Business layer. Different Data Access layers might include mock data used for testing stored in XML, a relational database for production, or even a Web service.
If you simply change the value of DataAccess
in the App.config file, the system will use a different Data Access layer. The implementation of the DaoFactory shown here uses reflection to determine which Data Access Object has been requested:
public static IBookDao getBookDao()
{
return (IBookDao) newInstance("BookDao");
}
private static Object newInstance(string a_type)
{
Configuration config = new Configuration();
string dataAccess = config.get("DataAccess");
string typeName = DAO_NAMESPACE + a_type + dataAccess;
Type newType = Type.GetType( typeName, true );
return Activator.CreateInstance( newType );
}
Data Access Layer
The Data Access layer encapsulates all access to the data source. This layer, which manages the connection with the data source to obtain and store data, is the only layer that actually changes the data. The Data Access layer completely hides the data source implementation details from its clients by providing a simple interface. This interface does not change when the underlying data source implementation changes, thus enabling the Data Access layer to use different storage schemes without affecting the business components.
The Data Access layer might need to provide a Value Object Assembler, which works similarly to the Controller layer between the UI and the Business layers. The Value Object Assembler will use data objects retrieved from various data sources to construct a composite Value Object. The Value Object carries the data for the model to the Business layer in a single method call. The model data can be complex, so it is recommended that this Value Object be immutable. That is, the Business layer should use the Value Object for presentation and processing, but should not make changes to it. The Data Access layer should be the only layer that actually changes the data.
In this next example, much of the actual work for accessing an XML database is hidden in the BaseDaoXML
class, but it illustrates the fact that the Data Access layer establishes a connection to the database and manipulates the data:
public class BookDaoXML : BaseDaoXML, IBookDao {
public BookDaoXML() : base("Book", typeof(BookTable)) {
}
public List getAllBooks() {
return (BookTable)ReadData();
}
?
}
Data Source Layer
The Data Source could be a persistent store such as an RDBMS or XML database, a Web service, or a repository. Because it is easier to write unit tests for Data Access layer components than for stored procedures in the Data Source, experience has shown that this layer should be as thin as possible. Consider performing as much data manipulation in the Data Access layer as possible, and use techniques such as stored procedures and data caching when performance drops below required thresholds. Wait to use these techniques until you can determine that system performance is unsatisfactory without them.
This general-purpose architecture illustrates how a developer might structure a system that will grow over time and accommodate changing requirements. By keeping the interfaces between architecture layers clean, you can evolve a system that is structurally sound, yet meets business demands.