Section 3.4. Designing and Factoring Interfaces


3.4. Designing and Factoring Interfaces

Syntax aside, how do you go about designing interfaces? How do you know which methods to allocate to which interface? How many members should each interface have? Answering these questions has little to do with .NET and a lot to do with abstract component-oriented analysis and design. An in-depth discussion of how to decompose a system into components and how to discover interface methods and properties is beyond the scope of this book. Nonetheless, this section offers a few pieces of advice to guide you in your interface-design effort.

3.4.1. Interface Factoring

An interface is a grouping of logically related methods and properties. What constitutes "logically related" is usually domain-specific. You can think of interfaces as different facets of the same entity. Once you have identified (after a requirements analysis) all the operations and properties the entity supports, you need to allocate them to interfaces. This is called interface factoring. When you factor an interface, always think in terms of reusable elements. In a component-oriented application, the basic unit of reuse is the interface. Would this particular interface factoring yield interfaces that other entities in the system can reuse? What facets of the entity can logically be factored out and used by other entities?

Suppose you wish to model a dog. The requirements are that the dog be able to bark and fetch and that it have a veterinary clinic registration number and a property for having received shots. You can define the IDog interface and have different kinds of dogs, such as Poodle and GermanShepherd, implement the IDog interface:

     public interface IDog     {        void  Fetch(  );        void  Bark(  );        long  VetClinicNumber{get;set;}        bool  HasShots{get;set;}     }     public class Poo: IDog     {...}            public class GermanShepherd : IDog     {...} 

However, such a composition of the IDog interface isn't well-factored. Even though all the interface members are things a dog should support, Fetch and Bark are more logically related to each other than to VetClinicNumber and HasShots. Fetch( ) and Bark( ) involve one facet of the dog, as a living, active entity, while VetClinicNumber and HasShots involve a different facet, one that relates it as a record of a pet in a veterinary clinic. A better approach is to factor out the VetClinicNumber and HasShots properties to a separate interface called IPet:

     public interface IPet     {        long  VetClinicNumber{ get; set; }        bool  HasShots{ get; set; }     }     public interface IDog     {        void  Fetch(  );        void  Bark(  );     } 

Because the pet facet is independent of the canine facet, other entities (such as cats) can reuse the IPet interface and support it:

     public interface IPet     {        long  VetClinicNumber{ get; set; }        bool  HasShots{ get; set; }     }     public interface IDog     {        void  Fetch(  );        void  Bark(  );     }     public interface ICat     {        void  Purr(  );        void  CatchMouse(  );     }            public class Poodle : IDog,IPet     {...}     public class Siamese : ICat,IPet     {...} 

This factoring, in turn, allows you to decouple the clinic-management aspect of the application from the actual pet type (be it dogs or cats). Factoring operations and properties into separate interfaces is usually done when there is a weak logical relation between methods. However, identical operations are sometimes found in several unrelated interfaces, and these operations are logically related to their respective interfaces. For example, both cats and dogs need to shed fur and feed their offspring. Logically, shedding is just a dog operation, as is barking, and is also just a cat operation, as is purring. In such cases, you can factor the interfaces into a hierarchy of interfaces instead of separate interfaces:

     public interface IMammal     {        void ShedFur(  );        void Lactate(  );     }     public interface IDog : IMammal     {        void  Fetch(  );        void  Bark(  );     }     public interface ICat : IMammal     {        void  Purr(  );        void  CatchMouse(  );     } 

3.4.2. Factoring Metrics

As you can see, proper interface-factoring results in more specialized, loosely coupled, fine-tuned, and reusable interfaces, and subsequently, those benefits apply to the system as well. In general, interface factoring results in interfaces with fewer members. When you design a component-based system, however, you need to balance out two countering forces (see Figure 3-1).

If you have too many granular interfaces, it will be easy to implement each interface, but the overall cost of interacting with all those interfaces will be prohibitive. On the other hand, if you have only a few complex, large, poorly factored interfaces, the cost of implementing those interfaces will be a prohibitive factor, even though the cost of interacting with them might be low. In any given system, the total effort involved in designing and maintaining the components that implement the interfaces is the sum of those two factors. As you can see from Figure 3-1, there is an area of minimum

Figure 3-1. Balancing the number of components and their size


cost or effort in relation to the size and number of interfaces. Because these interface-factoring issues are independent of the component technology used, I can extrapolate from my own and others' experiences of factoring and architecting large-scale applications and share a few rules of thumb and metrics I have collected about interface factoring.

Interfaces with just one member are possible, but you should avoid them. An interface is a facet of an entity, and that facet must be pretty dull if you can express it with just one method or property. Examine that single method: is it using too many parameters? Is it too coarse, and therefore, should it be factored into several methods? Should you factor this method or property into an already existing interface?

The optimal number of interface members (in my opinion and experience) is between three and five. If you design an interface with more memberssay, six to nineyou are still doing relatively well. However, try to look at the members to determine whether any can be collapsed into each other, since it's quite possible to over-factor methods and properties. If you have an interface with 12 or more methods, you should definitely find ways to factor the members into either separate interfaces or a hierarchy of interfaces. Your coding standard should set some upper limit never to be exceeded, regardless of the circumstances (say, 20).

Another rule involves the ratio of methods to properties among interface members. Interfaces allow clients to invoke abstract operations, without caring about actual implementation details. Properties are what is known as just-enough-encapsulation. It's much better to expose a member variable as a property than to give direct access to it, because then you can encapsulate the business logic of setting and reading that variable's value in the object, instead of spreading it over the clients. Ideally, you shouldn't bother clients with properties at all. Clients should invoke methods and let the object worry about how to manage its state. Consequently, interfaces should have more methods than properties, by a ratio of at least 2:1. The one exception is interfaces that do nothing except define properties; such interfaces should have properties only, with no methods.

It's best to avoid defining events as interface members, if possible. Leave it up to the object to decide whether it needs an event member variable or not. As you'll see in Chapter 6, there is more than one way to manage events.

Is .NET Well-Factored?

After writing down my rules of thumb and metrics for interface factoring, I was curious to see how the various interfaces defined by the .NET Framework look in light of these points. I examined more than 300 interfaces defined by .NET. I excluded from the survey the COM interoperation interfaces redefined in .NET, because I wanted to look at native .NET interfaces only. I also excluded from the results the outliersinterfaces with 0 members and interfaces with more than 20 members. I consider an interface with more than 20 members to be a poorly factored one, not to be used as an example. On average, a .NET Framework interface has 3.75 members, with a methods-to-properties ratio of 3.5:1. Less than 3% of the members are events. These metrics nicely reaffirm the rules of thumb outlined in this section; you could say that on average, .NET interfaces are well-factored.


A word of caution about factoring metrics: rules of thumb and generic metrics are only tools to help you gauge and evaluate your particular design. There is no substitute for domain expertise and experience. Always be practical, apply judgment, and question what you do in light of the metrics.



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

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