This chapter introduced you to the application you will be building throughout this book. You should have an understanding of some of the tradeoffs you have when making your initial design decisions. Also, you gained some insight into basic security with IIS and the .NET Framework. You have also gained an understanding of your objects, where they reside, and how they will communicate. In Chapter 3, "Creating the Application Infrastructure," you will build a robust and reusable set of base classes that you will be able to inherit from when creating the user interface throughout the rest of the application. You will also create a basic set of working business objects that will allow you to retrieve data from SQL Server and display it to the user.
Chapter 3: Creating the Application Infrastructure
There are many aspects of an application's infrastructure, including overall application conventions, coding patterns, and basic sets of reusable functionality. In this chapter you will not only examine the application architecture and the structure of the remoting calls, but you will also start developing working code for your application. This involves creating the stored procedures, shared objects, data-centric and user-centric classes, and a basic Windows interface.
You will see the difference between the application's logical model and the physical implementation of that model. In doing this, you will learn some of the additional choices you need to make and the consequences of those choices.
The business objects created in this chapter will provide the basic structure for all of your business objects and will eventually give you the ability to reduce and consolidate your code.
The user interface consists of base classes that implement a number of the application's features and that you will inherit from for almost all of the other forms in the application. Once you have a small part of the application working, you will enhance the user experience by implementing finding and sorting capabilities and a custom print routine that you will write once but reuse everywhere.
Designing the Physical Application Architecture
Designing the physical application architecture is a very different process than designing the application logically. The logical design process is a more high-level type of design, and the physical design process says, "OK, I have this map for building the application, now how do I actually get from point A to point B?" Although this is not an in-depth book on application architecture, it gives you an overview of how you convert a logical model to a physical model.
Before you begin writing an application designed for an enterprise, it is important to understand the big picture—not only in your own application but also in the enterprise for which you are building it. Although this book does not show an enterprise's overall architecture, you can examine an application's architecture from both a high and low level. This is critical to writing a solid application.
A little more than two years ago I was working on a large enterprise application, which was billed as the latest-and-greatest financial tool for the company's financial division. The total time for development was supposed to be around three years. I came onto the application a year and a half into development, and at last word it was expected to last another five years.
Although this in itself is enough to raise eyebrows, it might surprise you even more that in the entire time that I was there, I never saw an architectural model. The application was written in Visual Basic (VB) 6. In July 2002, the number of lines of code (excluding comments) was around 45,000.
That is not such a large amount of code, but maybe this will put it into perspective: There were no DLLs in the application, and there were fewer than 40 classes. All of the coding was in the forms, and there was no object-oriented design or methodology applied. A rough guess by some of the developers put the line count of duplicated code at around 10,000. As such, different methods that had the same routines had the routines coded differently. There was no way to tell where code was duplicated or what code had to change when the business rules changed. It was complete chaos, all because there was no comprehensive view of the application.
In defense of the current team, the original development team (including the people in charge of the original design) was replaced. However, that just goes to show that everyone must understand the design of an application so that others, if needed, can take over and complete the application.
You should examine the architecture, at a minimum, at the deployment/ component model and object model levels. There are obviously other levels to any application model, but you will concentrate on these because they most directly relate to how you will build the application.
Examining the Component/Deployment Model
Chapter 1, "Understanding Application Architecture: An Overview," discussed the advantages of a three-tier architecture, so I do not repeat them here. Instead, you will examine the application's components and how they are deployed using a three-tier architecture. Also, you will learn how this relates to one specific technology in .NET: remoting. Figure 3-1 shows the component/deployment model you will use for the NorthwindTraders application.
Figure 3-1: NorthwindTraders component/deployment model
First, notice that there is no connection between the client workstation and the application server. These objects run independently of one another. Second, note that the NorthwindShared assembly is on both the client and the server. This must be the exact same file on both machines. Why? Well, that takes a little bit of explaining and is a perfect introduction to .NET remoting.
How Objects Communicate Using Remoting
Chapter 2, "Building an N-Tier Application," gave a brief overview of remoting. This section introduces the actual mechanism of remoting and how it works. There are several different ways to use remoting, but the method you will use consists of calls made on the interfaces of the remote component. This is kind of a confusing topic for VB 6 developers because although VB has interfaces, no one really used them. In this scenario, they are mandatory. So to begin with, what is an interface?
An interface is a set of methods and properties that have no functionality. Yes, you read it right—they have no functionality. The response to this statement is usually, "Why the heck would I want to code something that does not work?" Good question. Before going further, let's look at a simple interface:
Public Interface ITest Function Add(int1 as Integer, int2 as Integer) As Integer End Interface
The first thing everyone notices about an interface is that there is no body to the method signature. In other words, as mentioned, interfaces have no functionality. You cannot call interfaces directly because you cannot use them. Also, there is no scope declaration statement in front of the Add method signature—this is because everything in an interface is public. But how do you actually use the interface, and what is its part in making remote calls across process boundaries? Let's create a small sample application to demonstrate this.
Start Visual Studio (VS) .NET and create a new VB console application called InterfaceExample. Add the code in Listing 3-1.
Listing 3-1: The InterfaceExample Application
Module Module1 Sub Main() Dim objAdd As New cAdd Dim objTip As New CalcTip Console.WriteLine("Processing cAdd Class.") ProcessNumbers(objAdd) Console.WriteLine("Processing CalcTip Class.") ProcessNumbers(objTip) Console.ReadLine() End Sub Private Sub ProcessNumbers(ByVal objI As ITest) Console.WriteLine(objI.Add(4, 5)) End Sub End Module Public Interface ITest Function Add(ByVal int1 As Integer, ByVal int2 As Integer) As Integer End Interface Public Class cAdd Implements ITest Public Function Add(ByVal int1 As Integer, ByVal int2 As Integer) _ As Integer Implements ITest.Add Return int1 + int2 End Function End Class Public Class CalcTip Implements ITest Public Function Add(ByVal int1 As Integer, ByVal int2 As Integer) _ As Integer Implements ITest.Add Return (int1 + int2) * 9.25 End Function End Class
This is an interface (ITest) that is implemented by the classes cAdd and CalcTip. A ProcessNumbers method takes as an argument an object of type ITest. This process calls the Add method of the interface and passes it the values 4 and 5, and the result outputs to the screen. Except, this is the problem: How did the numbers get added together when you know that there is no functionality in the ITest interface? The answer is that the functionality is invoked in the cAdd and CalcTip classes, not the ITest interface. Because cAdd and CalcTest implement the ITest interface, they are also of type ITest. Therefore, any method that can accept an object of type ITest can accept any object that implements the interface. In this example, the ProcessNumbers method knew nothing about either of the classes that were passed to it, but because the classes implement an interface that ProcessNumbers does know about, it can make calls on the methods that are visible to it. In this example there are two entirely different classes being passed to the same method and a call is being made on those classes. However, the ProcessNumbers method does not know that one class is different from the other because this method sees the same thing—an object of type ITest. It can only make calls on methods that are known through the ITest interface. If you place any other methods in either of these classes, you would not be able to call them from the ProcessNumbers method because they would not be part of the ITest interface. And that is how you are going to implement remoting.
By placing the interfaces into the NorthwindShared component, which is hosted on both the client and the server, you can make calls on objects that you do not know anything about! As long as your server objects implement an interface that the client knows about, the client objects can make calls against the server objects.
Using SOAP and Binary Messages
As mentioned in Chapter 2, "Building an N-Tier Application," when using a channel there are two formats in which the data can be serialized: as a Simple Object Access Protocol (SOAP) message or as a binary message. This section discusses the differences between the two in terms of tradeoffs you need to make as you start building the application. When working in an intranet, you should use the binary formatter because it creates a more compact message stream; a SOAP call contains a large amount of XML markup that adds to the size of the message stream. However, using the binary formatter presents a small problem: It serializes the version information contained in the object that is being serialized and all of the methods and attributes contained within the object (in contrast, for example, the XmlSerializer only serializes a class's public attributes). This makes deserializing the object that you pass somewhat problematic because you can only deserialize an object serialized this way with the same version of the object. There are two ways to handle this situation. The first way is to have the local class implement the ISerializeable interface, create your own serialization routines, and only serialize what you want. One of the properties of the BinaryFormatterSink class is the includeVersions property.
The includeVersions property is not a well-documented property, and it is only available when using an HttpChannel for communication.
Setting the includeVersions to False strips the version information off of the message stream and does not require it to deserialize the object. However, this is a lot of work and requires that every object that needs to access your object perform these operations. A much simpler solution is to use a structure to pass information across the network. Structures are fairly small and can be included in the same file that contains the interface.
This process is analogous to using a User Defined Type (UDT) to store object information in VB 6 and then serializing it into a string and deserializing it on the receiving end. In this instance as well, the UDT must be available on both the sending and receiving side.
You will use this method in your application to simplify the implementation.
Creating the Object Model
Creating an object model before constructing the application is extremely important. This provides several advantages for the entire team. Developers can understand where their piece of code fits into the overall scheme of things. It also helps point out any weaknesses in design before they become too much of a problem to fix. Another huge advantage is that everyone can see that a piece of code has been written to perform a specific task, and they can reuse that object. Reusable pieces of code are great, but developers have to know they exist to be able to use them!
This section presents two object models. Figure 3-2 contains a logical view of the user-centric objects. Figure 3-3 contains the physical implementation of the user-centric objects. The objects are grouped into four subsections in each model. For the purpose of this book, you will create all of the objects in the section that contains the Employee, Territory, and Region objects.
Figure 3-2: NorthwindTraders user-centric logical object model
Figure 3-3: NorthwindTraders user-centric physical object model
Before getting into a discussion on the Unified Modeling Language (UML) model and what it implies, you should know how to derive an object model. As mentioned, you will use the Northwind database that comes with SQL Server. There are many theories for how to derive an object model, but for a business application you can derive the object model directly from the data model. If possible, you should try to create a one-to-one relationship between tables and objects. Figure 3-4 shows the tables and their relationships in the Northwind database.
Figure 3-4: The Northwind database tables and relationships
By comparing the logical object model and the data model, you should be able to see this correlation. The only real deviation is that there are not any objects to represent the join tables. The logical object model is similar to the logical data model in that join tables are generally not displayed on a logical model. However, Figure 3-3 shows a series of objects with the extension Mgr. These objects help maintain relationships represented by one-to-many or many-to-many relationships in the data model. You should note that the objects maintain the same relationship between each other that the tables maintain.
There are many reasons to have an object model that deviates from a data model, but you should do this with care. On the assumption that your data modeler understands the business, you must make sure that if you deviate, it is for a good reason. The objects must always model the business process or you are guaranteed to have problems later.
Understanding the UML Static Model
If you have not seen a UML static model before, this will require some explanation. Three basic types of relationships can occur between objects: dependencies, aggregations, and compositions. The differences between the three can often be just a matter of semantics; however, the implementation of these three types of relationships can make a big difference in the design of the object relationships:
A dependency is when one object has a relationship with another object in some way. It may be that one object instantiates another object or that one object may have a reference to the object. In an object model diagram, a dotted line with an arrow at the end denotes a dependency.
Aggregation is when one object (the client object) contains a reference to another object (the supplier object), but the supplier object does not depend on the client object for its lifetime. In terms of implementation, when a reference between objects is made ByRef, it is an aggregation. When the client object is destroyed, the supplier object remains because it was only referenced ByRef. In the object model diagram, a hollow diamond represents aggregation.
Composition is when the client object controls the lifetime of the supplier object. In terms of implementation, when a client object gets a reference (or creates a reference) to a supplier object, it is referenced ByVal. So, when a client object is destroyed, the supplier object is also destroyed. In the object model diagram, a filled-in diamond represents composition. In this object model, only the Order/Order Details relationship is represented this way. The reason is easy to understand: An order detail item must be part of an order, but it cannot exist on its own.
The question to ask now is, "What does this model imply about how the application will be built and what drawbacks do I face with this design?" You should almost always ask this question after you have finished a model. There is no such thing as a perfect model, so there are always decisions to make and things to understand about a model. I explain two implications.
You should first understand the implication in regard to the Region and Territory objects. The Territory object has a dependency on the Region object and not the other way around. So, you will always be able to discover the Region that a Territory is a part of, but you will not be able to discover the Territories contained within a Region. The Region object knows nothing about the Territory object in this instance. This affects the display of information to the user. In other words, using this model, you cannot show a screen that lists all of the Territories in each of the Regions. The reason why you would make this choice will become apparent after you understand the second implication of this part of the model.
The second implication involves the relationship between the Employee and Territory Manager. This relationship tells you that you can determine which territory an employee is in if you are looking at the employee, but you cannot tell which employees are part of a territory if you are looking at a territory. The reason why this design is OK is because this is an order-entry system—it is not a reporting system. In other words, it is a transactional system as opposed to a data-mining tool.
In a transactional system of this nature, you would normally start at the Order screen to take or enter an order. The employee who is taking the order would enter their name, so who cares what territory or region in which they are a part? You would need that type of information for month-end or year-end reports, so it is separate from this system.
An excellent book on the topic of objects and object-oriented design is The Art Of Objects by Yun-Tung Lau (Addison-Wesley, 2000). Although this book is a bit theoretical and is not for the faint of heart, it has everything you could ever want to know about objects and their relationships. A more down-to-earth book on applying UML is UML Distilled (Addison-Wesley, 1999). This is a quick-and-easy how-to manual that explains each of the diagrams used in UML modeling, how to use them, and what the notations mean.
Understanding the Data-Centric Object Model
In contrast to the user-centric object model, the data-centric object model is much simpler (see Figure 3-5).
Figure 3-5: NorthwindTraders data-centric object model
These objects check business rules, and they save, delete, and retrieve information from the database. There are no relationships between these objects (except for the Order and Order Details). It is up to the application that consumes them to use them in whatever fashion it chooses. Again, you should note that this is not entirely true—there is the one relationship between the Order and Order Detail objects. The reason for this is the composition of the objects. The Order Details do not know how to save themselves because they are part of an order. The Order object does know about the Order Details and knows how to save the order details. In terms of implementation, the Order Details object can save itself, but the information needs to come from the Order object.
Having examined the object models, you now have enough information to start building your application.