1. Trang chủ
  2. » Công Nghệ Thông Tin

Visual Basic 2005 Design and Development - Chapter 7 pps

42 319 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 42
Dung lượng 740,28 KB

Nội dung

Design Patterns A design pattern consists of one or more objects that together perform some useful task. In a sense, a design pattern is an algorithm for using classes to do something useful. It’s a recipe for building part of an application. A design pattern may include objects from a single class, or an ensemble of classes working together. As you refine your application’s design, you should start looking for common design patterns. You may discover a group of classes that implement a pattern. You may also find a group of classes that you could rewrite to follow a pattern. In either case, if you can rewrite your design to follow a design pattern, then you can take advantage of the solutions that other developers have developed to solve the same problem. That can save you time and gives you some confidence that the solution will work correctly so you can move on to design other parts of the application. Some patterns also make an application easier to implement, debug, and maintain. For example, the Adapter and Facade patterns described later in this chapter help decouple classes so they are less tightly related. That lets you build, debug, and modify the classes with less impact on the rest of the application. Since the seminal book Design Patterns by Gamma et. al (Boston: Addison-Wesley, 1995) was pub- lished, all sorts of patterns have been devised for data access, integration, interoperability, services, security, Web Services, configuration, exceptions, testing, threat modeling, legacy applications, project management, and many more. This chapter, or even this whole book, doesn’t have room to cover them all. Rather than just listing the names of a bunch of design patterns with no explana- tion, this chapter describes in more detail some of the patterns that I have found most useful in the applications that I’ve built. A lot of the design pattern books treat their patterns in a very formal way with introductory sec- tions for each pattern giving a synopsis, context, “forces,” and other sections eventually leading up to a solution, which is often so simple it’s anticlimactic. In some cases, the prelude sections are so stilted and full of technical diagrams that they’re practically unintelligible. This chapter takes a much more intuitive approach. 11_053416 ch07.qxd 1/2/07 6:31 PM Page 163 For a more complete treatment of design patterns, see a book entirely about them such as the original Design Patterns. This book was written by four authors (Gamma, Helm, Johnson, and Vlissides), who are sometimes called “the gang of four,” so the book is also sometimes called “the gang of four,” or GOF. You can also see the book Visual Basic Design Patterns by Mark Grand and Brad Merrill (Indianapolis: Wiley, 2005). The patterns that I find most useful can be grouped into three categories: creation patterns, relation patterns, and behavior patterns. Creation Patterns Creation patterns are patterns that deal with how objects are created. The Clone pattern provides a way to make new objects by copying existing ones. The Factory pattern uses one class to create instances of other classes. Clone Sometimes it’s useful to make an exact copy of an existing object. That’s what a clone is: a copy of an object that is initialized with the same property values as the object from which it was cloned. The Clone pattern is usually called the Prototype pattern in the design pattern literature. The idea is that the application can make a new object based on a prototype object by cloning the prototype. It’s relatively easy to allow cloning in Visual Basic. Simply add a Clone function to a class that returns a new instance of the class with the same parameters as the existing object. In fact, Visual Basic’s Object class provides a MemberwiseClone method that returns a new object with the same property values as the original object. Because all classes are descendants of the Object class, they can all have the MemberwiseClone method. The following code shows how a program might make a clone of a Customer object named old_customer: Dim new_customer As Customer = DirectCast(old_customer.MemberwiseClone, Customer) The MemberwiseClone method returns a generic Object so this code uses DirectCast to convert the result into a Customer object. To make cloning objects a little easier, you can make a Clone method that wraps up the call to MemberwiseClone. The following code shows how a simple Customer class can use MemberwiseClone to implement a Clone method: Public Class Customer ‘ Code omitted ‘ Return a shallow clone. Public Function Clone() As Customer 164 Part I: Design 11_053416 ch07.qxd 1/2/07 6:31 PM Page 164 Return DirectCast(Me.MemberwiseClone(), Customer) End Function End Class Now the main program can use this method, as in the following code: Dim new_customer As Customer = old_customer.Clone() The MemberwiseClone method makes a new copy of an object and initializes its fields to the same val- ues used by the original object. That works well for simple data types such as Integer or String but it doesn’t work as well with references to other objects. For example, suppose an Order item contains a reference to a Customer object. When you clone an Order, should the clone contain a reference to the same Customer object as the original, or should it contain a reference to a new Customer object that has been cloned from the original? If the new Order has a reference to the existing object, then changes to the joint Customer object are shared by both Orders. If the new Order object has a reference to a new copy of the Customer, then changes to the two Customer objects happen independently. A clone that uses the same references as the original object is called a shallow copy. The MemberwiseClone method makes shallow copies. A clone that uses references to new objects is called a deep copy. You could also define partially deep copies if you want to. For example, suppose an Order item has ref- erences to a Customer object, and a collection of references to OrderItem objects that describe the items that are included in the order. You might want an Order clone to share the same Customer object as the original, but get its own new OrderItems collection. This would let you easily make a new order for an existing customer. This kind of partially deep clone would be defined for a particular application, so there aren’t really any standards for defining them. The following code shows an Order class that can make shallow or deep copies: Public Class Order Public OrderCustomer As Customer Public OrderItems As New List(Of OrderItem) ‘ Code omitted ‘ Return a deep or shallow clone. Public Function Clone(ByVal deep As Boolean) As Order ‘ Start with a shallow clone. Dim new_order As Order = DirectCast(Me.MemberwiseClone(), Order) ‘ If appropriate, copy deeper data. If deep Then With new_order ‘ Clone the OrderCustomer. .OrderCustomer = Me.OrderCustomer.Clone() ‘ Clone the OrderItems list. .OrderItems = New List(Of OrderItem) For Each order_item As OrderItem In Me.OrderItems .OrderItems.Add(order_item.Clone()) 165 Chapter 7: Design Patterns 11_053416 ch07.qxd 1/2/07 6:31 PM Page 165 Next order_item End With End If Return new_order End Function End Class The Order class contains a reference to a Customer object and a generic list of OrderItem objects. The Clone method takes a parameter indicating whether it should return a shallow or deep copy. The function starts by using MemberwiseClone to make a shallow copy. If it should make a deep copy, it then sets the new object’s Customer and OrderItem references to clones of the original objects. In this example, the Customer and OrderItem classes don’t contain any references, so they only provide shallow copies, and the Order class’s Clone method doesn’t need to pass a parameter into the Customer and OrderItem class’s Clone methods. If those classes did support shallow and deep cloning, you would probably want to pass the same deep parameter into those methods. The following code shows how the Clones example program demonstrates cloning in the Customer and OrderItems classes: Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Dim txt_originals As String = “” Dim txt_clones As String = “” ‘ Make and clone a Customer. Dim cust1 As New Customer(“Rod”, “Stephens”, “1337 Leet St”, “Bugsville”, _ “AZ”, “87654”) Dim cust2 As Customer = cust1.Clone() txt_originals &= “*** cust1:” & vbCrLf & cust1.ToString() & “ ” & _ vbCrLf txt_clones &= “*** cust2:” & vbCrLf & cust2.ToString() & “ ” & vbCrLf ‘ Make and clone an Order. Dim order1 As New Order With order1 .OrderCustomer = New Customer(“Bob”, “Hacker”, “123 Decompile Ct”, _ “Attatash”, “ME”, “01234”) .OrderItems.Add(New OrderItem(“Cookies”, 12)) .OrderItems.Add(New OrderItem(“Pencil”, 2)) .OrderItems.Add(New OrderItem(“Notebook”, 6)) End With ‘ Make a shallow clone. Dim order2 As Order = order1.Clone(False) ‘ Make a deep clone. Dim order3 As Order = order1.Clone(True) ‘ Modify the original Order object. With order1.OrderCustomer 166 Part I: Design 11_053416 ch07.qxd 1/2/07 6:31 PM Page 166 .FirstName = “Mindy” .LastName = “Modified” .Street = “12 Altered Ave” .City = “Changed City” .State = “AL” .Zip = “98765” End With With order1.OrderItems(0) .Item = “Rice (grain)” .Quantity = 10000 End With ‘ Display the three Orders. txt_originals &= “*** order1: “ & vbCrLf & order1.ToString() & _ “ ” & vbCrLf txt_clones &= “*** order2: “ & vbCrLf & order2.ToString() & _ “ ” & vbCrLf txt_originals &= “*** order1: “ & vbCrLf & order1.ToString() & _ “ ” & vbCrLf txt_clones &= “*** order3: “ & vbCrLf & order3.ToString() & _ “ ” & vbCrLf ‘ Display the results. txtOriginals.Text = txt_originals txtClones.Text = txt_clones txtOriginals.Select(0, 0) txtClones.Select(0, 0) End Sub The code first creates Customer object. Its constructor initializes its properties. The code uses the Customer class’s Clone method to make a copy of the object. It then uses the objects’ ToString meth- ods to add textual representations of the objects to a pair of output strings. Next, the program makes an Order item, attaches a Customer object, and fills its OrderItems list with OrderItem objects. It then makes shallow and deep clones of the Order object. It modifies some of the values in the original object, and then adds textual representations of all three objects to the output strings. The code finishes by displaying the result strings. Figure 7-1 shows the result. Notice that the Customer object on the top left is the same as its clone on the right. The original Order item named order1 on the left matches the shallow copy order2 on the right. The deep copy order3 does not match, because it got its own new Customer and OrderItem objects when it was copied, so those items were not modified when the program altered the original Order’s data. Visual Basic defines an ICloneable interface to make cloning more consistent. Unfortunately, the inter- face defines a Clone method, but it doesn’t take a parameter indicating whether it should be a deep or shallow copy, and it returns a generic Object instead of a more specific object type. The intent is that the program will use MemberwiseClone to make a shallow copy, and Clone to make a deep copy. In either case, the main program must use DirectCast to convert the returned Object into an appropriate type. 167 Chapter 7: Design Patterns 11_053416 ch07.qxd 1/2/07 6:31 PM Page 167 Figure 7-1: Program Clones demonstrates shallow and deep clones. The ICloneable interface doesn’t provide much advantage over simply writing your own Clone method that takes a parameter indicating the kind of copy to make and returning a specific object type. Unless you need to work with another class that requires the ICloneable interface, you are just as well off writing your own Clone method. Factory A factory is a class that provides a means for creating instances of other classes. The main program cre- ates the Factory object and calls a method to create an instance of another class. Usually the factory could return more than one type of object depending on the circumstances. Pulling the decision-making process into the class helps isolate the main program from information about those created classes. Suppose it’s hard to decide what type of object to create. Perhaps the type of object depends on the user’s selections, the value of a calculation, or the data in a file. In that case, the program must perform some sort of complicated test, and then create the appropriate object. For example, suppose the user opens a file that might be a bitmap, text file, or database. The program must create a different kind of object to handle each choice. If file_info is a FileInfo object represent- ing the selected file, then you could use code similar to the following to create the right object. Here, FileProcessor is a parent class from which BitmapProcessor, TextFileProcessor, MdbProcessor, and OtherProcessor are derived: ‘ Make an appropriate FileProcessor. Dim file_processor As FileProcessor = Nothing Select Case file_info.Extension.ToLower Case “.bmp” file_processor = New BitmapProcessor(file_info) Case “.txt” file_processor = New TextFileProcessor(file_info) Case “.mdb” 168 Part I: Design 11_053416 ch07.qxd 1/2/07 6:31 PM Page 168 file_processor = New MdbProcessor(file_info) Case Else file_processor = New OtherProcessor(file_info) End Select ‘ Tell the FileProcessor to do its thing. fp.ProcessFile() The Select Case statement examines the file’s extension and creates an object of the appropriate FileProcessor subclass. The program then calls the processor’s ProcessFile method to take action. This code works, but it locks information about the file types into the main program’s code. If you later needed to change the types of files supported, or the method by which the program decides which type of processor to build, you must update the main program. You can pull the intelligence out of the main program into its own class by making a Factory class. The FileProcessorFactory class shown in the following code holds the logic to create different kinds of FileProcessor subclasses: ‘ Make various kinds of FileProcessor objects. Public Class FileProcessorFactory Public Function MakeFileProcessor(ByVal file_name As String) As FileProcessor Dim file_info As New FileInfo(file_name) ‘ See what kind of file this is. Dim file_processor As FileProcessor = Nothing Select Case file_info.Extension.ToLower Case “.bmp” file_processor = New BitmapProcessor(file_info) Case “.txt” file_processor = New TextFileProcessor(file_info) Case “.mdb” file_processor = New MdbProcessor(file_info) Case Else file_processor = New OtherProcessor(file_info) End Select Return file_processor End Function End Class This class contains a Select Case statement similar to the one shown previously, but now it’s not tied into the main program. The main program could use the following code to process files: ‘ Make a FileProcessorFactory. Dim fp_factory As New FileProcessorFactory() ‘ Make a FileProcessor. Dim fp As FileProcessor = fp_factory.MakeFileProcessor(dlgOpen.FileName) ‘ Tell the FileProcessor to do its thing. fp.ProcessFile() 169 Chapter 7: Design Patterns 11_053416 ch07.qxd 1/2/07 6:31 PM Page 169 The code makes a new FileProcessorFactory object and calls its MakeFileProcessor function to make an object from one of the FileProcessor subclasses. It then calls that object’s ProcessFile method. Notice that the factory creates instances of classes that all inherit from the FileProcessor base class. In general, the objects that the factory creates should either come from the same ancestor class, or imple- ment the same interface so the main program can do something meaningful with them. Otherwise, the program will need to use some sort of test such as If TypeOf returned_object Is Class1 Then and so forth. That pulls logic about the subclasses back into the main program and defeats much of the purpose of the factory. For another example, suppose you build a base class named ErrorProcessor. The subclass EmailError Processor sends error messages to a developer and the subclass LogFileErrorProcessor logs error messages into a file. When an error occurs, the program checks environment variables to see whether it should log errors into a file or send email. If it should send email, it checks other variables to decide to whom it should send the mail. You could put all of these tests in the main program and repeat them every time there is an error message, but it would be better to make an ErrorProcessorFactory to create an object from the appropriate ErrorProcessor subclass. I have also seen programs where developers made a factory class for each concrete class that they will want to create. For example, a CustomerFactory object creates a Customer object. Sometimes there may be a FactoryBase class from which the specific factory classes inherit. The FactoryBase class would define a CreateInstance function or some other method for creating an instance of the concrete class. In this scenario, you can use the factory objects to defer creating the concrete objects. For example, you can pass a CustomerFactory object into a subroutine that may need to make a Customer object. If the routine decides it doesn’t need a Customer object, it doesn’t need to create one. If the Customer class is compli- cated and hard to initialize, not creating a Customer can save the program some time and memory. Similarly, you can pass a factory object into a subroutine that can later use it to create an object from the concrete class, all without knowing what type of object it is creating. In this approach, the decision about the type of object was made earlier when you picked the appropriate factory class, but actually creating the object has been deferred. One misuse of factory classes that I’ve seen is when the program uses a set of factory classes to immedi- ately create instances of their classes. For example, the program creates an InvoiceFactory object and then immediately uses it to create an Invoice object. In that case, you could just as well have moved any initialization code from the InvoiceFactory into the Invoice class’s constructors. Another varia- tion would be to give the Invoice class a public shared function that returns an Invoice object. Either of these solutions avoids having to have the InvoiceFactory class, avoids the need to create instances of that class, and moves logic dealing with the Invoice class inside that class where it belongs. Relation Patterns Relation patterns deal with the ways in which objects are related to each other. They affect the object- oriented structure of the application. The Adapter, Facade, and Interface patterns all provide front-ends to classes. They allow one class to interact with one or more other classes in as simpler, more isolated way. That can help decouple the classes so they are easier to write, debug, and maintain separately. 170 Part I: Design 11_053416 ch07.qxd 1/2/07 6:31 PM Page 170 Adapter An Adapter class provides an interface between two classes. It lets two classes cooperate when they other- wise couldn’t because they have mismatched interfaces. For example, suppose you have written an event logging system that can log messages into a file or email messages to a developer. Now, suppose you decide you also want to be able to send messages to a pager. The method your code uses to log messages into a file or send them in an email doesn’t work the same way as the Web Service that you would call to send the message to a pager. To make the existing classes work with the new pager class, you could write an Adapter between them. The Adapter would let the main program call methods in the same way it does now when using a log file or email. It would then pass requests along to the Web Service in whatever format it requires. Figure 7-2 shows the situation graphically. Figure 7-2: An Adapter allows a program to work with an incompatible class. Because the Adapter wraps up another object, in this case a Web Service, it is also sometimes called a Wrapper. The Adapter pattern is closely related to the Interface and Facade patterns. Facade A Facade is basically an interface to a large and potentially complicated collection of objects that perform a single role. For example, a program might use DataGatherer, ReportFormatter, and ReportPrinter classes to build and print reports. You can make printing reports simpler by hiding the process behind a Facade. Figure 7-3 shows the idea graphically. Figure 7-3: A Facade presents a simple interface for a complex collection of objects. Application Facade DataGatherer RepeatFormatter ReportPrinter PagerServiceAdapterMessageLogger 171 Chapter 7: Design Patterns 11_053416 ch07.qxd 1/2/07 6:31 PM Page 171 Note that the classes do not need to be strictly contained in the Facade. For example, the DataGatherer class could be used in other operations, perhaps to build a report for display rather than printing. In that case, you might want another Facade to hide the details of report display. Interface An Interface is basically the same as an Adapter, but it is used for a different purpose. You use an Adapter to allow existing incompatible classes to work together. You use an Interface to isolate two classes so they can evolve independently. It helps keep two classes loosely coupled. For example, suppose your application generates a series of reports with a standard format. Early in the project, you may not have completely finalized the report format or the data that will be displayed in the reports. If you make the CustomerOrder class call the ReportGenerator class directly, then you will need to change the CustomerOrder code whenever the ReportGenerator code changes. When the data stored in the CustomerOrder class changes, you may also need to make changes to the ReportGenerator class. To decouple the classes and make development easier, you can insert an Interface between the two classes. The CustomerOrder class calls Interface methods and the Interface passes them along to the ReportGenerator. Now if you make changes to one of the classes, you only need to make correspond- ing changes to the Interface and the other class can remain unchanged. In practice, changes to one class often require changes to the other despite the Interface. For example, if you add new data to the CustomerOrder class, you’ll eventually need to change the ReportGenerator to display the new data. However, the Interface lets you change CustomerOrder without changing ReportGenerator right away. The developers working on that class can continue their work independently, and process the new data when they are ready to do so. I’ve worked on a couple of projects where tight coupling between classes made it very difficult for develop- ers working on the classes to get anything done. The first class would change and the second class would break until it was updated. The next day the second class would change and first would break until it was fixed. Adding an Interface between the two allowed them both to move forward more quickly. Behavior Patterns Behavior patterns deal with the behavior of parts of the application. The Abstract Base Class pattern uses a base class to determine the properties and behavior of derived classes. Chain of Responsibility and Chain of Events allow a program to apply a series of methods to an object or event until one of them handles it. The Command pattern lets you treat a method as an object that you can pass around to different pieces of code. Commands can be useful for implementing the Chain of Responsibility, Chain of Events, and Strategy patterns. 172 Part I: Design 11_053416 ch07.qxd 1/2/07 6:31 PM Page 172 [...]... System.EventArgs) Handles MyBase.Load m_Handlers.Add(New PrimeHandler()) m_Handlers.Add(New PowerHandler()) m_Handlers.Add(New EvenHandler()) m_Handlers.Add(New UnhandledHandler()) End Sub ‘ Process a number Private Sub btnProcess_Click(ByVal sender As System.Object, _ 177 11_053416 ch 07. qxd 1/2/ 07 6:31 PM Page 178 Part I: Design ByVal e As System.EventArgs) Handles btnProcess.Click Dim value As Integer... handler As NumberHandler In m_Handlers If handler.WasHandled(value) Then Exit For Next handler End Sub End Class When the program’s form loads, the code makes a List of NumberHandler objects and adds subclasses of the NumberHandler class to the list PrimeHandler processes prime numbers, PowerHandler processes numbers that are powers of other numbers, and EvenHandler processes even numbers The UnhandledHandler... whether the object processed an integer parameter Public MustInherit Class NumberHandler Public MustOverride Function WasHandled(ByVal value As Integer) As Boolean End Class 176 11_053416 ch 07. qxd 1/2/ 07 6:31 PM Page 177 Chapter 7: Design Patterns Child classes must override the WasHandled function The following code shows a PowerHandler class that displays a message if the input value is a power (greater... views and controllers because you can type new values into them The graph is also both a view and controller because it lets you click and drag to change a value Figure 7- 6 : Program ModelViewController displays data in text boxes, a label, and a graph 186 11_053416 ch 07. qxd 1/2/ 07 6:31 PM Page 1 87 Chapter 7: Design Patterns The following code shows the program’s ScoreModel class: ‘ Stores score data... notify the user and log the file in some way Then the NotifyUserErrorHandler’s WasHandled method could display a message and return False Next the program would let the EmailErrorHandler and LogErrorHandler objects also process the message Chain of Events A Chain of Events is similar to a Chain of Command, but it is somewhat more specific A Chain of Command sends an item to a series of handler objects... you can set a breakpoint in the property set procedure and examine the value as it is set 194 11_053416 ch 07. qxd 1/2/ 07 6:31 PM Page 195 Chapter 7: Design Patterns Building property procedures for every variable is a bit cumbersome In fact, this book and most other Visual Basic books usually make property variables public so that the code is simpler and easier to read However, property procedures give... 11_053416 ch 07. qxd 1/2/ 07 6:31 PM Page 181 Chapter 7: Design Patterns In this example, the RaiseEvent code loops through the stored delegates, invoking each until one of the sets was_handled to True That lets the program skip any remaining event handlers Example program ChainOfEvents uses event handlers that check a number to see whether it is prime, a power, or even If any of the event handlers takes... appropriate message and sets was_handled to True so that the program skips the remaining event handlers Custom events don’t give you an easy way to prioritize event handlers (other than the fact that they are executed in the registration order) If you need to order the handlers more dynamically, use the Chain of Command pattern described in the previous section Command In the Command design pattern, an... and, finally, display a message to the user You can give each of the objects a priority and sort the list of handlers so that the program tries them in priority order Note that an object’s WasHandled function should return True only if the item is completely handled so that other handlers don’t need to look at it An object’s WasHandled function might take some action to partially handle the item, and. .. class’s constructor to save a reference to the ScoreModel and it initializes m_Label 188 11_053416 ch 07. qxd 1/2/ 07 6:31 PM Page 189 Chapter 7: Design Patterns When it receives a DataModified event, the object concatenates all of the model’s values into a single string and displays the result in the label This is the label shown at the bottom of Figure 7- 6 The following code shows the ScoreGraphView class . _ 177 Chapter 7: Design Patterns 11_053416 ch 07. qxd 1/2/ 07 6:31 PM Page 177 ByVal e As System.EventArgs) Handles btnProcess.Click Dim value As Integer = Integer.Parse(txtValue.Text) For Each handler. NumberHandler Public MustOverride Function WasHandled(ByVal value As Integer) As Boolean End Class 176 Part I: Design 11_053416 ch 07. qxd 1/2/ 07 6:31 PM Page 176 Child classes must override the WasHandled. Handles MyBase.Load m_Handlers.Add(New PrimeHandler()) m_Handlers.Add(New PowerHandler()) m_Handlers.Add(New EvenHandler()) m_Handlers.Add(New UnhandledHandler()) End Sub ‘ Process a number. Private

Ngày đăng: 14/08/2014, 11:20

TỪ KHÓA LIÊN QUAN