Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 80 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
80
Dung lượng
0,98 MB
Nội dung
342 Thinking in C# www.ThinkingIn.NET .NET to program Windows Forms, it will place all the code relating to constructing the user-interface into a method called InitializeComponent( ); this method may be hundreds of lines long, but it contains no control-flow operators, so it’s length is irrelevant. On the other hand, the 15 lines of this leap year calculation are about as complex as is acceptable: //:c09:LeapYearCalc.cs using System; class LeapYearCalc { static bool LeapYear(int year){ if (year % 4 != 0) { return false; } else { if (year % 400 == 0) { return true; } else { if (year % 100 == 0) { return false; } else { return true; } } } } public static void Test(int year, bool val){ if (val == LeapYear(year)) { Console.WriteLine( "{0} correctly calced as {1}", year, val); return; } throw new TestFailedException( String.Format("{0} not calc'ed as {1}", year, val) ); } public static void Main(){ Test(1999, false); Test(2000, true); Test(1900, false); } Chapter 9: Coupling and Cohesion 343 } class TestFailedException : ApplicationException{ public TestFailedException(String s): base(s){ } }///:~ Some simple testing code is shown because, less than a month before this book went to press, we found a bug in the LeapYearCalc( ) function had! So maybe the 15 lines in that function are a little more complex than allowable… Make stuff as private as possible Now that we’ve introduced the concept of coupling and cohesion, the use of the visibility modifiers in C# should be more compelling. The more visible a piece of data, the more available it is to be used for common coupling or communicational and worse forms of cohesion. The very real advantages that come from object-orientation, C#, and the .NET Framework do not derive from the noun.Verb( ) form of method calls or from using brackets to specify scope. The success of the object-oriented paradigm stems from encapsulation, the logical organization of data and behavior with restricted access. Coupling and cohesion are more precise terms to discuss the benefits of encapsulation, but class interfaces, inheritance, the visibility modifiers, and Properties – the purpose of all of these things is to hide a large number of implementation details while simultaneously providing functionality and extensibility. Why do details need to be hidden? For the original programmer, details that are out of sight are out of mind, and the programmer frees some amount of his or her finite mental resources for work on the next issue. More importantly than this, though, details need to be hidden so software can be tested, modified, and extended. Programming is a task that is characterized by continuously overcoming failure: a missed semicolon at the end of a line, a typo in a name, a method that fails a unit test, a clumsy design, a customer who says “this isn’t what I wanted.” So as a programmer you are always revisiting existing work, whether it’s three minutes, three weeks, or three years old. Your productivity as a professional programmer is not governed by how fast you can create, it is governed by how fast you can fix. And the speed with which you can fix things is influenced by the number of details that must be characterized as relevant or irrelevant. Objects localize and isolate details. 344 Thinking in C# www.MindView.net Coupling, cohesion, and design trends Coupling and cohesion, popularized by Ed Yourdon and Larry Constantine way back in the 1970s, are still the best touchstones for determining whether a method or type is built well or poorly. The most important software engineering book of the 1990s was Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995) by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (the “Gang of Four”). What really set Design Patterns apart is that it was based on an archaeological approach to design; instead of putting their no-doubt-clever heads together and saying “Here’s a new way to solve this problem,” the book documents common structures and interactions (design patterns) that they found in proven software systems. When compared to other object-oriented design books, what leaps out about Design Patterns is the complete lack of references to objects that correspond to physical items in the real world and the recurring emphasis of techniques to decrease coupling and increase cohesion. An interesting question is whether low coupling and high cohesion are a cause of good design or a consequence of it. The traditional view has been that they are a consequence of design: you go into your cubicle, fire up your CASE tool, think deep thoughts, and emerge with a set of diagrams that will wow the crowds at the design review. This view is challenged by one of the better books of the past few years: Martin Fowler’s Refactoring: Improving the Design of Existing Code (Addison-Wesley, 1999). This book makes the fairly radical claim that taking “simple, even simplistic” steps on existing code, no matter how chaotic, leads to good design. Fowler goes even further and points out that without refactoring, the design of a system decays over time as the system is maintained; this is one of those obvious-in-retrospect observations that invalidates an entire worldview, in this case, the worldview that design is done with a diagramming tool and a blank piece of paper. Refactoring is changing the internal structure of your code without changing its internal behavior; Fowler presents a suite of refactorings and “code smells” to indicate when refactoring is needed. The book doesn’t explicitly address issues of Chapter 9: Coupling and Cohesion 345 coupling and cohesion 5 , but when viewed through the lens of structured design, refactoring is clearly driven by these concerns. Summary Any software project of more than a few hundred lines of code should be organized by a principle. This principle is called the software’s architecture. The word architecture is used in many ways in computing; software architecture is a characteristic of code structure and data flows between those structures. There are many proven software architectures; object-orientation was originally developed to aid in simulation architectures but the benefits of objects are by no means limited to simulations. Many modern-day projects are complex enough that it is appropriate to distinguish between the architecture of the overall systems and the architecture of different subsystems. The most prevalent examples of this are Web-based systems with rich clients, where the system as a whole is often an n-tier architecture, but each tier is a significant project in itself with its own organizing principle. Where the aims of architecture are strategic and organizational, the aims of software design are tactical and pragmatic. The purpose of software design is to iteratively deliver client value as inexpensively as possible. The most important word in that previous sentence is “iteratively.” You may fool yourself into believing that design, tests, and refactoring are wastes of time on the current iteration, but you can’t pretend that they are a waste of time if you accept that whatever you’re working on is likely to be revisited every three months, especially if you realize that if you don’t make things clear, they’re going to be going to be calling you at 3 o’clock in the morning when the Hong Kong office says the system has frozen 6 . Software design decisions, which run the gamut from the parameters of a method to the structure of a namespace, are best made by consideration of the principles of coupling and cohesion. Coupling is the degree to which two software elements are interdependent; cohesion is a reflection of a software element’s internal 5 Like Extreme Programming, another excellent recent book, Refactoring promotes homespun phrases like “code smells” and “the rule of three” that are no more or less exclusionary than the software engineering jargon they pointedly avoid. 6 Actually, they’ll call the IT guys first. That’s why it’s important to cultivate the perception that you know absolutely nothing about system administration and hardware. 346 Thinking in C# www.ThinkingIn.NET dependencies. Good software designs are characterized by loose coupling and high cohesion. With the rise of object orientation, the word “encapsulation” has come to be used to characterize all of the benefits of detail hiding, high cohesion, and loose coupling. At this halfway point in the book, we have covered C# as a language and the concepts of object-orientation. However, we’ve hardly scratched the surface of the .NET Framework SDK, hundreds of classes and namespaces that provide an object-oriented view of everything from data structures to user-interfaces to the World Wide Web. From hereon out, the concerns of the book are generally less specific to the C# language per se and more generally applicable to the capabilities that the .NET Framework would make available to any language. This does not mean that we’ve exhausted our discussion of the C# language, however. Some of the most interesting aspects of the C# language are yet to be introduced. Exercises 1. Try pair programming on one of the problems in the party domain. Try to reserve judgment until you've paired with programmers who are more, less, and similarly experienced. 2. Read Appendix C, “Test-First Programming with NUnit” and tackle a simple task in the party domain via test-first programming. 3. Write a one-page essay evaluating your personal experience with pair and test-first programming. 4. Fill in the following Venn diagram comparing aspects of software development with physical architecture. Chapter 9: Coupling and Cohesion 347 Software Development Architecture Shared 5. Write a one-page essay defending or refuting the statement “Software is architecture.” 6. The hardware manufacturers are thrilled with your work with the robotic party servant and want you to lead the development of all the robot's behavioral software. What kind of architecture will you adopt? Why? 7. Evaluate your party servant system. Use everything that you have learned to improve your design and implementation. 349 10: Collecting Your Objects It’s a fairly simple program that has only a fixed quantity of objects with known lifetimes. In general, your programs will always be creating new objects based on some criteria that will be known only at the time the program is running. You won’t know until run-time the quantity or even the exact type of the objects you need. To solve the general programming problem, you need to be able to create any number of objects, anytime, anywhere. So you can’t rely on creating a named reference to hold each one of your objects: MyType myObject; since you’ll never know how many of these you’ll actually need. To solve this rather essential problem, C# has several ways to hold objects (or rather, references to objects). The built-in type is the array, which has been discussed before. Also, the C# System.Collections namespace has a reasonably complete set of container classes (also known as collection classes). Containers provide sophisticated ways to hold and manipulate your objects. Containers open the door to the world of computing with data structures, where amazing results can be achieved by manipulating the abstract geometry of trees, vector spaces, and hyperplanes. While data structure programming lies outside of the workaday world of most programmers, it is very important in scientific, graphic, and game programming. Arrays Most of the necessary introduction to arrays was covered in Chapter 5, which showed how you define and initialize an array. Holding objects is the focus of this chapter, and an array is just one way to hold objects. But there is a number of other ways to hold objects, so what makes an array special? There are two issues that distinguish arrays from other types of containers: efficiency and type. The array is the most efficient way that C# provides to store 350 Thinking in C# www.ThinkingIn.NET and randomly access a sequence of objects (actually, object references). The array is a simple linear sequence, which makes element access fast, but you pay for this speed: when you create an array object, its size is fixed and cannot be changed for the lifetime of that array object. You might suggest creating an array of a particular size and then, if you run out of space, creating a new one and moving all the references from the old one to the new one. This is the behavior of the ArrayList class, which will be studied later in this chapter. However, because of the overhead of this size flexibility, an ArrayList is measurably less efficient than an array. The vector container class in C++ does know the type of objects it holds, but it has a different drawback when compared with arrays in C#: the C++ vector’s operator[] doesn’t do bounds checking, so you can run past the end 1 . In C#, you get bounds checking regardless of whether you’re using an array or a container— you’ll get an IndexOutOfRangeException if you exceed the bounds. As you’ll learn in Chapter 11, this type of exception indicates a programmer error, and thus you don’t need to check for it in your code. As an aside, the reason the C++ vector doesn’t check bounds with every access is speed—in C# you have the performance overhead of bounds checking all the time for both arrays and containers. The other generic container classes that will be studied in this chapter, ICollection, IList and IDictionary, all deal with objects as if they had no specific type. That is, they treat them as type object, the root class of all classes in C#. This works fine from one standpoint: you need to build only one container, and any C# object will go into that container. This is the second place where an array is superior to the generic containers: when you create an array, you create it to hold a specific type. This means that you get compile-time type checking to prevent you from putting the wrong type in, or mistaking the type that you’re extracting. Of course, C# will prevent you from sending an inappropriate message to an object, either at compile-time or at run-time. So it’s not much riskier one way or the other; it’s just nicer if the compiler points it out to you, faster at run-time, and there’s less likelihood that the end user will get surprised by an exception. Typed generic classes (sometimes called “parameterized types” and sometimes just “generics”) are not part of the initial .NET framework but will be. Unlike C++’s templates or Java’s proposed extensions, Microsoft wishes to implement support for “parametric polymorphism” within the Common Language Runtime itself. Don Syme and Andrew Kennedy of Microsoft’s Cambridge (England) Research Lab 1 It’s possible, however, to ask how big the vector is, and the at( ) method does perform bounds checking. Chapter 10: Collecting Your Objects 351 published papers in Spring 2001 on a proposed strategy and Anders Hjelsberg hinted at C#’s Spring 2002 launch that implementation was well under way. For the moment, though, efficiency and type checking suggest using an array if you can. However, when you’re trying to solve a more general problem arrays can be too restrictive. After looking at arrays, the rest of this chapter will be devoted to the container classes provided by C#. Arrays are first-class objects Regardless of what type of array you’re working with, the array identifier is actually a reference to a true object that’s created on the heap. This is the object that holds the references to the other objects, and it can be created either implicitly, as part of the array initialization syntax, or explicitly with a new expression. Part of the array object is the read-only Length property that tells you how many elements can be stored in that array object. For rectangular arrays, the Length property tells you the total size of the array, the Rank property tells you the number of dimensions in the array, and the GetLength(int) method will tell you how many elements are in the given rank. The following example shows the various ways that an array can be initialized, and how the array references can be assigned to different array objects. It also shows that arrays of objects and arrays of primitives are almost identical in their use. The only difference is that arrays of objects hold references, while arrays of primitives hold the primitive values directly. //:c10:ArraySize.cs // Initialization & re-assignment of arrays. using System; class Weeble { } // A small mythical creature public class ArraySize { public static void Main() { // Arrays of objects: Weeble[] a; // Null reference Weeble[] b = new Weeble[5]; // Null references Weeble[,] c = new Weeble[2, 3]; //Rectangular array Weeble[] d = new Weeble[4]; for (int index = 0; index < d.Length; index++) d[index] = new Weeble(); // Aggregate initialization: [...]... columns, so {{2, 4}, {5, 6}} becomes {{4. 25, 75} ,{1. 25, -0. 25} }: 362 Thinking in C# www.ThinkingIn.NET Horizontal transform 5 4 3 1 6 5. 5 0 .5 4. 25 2 75 Vertical transform 1. 25 -. 25 Figure 10-2: The Haar transform is a horizontal followed by vertical transform Wavelets have many interesting characteristics, including being the basis for some excellent compression routines, but are expensive to compute for... reference int[] g = new int [5] ; int[] h = new int[4]; for (int index = 0; index < h.Length; index++) h[index] = index*index; int[] i = { 11, 47, 93}; // Compile error: Use of unassigned local variable 'f' //! Console.WriteLine("f.Length=" + f.Length); Console.WriteLine("g.Length = " + g.Length); // The primitives inside the array are // automatically initialized to zero: 352 Thinking in C# www.MindView.net... zero if the 358 Thinking in C# www.ThinkingIn.NET argument is equal, and a positive value if the current object is greater than the argument Here’s a class that implements IComparable and demonstrates the comparability by using Array.Sort( ): //:c10:CompType.cs // Implementing IComparable in a class using System; public class CompType: IComparable { int i; int j; public CompType(int n1, int n2) { i... are pointing to the same array object on the heap The second part of ArraySize.cs shows that primitive arrays work just like object arrays except that primitive arrays hold the primitive values directly 354 Thinking in C# www.ThinkingIn.NET The Array class In System.Collections, you’ll find the Array class, which has a variety of interesting properties and methods Array is defined as implementing ICloneable,... Sequence coupling is usually acceptable (unlike, say, “internal data coupling” where one class directly writes to another class’s variables without using properties or methods to 374 Thinking in C# www.ThinkingIn.NET access the variables) Given that the check in VerticalTransform( ) is not within a loop, changing the contract doesn’t seem worth what will certainly be an unmeasurably small difference in performance... Can you reason why?: //:c10:InPlace.cs //Compile with: //csc /reference:FastBitmapper1.exe InPlace.cs //Add timing code to FastBitmapper to test speed using FastBitmapper; internal class InPlace : Transform { int length; int height; 372 Thinking in C# www.MindView.net int halfLength; int halfHeight; //Half the length of longer dimension double[] diff = null; private void LazyInit(double[,] matrix) { height... move references; in our case, the loops in Transform both read and write a large block of memory based on the original passed -in address The line fixed(double* pMatrix = matrix) pins the rectangular array matrix in memory and initializes a pointer to the beginning of that memory Pointers initialized in a fixed declaration are read-only and for the purposes of pointer Chapter 10: Collecting Your Objects... during debugging a single wavelet step is much easier to comprehend than a fully processed one, as Figure 10-4 illustrates) 366 Thinking in C# www.ThinkingIn.NET Figure 10-4: The results of one step of a Haar wavelet on a black-and-white photo The Transform2D( ) method iterates steps times over the matrix, first performing a horizontal transform and then performing a vertical transform Alternating... newList[0] != weebleList[0]; Console.WriteLine("New references == " + newReferences); //Copying a rectangular array works string[,] newSquareArray = new string[famousCouples.GetLength(0), famousCouples.GetLength(1)]; Array.Copy(famousCouples, newSquareArray, famousCouples.Length); 356 Thinking in C# www.MindView.net / /In- place sorting string[] sortedDays = new string[dayList.Length]; Array.Copy(dayList,... Console.WriteLine( "c.Length[{0}] = {1}", rank, c.GetLength(rank)); } // The references inside the array are // automatically initialized to null: for (int index = 0; index < b.Length; index++) Console.WriteLine("b[" + index + "]=" + b[index]); Console.WriteLine("d.Length = " + d.Length); Console.WriteLine("d.Length = " + d.Length); a = d; Console.WriteLine("a.Length = " + a.Length); // Arrays of primitives: int[] . 342 Thinking in C# www.ThinkingIn.NET .NET to program Windows Forms, it will place all the code relating to constructing the user-interface into a method called InitializeComponent(. Arrays of primitives: int[] f; // Null reference int[] g = new int [5] ; int[] h = new int[4]; for (int index = 0; index < h.Length; index++) h[index] = index*index; int[] i = { 11, 47,. issues that distinguish arrays from other types of containers: efficiency and type. The array is the most efficient way that C# provides to store 350 Thinking in C# www.ThinkingIn.NET and randomly