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

Absolute C++ (4th Edition) part 84 pdf

10 448 0

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

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 10
Dung lượng 218,32 KB

Nội dung

20 Patterns and UML Einstein argued that there must be simplified explanations of nature, because God is not capricious or arbitrary. No such faith comforts the software engineer. Much of the complexity that he must master is arbi- trary complexity. F. Brooks, “No Silver Bullet: Essence and Accidents of Software Engineering,” IEEE Computer , April 1987. INTRODUCTION Patterns and UML are two software design tools that apply no matter what programming language you are using, as long as the language provides for classes and related facilities for object-oriented programming. This chapter presents a very brief introduction to these two topics. It contains no new details about the C++ language. A pattern in programming is very similar to a pattern in any other context. It is a kind of template or outline of a software task that can be realized as dif- ferent code in different, but similar, applications. UML is a graphical language that is used for designing and documenting software created within the object-oriented programming framework. This chapter uses some material from all of the chapters that come before. However, if you have read most, but not all, of the previous chapters, you can still get all or most of the benefit from reading this chapter. Patterns I bid him look into the lives of men as though into a mirror, and from others to take an example for himself. Terence (Publius Terentius Afer, 190–159 B . C .), Adelphoe Patterns are design principles that apply across a variety of software applica- tions. To be useful the pattern must apply across a variety of situations. To be substantive the pattern must make some assumptions about the domain of applications to which it applies. For example, the Iterator pattern applies to container classes of almost any kind. Recall that when we discussed iterators in Chapter 19, we first described them in the abstract as ways of cycling through a range of data in any kind of container. We then gave specific applications of 20.1 pattern 20_CH20.fm Page 838 Monday, August 18, 2003 2:08 PM Patterns 839 the Iterator pattern , such as list iterator, constant list iterator, reverse list iterator, con- stant reverse list iterator, vector iterator, constant vector iterator, reverse vector iterator, constant reverse vector iterator, and so forth. Using the overriding pattern of an iterator allowed you to organize your knowledge about container manipulation so that you could easily understand and communicate about software that used the container and iterator classes of the STL. Imagine the huge amount of detail you would have had to digest if we had pre- sented each kind of container iterator separately, with different names for begin( ) , end( ) , and ++ . Indeed, to make sense of that mountain of detail, you might have had to invent the Iterator pattern yourself. Now that you know about the Iterator pattern, you would surely see that pattern no matter how we presented iterators. However, until somebody had the insight to see and explain the pattern, the various iterators were a large number of different applications that seemed similar but were not organized by any overriding principles. Another related pattern we have been using is the Container pattern. In fact, the way patterns are usually organized, these would be seen as parts of a larger pattern known as the Container-Iterator pattern . This brief chapter can give only a taste of what patterns are all about. In this section we will discuss a few sample patterns to let you see what patterns look like. There are many more known and used patterns and many more yet to be explicated. This is a new and still developing field of software engineering. ■ ADAPTER PATTERN The Adapter pattern transforms one class into a different class without changing the underlying class, merely by adding a new interface. (The new interface replaces the old interface of the underlying class.) For example, in Chapter 19 we mentioned that the stack and queue template classes of the STL were adapter classes. We described both the stack and queue interfaces and said you could choose the underlying class that would actually store the data. For example, you can have a stack of int s with an under- lying vector, stack<int, vector<int> > or a stack of int s with an underlying list, stack<int, list<int> > (or a stack with some other underlying container class, but two is enough for our point). In either case, list or vector, the underlying class is not changed. Only an interface is added. How might the interface be added? That is an implementation detail that need not be part of the Adapter pattern. There are, however, at least two obvious ways to do it. For example, for a stack adapter the underlying container class could be a member vari- able of the stack class, or the stack class could be a derived class of the underlying container class. ■ THE MODEL-VIEW-CONTROLLER PATTERN The Model-View-Controller pattern is a way of dividing the I/O task of an applica- tion from the rest of the application. The Model part of the pattern performs the heart of the application. The View part is the output part; it displays a picture of the Model’s Iterator pattern Container- Iterator pattern Adapter pattern Model- View- Controller pattern 20_CH20.fm Page 839 Monday, August 18, 2003 2:08 PM 840 Patterns and UML state. The Controller is the input part; it relays commands from the user to the Model. Normally each of the three interacting parts is realized as an object with responsibilities for its own tasks. The Model-View-Controller pattern is an example of a divide-and- conquer strategy: One big task is divided into three smaller tasks with well-defined responsibilities. Display 20.1 diagrams the Model-View-Controller pattern. As a very simple example, the Model might be a container class, such as a stack. The View might display the top of the stack. The Controller gives commands to push or pop data on the stack. The Model (the stack) notifies the View to display a new top-of- stack value whenever the stack contents change. Any application can be made to fit the Model-View-Controller pattern, but it is par- ticularly well suited to GUI (graphical user interface) design projects, where the View can indeed be a visualization of the state of the Model. (A GUI is simply a windowing interface of the form you find in most modern software applications, as opposed to the simple text I/O we have used in this book.) For example, the Model might be an object to represent your list of computer desktop object names. The View could then be a GUI object that produces a screen display of your desktop icons. The Controller relays commands to the Model (which is a desktop object) to add or delete names. The Model object notifies the View object when the screen needs to be updated. We have presented the Model-View-Controller pattern as if the user were the Con- troller, primarily to simplify the example. The Controller need not be under the direct control of the user, but could be some other kind of software or hardware component. Display 20.1 Model-View-Controller Pattern Controller ManipulateNotify action1( ) action2( ) . . . data1 data2 . . . Model update( ) View 20_CH20.fm Page 840 Monday, August 18, 2003 2:08 PM Patterns 841 Example A S ORTING P ATTERN The most efficient sorting algorithms all seem to follow a similar pattern. Expressed recursively, they divide the list of elements to be sorted into two smaller lists, recursively sort the two smaller lists, and then recombine the two sorted lists to obtain the final sorted list. In Display 20.2 this pattern is expressed as a template function to sort an array into increasing order using the < operator. Our Sorting pattern uses a divide-and-conquer strategy. It divides the entire collection of ele- ments to be sorted into two smaller collections, sorts the smaller collections by recursive calls, and then combines the two sorted collections to obtain the final sorted array. The following is the heart of our Sorting pattern: int splitPt = split(a, begin, end); sort(a, begin, splitPt); sort(a, splitPt, end); join(a, begin, splitPt, end); Although the pattern imposes some minimum requirements on the functions split and join, the pattern does not say exactly how the functions split and join are defined. Different defini- tions of split and join will yield different sorting algorithms. Array indexes are examples of iterators, and we will use the notation [begin, end) from Chapter 19 to comment the pattern and indeed to think about and derive the pattern. The function split rearranges the elements in the interval [begin, end) and then divides the interval at a split point, splitPt. The two smaller intervals [begin, splitPt) and [splitPt, end) are then sorted by a recursive call to the function sort. Note that the split function both rearranges the elements in the array interval [begin, end) and returns the index splitPt that divides the interval [begin, end). After the two smaller intervals are sorted, the function join then combines the two sorted intervals to obtain the final sorted version of the entire larger interval. The pattern says nothing about how the function split rearranges and divides the interval [begin, end). In a simple case, split might simply choose a value splitPt between begin and end and divide the interval into the points before splitPt and the points after splitPt, with no rearranging. We will see an example that realizes the Sorting pattern by defining split this way. On the other hand, the function split could do something more elaborate, such as moving all the “small” elements to the front of the array and all the “large” elements toward the end of the array. This would be a step on the way to fully sorting the values. We will also see an example that realizes the Sorting pattern in this second way. The simplest realization of this Sorting pattern is the mm mm ee ee rr rr gg gg ee ee ss ss oo oo rr rr tt tt realization given in Display 20.3. A test program is given in Display 20.4. The merge sort is an example in which the definition of split is very simple. It divides the array into two intervals with no rearranging of elements. The join function is more complicated. After the two subintervals are sorted, it merges the two sorted subintervals, copying elements from the array to a temporary array. The merging starts by comparing the smallest elements in each smaller sorted interval. The smaller of these two ele- merge sort 20_CH20.fm Page 841 Monday, August 18, 2003 2:08 PM 842 Patterns and UML Display 20.2 Divide-and-Conquer Sorting Pattern 1 //This is the file sortpattern.cpp. 2 template <class T> 3 int split(T a[], int begin, int end); 4 //Rearranges elements [begin, end) of array a into two intervals 5 //[begin, splitPt) and [splitPt, end), such that the Sorting pattern works. 6 //Returns splitPt. 7 template <class T> 8 void join(T a[], int begin, int splitPt, int end); 9 //Combines the elements in the two intervals [begin, split) and 10 //[splitPt, end) in such a way that the Sorting pattern works. 11 template <class T> 12 void sort(T a[], int begin, int end) 13 //Precondition: Interval [begin, end) of a has elements. 14 //Postcondition: The values in the interval [begin, end) have 15 //been rearranged so that a[0] <= a[1] <= <= a[(end - begin) - 1]. 16 { 17 if ((end - begin) > 1) 18 { 19 int splitPt = split(a, begin, end); 20 sort(a, begin, splitPt); 21 sort(a, splitPt, end); 22 join(a, begin, splitPt, end); 23 }//else sorting one (or fewer) elements, so do nothing. 24 } 25 template <class T> 26 void sort(T a[], int numberUsed) 27 //Precondition: numberUsed <= declared size of the array a. 28 //The array elements a[0] through a[numberUsed - 1] have values. 29 //Postcondition: The values of a[0] through a[numberUsed - 1] have 30 //been rearranged so that a[0] <= a[1] <= <= a[numberUsed - 1]. 31 { 32 sort(a, 0, numberUsed); 33 } Display 20.3 Merge Sort Realization of Sort Pattern (part 1 of 2) 1 //File mergesort.cpp: the merge sort realization of the Sorting pattern. 2 template <class T> 3 int split(T a[], int begin, int end) 4 { 5 return ((begin + end)/2); 6 } 20_CH20.fm Page 842 Monday, August 18, 2003 2:08 PM Patterns 843 Display 20.3 Merge Sort Realization of Sort Pattern (part 2 of 2) 7 template <class T> 8 void join(T a[], int begin, int splitPt, int end) 9 { 10 T *temp; 11 int intervalSize = (end - begin); 12 temp = new T[intervalSize]; 13 int nextLeft = begin; //index for first chunk 14 int nextRight = splitPt; //index for second chunk 15 int i = 0; //index for temp 16 //Merge till one side is exhausted: 17 while ((nextLeft < splitPt) && (nextRight < end)) 18 { 19 if (a[nextLeft] < a[nextRight]) 20 { 21 temp[i] = a[nextLeft]; 22 i++; nextLeft++; 23 } 24 else 25 { 26 temp[i] = a[nextRight]; 27 i++; nextRight++; 28 } 29 } 30 31 while (nextLeft < splitPt)//Copy rest of left chunk, if any. 32 { 33 temp[i] = a[nextLeft]; 34 i++; nextLeft++; 35 } 36 while (nextRight < end) //Copy rest of right chunk, if any. 37 { 38 temp[i] = a[nextRight]; 39 i++; nextRight++; 40 } 41 for (i = 0; i < intervalSize; i++) 42 a[begin + i] = temp[i]; 43 } 20_CH20.fm Page 843 Monday, August 18, 2003 2:08 PM 844 Patterns and UML Display 20.4 Demonstrating the Sorting Pattern 1 //Tests the Divide-and-Conquer Sorting pattern. 2 #include <iostream> 3 using std::cout; 4 using std::cin; 5 using std::endl; 6 #include "sortpattern.cpp" 7 #include "mergesort.cpp" 8 void fillArray(int a[], int size, int& numberUsed); 9 //Precondition: size is the declared size of the array a. 10 //Postcondition: numberUsed is the number of values stored in a. 11 //a[0] through a[numberUsed - 1] have been filled with 12 //nonnegative integers read from the keyboard. 13 14 int main( ) 15 { 16 cout << "This program sorts numbers from lowest to highest.\n"; 17 int sampleArray[10], numberUsed; 18 fillArray(sampleArray, 10, numberUsed); 19 sort(sampleArray, numberUsed); 20 cout << "In sorted order the numbers are:\n"; 21 for (int index = 0; index < numberUsed; index++) 22 cout << sampleArray[index] << " "; 23 cout << endl; 24 return 0; 25 } 26 void fillArray(int a[], int size, int& numberUsed) 27 < The rest of the definition of fillArray is given in Display 5.5. > S AMPLE D IALOGUE This program sorts numbers from lowest to highest. Enter up to 10 nonnegative whole numbers. Mark the end of the list with a negative number. 80 30 50 70 60 90 20 30 40 -1 In sorted order the numbers are: 20 30 30 40 50 60 70 80 90 20_CH20.fm Page 844 Monday, August 18, 2003 2:08 PM Patterns 845 ments is the smallest of all the elements in either subinterval, and so it is moved to the first posi- tion in the temporary array. The process is then repeated with the remaining elements in the two smaller sorted intervals to find the next smallest element, and so forth. There is a trade-off between the complexity of the functions split and join. You can make either of them simple at the expense of making the other more complicated. For the merge sort, split was simple and join was complicated. We next give a realization in which split is com- plicated and join is simple. Display 20.5 gives the qq qq uu uu ii ii cc cc kk kk ss ss oo oo rr rr tt tt realization of our Sorting pattern. If the line #include "mergesort.cpp" in Display 20.4 is replaced by the following: #include "quicksort.cpp" then the program will give the same input and output. The files mergesort.cpp and quick- sort.cpp give two different realizations of the same Sorting pattern. In the quick-sort realization, the definition of split is quite sophisticated. An arbitrary value in the array is chosen; this value is called the ss ss pp pp ll ll ii ii tt tt tt tt ii ii nn nn gg gg vv vv aa aa ll ll uu uu ee ee . In our realization we chose a[begin], as the splitting value, but any value will do equally well. The elements in the array are rearranged so that all those elements that are less than or equal to the splitting value are at the front of the array and all the values that are greater than the splitting value are at the other end of the array; the splitting value is placed so that it divides the entire array into these smaller and larger ele- ments. Note that the smaller elements are not sorted and the larger elements are not sorted, but all the elements before the splitting value are smaller than any of the elements after the splitting value. The smaller elements are sorted by a recursive call, the larger elements are sorted by another recursive call, and then these two sorted segments are combined with the join function. In this case the join function is as simple as it possibly could be: It does nothing. Since the sorted smaller elements all preceed the sorted larger elements, the entire array is sorted. (The quick-sort realization can be done without the use of a second temporary array (temp). However, that detail would only distract from the message of this example. In a real application, you may or may not, depending on details, want to consider the possibility of using the quick- sort realization without a temporary array.) ■ EFFICIENCY OF THE SORTING PATTERN Essentially any sorting algorithm can be realized using this Sorting pattern. However, the most efficient implementations are those for which the split function divides the array into two substantial chunks, such as half and half, or one-fourth and three- fourths. A realization of split that divides the array into one, or a very few, elements and the rest of the array will not be very efficient. quick sort splitting value 20_CH20.fm Page 845 Monday, August 18, 2003 2:08 PM 846 Patterns and UML Display 20.5 Quick-Sort Realization of Sorting Pattern (part 1 of 2) 1 //File quicksort.cpp: the quick-sort realization of the Sorting pattern. 2 #include <algorithm> 3 using std::swap; 4 template <class T> 5 int split(T a[], int begin, int end) 6 { 7 T *temp; 8 int size = (end - begin); 9 temp = new T[size]; 10 T splitV = a[begin]; 11 int up = 0; 12 int down = size - 1; 13 //Note that a[begin] = splitV is skipped. 14 for (int i = begin + 1; i < end; i++) 15 { 16 if (a[i] <= splitV) 17 { 18 temp[up] = a[i]; 19 up++; 20 } 21 else 22 { 23 temp[down] = a[i]; 24 down ; 25 } 26 } 27 //0 <= up = down < size 28 temp[up] = a[begin]; //Positions the split value, splitV. 29 //temp[i] <= splitV for i < up; temp[up] = splitV; temp[i] > splitV for 30 //i > up. So, temp[i] <= temp[j] for i in [0, up) and j in [up, end). 31 for (i = 0; i < size; i++) 32 a[begin + i] = temp[i]; 33 34 if (up > 0) 35 return (begin + up); 36 else 37 return (begin + 1); //Ensures that both pieces are nonempty. 38 } 20_CH20.fm Page 846 Monday, August 18, 2003 2:08 PM Patterns 847 Tip For example, the merge sort realization of split divides the array into two roughly equal parts, and a merge sort is indeed very efficient. It can be shown, although we will not do so here, that the merge sort runs in time O(N log N) and that any sorting algo- rithm (that meets some minimal and reasonable constraints) cannot be faster than O(N log N). So, a merge sort is, in some sense, optimal. The quick-sort realization of split divides the array into two portions that might be almost equal or might be very different in size depending on the choice of a splitting value. Since in extremely unfortunate cases the split might be very uneven, the worst- case running time for a quick sort is O(N 2 ), which is much slower than the O(N log N) we obtained for the merge sort. However, for an array that is filled with randomly cho- sen values, most splitting values will produce a division that is close enough to an even division. So, under a suitable definition of average case, the quick sort has an average- case running time that is O(N log N). In practice, the quick sort is one of the best- performing sorting algorithms. The selection sort algorithm, which we discussed in Chapter 5, divides the array into two pieces, one with a single element and one with the rest of the array interval (see Self-Test Exercise 2). Because of this uneven division, the selection sort has a worst-case, and even average-case, running time that is O(N 2 ). In practice, the selec- tion sort is not a very fast sorting algorithm, although it does have the virtue of simplic- ity. We do not have room to provide proof of these running times in this book, but you can find such results in almost any data structures or analysis of algorithms text. P RAGMATICS AND P ATTERNS You should not feel compelled to follow all the fine details of a pattern. Patterns are guides, not requirements. For example, we did the quick-sort implementation by exactly following the pat- tern in order to have a clean example. In practice, we would have taken some liberties. Notice that, with a quick sort, the join function does nothing. In practice, we would simply eliminate the calls to join. These calls incur overhead and accomplish nothing. Display 20.5 Quick-Sort Realization of Sorting Pattern (part 2 of 2) 39 template <class T> 40 void join(T a[], int begin, int splitPt, int end) 41 { 42 //Nothing to do. 43 } 44 20_CH20.fm Page 847 Monday, August 18, 2003 2:08 PM . applica- tion from the rest of the application. The Model part of the pattern performs the heart of the application. The View part is the output part; it displays a picture of the Model’s Iterator. August 18, 2003 2:08 PM 840 Patterns and UML state. The Controller is the input part; it relays commands from the user to the Model. Normally each of the three interacting parts is realized as. return ((begin + end)/2); 6 } 20_CH20.fm Page 842 Monday, August 18, 2003 2:08 PM Patterns 843 Display 20.3 Merge Sort Realization of Sort Pattern (part 2 of 2) 7 template <class T> 8 void

Ngày đăng: 04/07/2014, 05:21

TỪ KHÓA LIÊN QUAN