Becoming familiar with the design guidelines of the .NET Framework libraries will help you in using the framework and creating your own classes. A well-designed managed class library is:
For several reasons, you are strongly advised to treat these guidelines as prescriptive when building your own library. Developer productivity can be demonstrably hampered by inconsistent design. Development tools and add-ins will turn some of these guidelines into de facto rules, thereby reducing the value of nonconforming components . These nonconforming components may work, but will not function to their full potential. Even so, in some instances good library design dictates that you should break these rules. In such cases, you should provide solid justification for your decision. This section primarily focuses on creating libraries that are consistent and predictable. Chapter 4 focuses on security issues for libraries. The CLR makes it very easy to write multilanguage libraries by adhering to the Common Language Specification (CLS); see Chapter 2 for more information on the CLS. Naming GuidelinesOne of the most important elements of predictability and discoverability in a managed class library is the use of a consistent naming pattern. One goal with consistent naming is to avoid many common user questions once these conventions are understood and widely used. Naming guidelines focus on three issues:
This section focuses primarily on casing guidelines. For more information on mechanics and word choice, see the .NET Framework Design Guidelines. The .NET Framework uses two primary casing styles: one popularized by the Pascal language and one that is reminiscent of a hump on a camel.
All types of identifiers (such as type names and member names) should use Pascal casing except for protected instance fields (which generally should not be used, because properties are almost always a better choice) and parameter and local variables that should use camel casing. Keep in mind the following best practices for naming:
Member UsageHaving a well-designed library involves more than simply using the correct naming conventions. You must know how and when to use the different kinds of members on a type to express specific concepts. The library designer uses members to communicate such information to the developers using the particular library. For example, a designer might choose to use a property rather than a method to express the length of a collection because the semantics of a property most closely fit the need at hand. Properties Versus MethodsA library developer often faces the choice of when to use a method to represent functionality and when to use a property for this purpose. The underlying technology is flexible enough to allow any operation to be expressed as a property or a method. There are clear motivations to maintain two distinct concepts, however. Because of language syntax and consistent use in the .NET Framework, developers have come to think of properties as being synonymous with fields. They do not think of a field as being able to perform some operation; rather, a field simply holds information. This statement does not mean that the implementation of the property must simply return a field; complex cache schemes and event notifications are common within property accessors. Methods, on the other hand, are reserved for operations; they are active things that perform some operation on the state of the world. Here are some guidelines on the use of properties versus methods:
Properties that return arrays can be very misleading. Usually, it is necessary to return a copy of the internal array to ensure that the user cannot change the internal state. This requirement, coupled with the fact that a user could easily assume the member is an indexed property, leads to inefficient code such as the following: EmployeeList l = FillList(); for (int i = 0; i < l.Length; i++) { if (l.All[i]== x){...} } In the preceding code, each call to the All property creates a copy of the array ” n + 1 copies for this loop. Had the library designer used a method instead of a property, this line of code would look like the following: if (l.GetAll()[i]== x){...} This code more obviously indicates when the user is doing the wrong thing. Use of Indexed PropertiesIn the same way that properties represent logical backing stores, indexed properties represent logical arrays. They are often referred to as "indexers" because in their most common usage they overload the indexer operator ("[]" in C#, for example). In C#, you define a default indexed property in the following way: public object this[int index] { } The usage of such a property is very natural: Object o = l[42]; Following are best practices for using indexed properties:
Use of MethodsGuidelines for good use of methods can be summarized as follows :
Let's look at several of these guidelines in more detail. Virtual Versus Nonvirtual MethodsVirtual methods are points of specialization in your library and, as such, they must be carefully defined. Users can subtype your type and customize the implementation of any virtual method to do almost anything. For this reason, you must exercise caution whenever you call a virtual method in your library. Users will expect you to continue to call the virtual methods in the same way as you version your framework. Decide explicitly when to use virtual methods, and document clearly the contract of that member (for example, when you are guaranteed to call it). OverloadingMany languages do not provide a way for developers to select exactly which overload is called. In such a case, all methods with the same name should do the same thing, so it doesn't really matter which method is called. A good example involves String.Append() , which has overloads that take Int32 , Object , Double , and other types of arguments. Of course, all of these overloads do the same thing ”append the string version of the argument to a new string and return it. Another example of this problem involves String.IndexOf() . The beta release of the .NET Framework included overloads of IndexOf() that took a string and returned the index of the start of that substring in the string as well as an overload of IndexOf() that took a char array and returned the index of the first occurrence of any element in the array. This was a poor design, as these methods did semantically different things. The COBOL language clearly highlighted this issue. COBOL offers a subtle conversion from a string to a char array that prevented the COBOL user from being able to select which method was called. The Beta2 version of the .NET Framework fixed this problem by including an IndexOfAny method for the char array overload. Default ValuesIn a family of overloaded methods, the complex method should use parameter names that indicate a change from the default state assumed in the simple method. For instance, in the example below, the first method assumes that the lookup will not be case sensitive. In the second method, the name " ignoreCase " is used rather than " caseSensitive " because the former indicates how we are changing from the default behavior. 1: MethodInfo Type.GetMethod (String name); //ignoreCase = false 2: MethodInfo Type.GetMethod (String name, boolean ignoreCase); It is very common to use a zeroed state for the default value (such as 0, 0.0, false, or ""). Variable Numbers of ParametersWhere it is appropriate to have variable numbers of parameters to a method, use the convention of declaring N methods with increasing numbers of parameters and provide a method that takes an array of values for numbers greater than N. For most cases, N = 3 or N = 4 is appropriate. Make only the most complete overload virtual (if extensibility is needed) and define the other operations in terms of it. Listing 7.1 illustrates this point. Listing 7.1 Using variable numbers of parameterspublic class Foo { public void Bar(string a) { Bar (a, 0, false); } public void Bar(string a, int b) { Bar (a, b, false); } public void Bar(string a, int b, false c) { // core implementation here } } Use of TypesTypes are the unit of encapsulation in the runtime environment. As described in Chapter 2, two basic kinds of types exist:
Enumerations (enums) are special kinds of value types that serve as named constants. An interface type is a partial description of a value, potentially supported by many object types. Classes Versus InterfacesFrom a versioning perspective, interfaces are less flexible than classes are. With a class, you can ship version 1 of the application and then decide to add another method in version 2. As long as the method is not abstract (i.e., as long as you provide a default implementation of the method), any existing derived classes will continue to function with no changes.
Use of Value TypesThe .NET Framework has an extensible primitive type system. Value types are the mechanisms it offers to create your own types that behave like, and are as efficient as, the built-in primitive types such as Int32 and Double . You should use value types only to represent values that behave like primitive types ”that is, types that have value (copy) semantics by default. When you pass a value of a value type to a method, you expect to pass a copy of the value (pass by value), you expect assignments to copy the value, and you expect the method to return a copy. Use value types for types that meet any of the following criteria:
Use of EnumerationsEnumerations are the mechanism that the .NET Framework uses to support named constants. Enumerations are a much better way to represent named constant values than the traditional methods such as #define or public static constants, because the latter mechanisms do not offer compile-time type checking. Consider an API such as the following: public static FileStream Open (string name, int mode ) You have no idea what values to pass for mode , and you have no choice except to look up this information in the documentation (if it is documented at all!). If you use an enumeration to represent the mode, then the API becomes more self-documenting , as in the following case: public static FileStream Open (string name, FileMode mode ) You can even build tools to automatically show the possible options for the enumeration. Be aware that the .NET Framework supports C-style enumerations, so the compiler and runtime environment do not perform any type checking to ensure that the value passed into the enumeration is actually defined on the enumeration. For this reason, you must check all enumeration values to confirm that they are in the expected range. Listing 7.2 illustrates a common pattern for making sure enum values are defined. Listing 7.2 Checking enum valuespublic static FileStream Open (string name, FileMode mode) { switch (mode) { case FileMode.Append: //do Append break; case FileMode.Create: //do Create break; //etc... default: throw new ArgumentException("not a valid FileMode"); } //create new FileStream and return it |