Design Guidelines


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:

  • Consistent: Similar design patterns are implemented across libraries.

  • Predictable: Functionality is easily discoverable. There is typically only one way to perform a specified task.

  • Secure: The library is callable from semi-trusted code.

  • Multilingual: Functionality is accessible to many different programming languages.

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 Guidelines

One 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:

  • Casing : using the correct capitalization style

  • Mechanics: using the correct nouns for class names, verbs for method names , and so forth

  • Word choice: using terms consistently across libraries

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.

  • Pascal casing: This convention capitalizes the first character of each word. Example: B ack C olor.

  • Camel casing: This convention capitalizes the first character of each word except the first word. Example: b ack C olor.

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:

  • Do not use names that require case sensitivity, as components must be fully usable in both case-sensitive and case-insensitive languages. Because case-insensitive languages cannot distinguish between two names within the same context that differ only by case, components must avoid this situation.

  • Do not use Hungarian notation [2] for parameter or variable names. Instead, use descriptive parameter names based on a parameter's or variable's meaning or semantics, rather than its type.

    [2] Hungarian notation (HN) is a naming convention invented by Charles Simonyi. Hungarian notation names identifiers with a lowercase prefix to identify the class or usage of the variable. Examples: m_nSize , hwndParent , and lpszFile .

  • Do not use abbreviations in identifiers (including parameter names), if possible. If you must use abbreviations, then follow the casing rules for that type of identifier for any abbreviation consisting of more than two characters, even if it is not the standard abbreviation. (Use all uppercase for abbreviations consisting of two or fewer characters ). Example: namespace System.Web.UI .

  • Do not specify the same name for namespaces and classes. For example, do not use Debug for a namespace name and have a class named Debug . Also, avoid using identifiers that conflict with common keywords in other languages such as Alias and Select .

Member Usage

Having 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 Methods

A 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:

  • Do use a property if the member has a logical backing store.

  • Do use a method:

    • If the operation is a conversion (such as Object.ToString() ).

    • If the operation is expensive (orders of magnitude slower than a field set would be).

    • If the get accessor has an observable side effect.

    • If calling the member twice in succession produces different results.

    • If the order of execution is important.

    • If the member is static but returns a mutable value.

    • If the member must return an array.

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 Properties

In 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:

  • Do use only one named indexed property per class and make it the default indexed property for that class. (The C# language enforces this practice automatically.)

  • Do not use nondefault indexed properties. (The C# language enforces this practice automatically.)

  • Do use the name "Item" for indexed properties unless there is an obviously better name (for example, a Chars property on String ). (The C# language enforces this practice automatically.)

  • Do use indexed properties when the logical backing store is an array.

  • Do not mix indexed properties and overloaded methods.

Use of Methods

Guidelines for good use of methods can be summarized as follows :

  • Do default to having nonvirtual methods when possible.

  • Do use overloading only when providing different methods that do semantically the same thing.

  • Do favor overloading rather than default arguments (default arguments do not version well and are not allowed in the CLS).

  • Do use default values correctly.

  • Do not use "reserved" parameters. If more data are needed in the next version, you can add more overloading.

  • Do use overloading for variable numbers of parameters.

Let's look at several of these guidelines in more detail.

Virtual Versus Nonvirtual Methods

Virtual 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).

Overloading

Many 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 Values

In 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 Parameters

Where 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 parameters
 public 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 Types

Types are the unit of encapsulation in the runtime environment. As described in Chapter 2, two basic kinds of types exist:

  • Object types ( classes ) are by far the most common kind of types used. Classes can be abstract (subclasses must provide an implementation) and sealed (no subclasses are allowed).

  • Value types describe values that are represented as sequences of bits.

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 Interfaces

From 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.

  • Do favor using classes over any other type.

  • Do use interfaces:

    • If you have concrete examples where several unrelated classes want to support the protocol.

    • If these classes already have established base classes (i.e., some are UI controls, some are Web services).

    • If aggregation is not appropriate or practical.

Use of Value Types

The .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:

  • The types act like primitive types.

  • Value semantics are desirable.

  • The instance size is less than 16 bytes (with anything larger, the copying may become expensive).

  • The types are immutable (they offer no way to change the state of an instance).

Use of Enumerations

Enumerations 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 values
 public 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 


Programming in the .NET Environment
Programming in the .NET Environment
ISBN: 0201770180
EAN: 2147483647
Year: 2002
Pages: 146

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net