Interfaces, Abstract Classes, and Inversion of Dependency
In Framework Design Guidelines (Cwalina and Abrams, Addison-Wesley), the authors have a section in Chapter Four entitled "Choosing Between Class and Interface" that is very revealing about the "behind the scenes" goings on during the development of the .NET Framework.
They say that in general, classes are the preferred construct for exposing abstractions, the logical basis of this being that once you ship an interface, the set of members is baked forever - any additions would break existing types that implement the interface.
Classes are much more flexible - you can add members to classes that have already shipped, and as long as the method has a default implementation, existing derived classes continue to function undisturbed.
They provide an example (although not a very good one) of how difficult it would be to add timeout support for streams. All of the options have substantial development cost and usability issues.
It seems that one of the primary arguments for interfaces is that they allow separating contract from implementation. However, this incorrectly assumes that you can't separate contracts from implementation using classes. Abstract classes residing in a separate assembly from their concrete implementations are an excellent way to achieve this kind of separation.
Bottom line, "DO" favor defining classes over interfaces. Of course this does not address the concept of being able to dynamically load an assembly with a class that implements a specified interface and cast the object to an instance of the I<Whatever> interface it is, and make method calls on it.
The Inversion of Dependency concept comes from work by Robert Martin.
First he defines "Bad Design", before getting on to his concept. To Martin, "Bad Design" equals the following three concepts:
1.It is hard to change because every change affects too many other parts of the sys-
tem. (Rigidity)
2.When you make a change, unexpected parts of the system break. (Fragility)
3.It is hard to reuse in another application because it cannot be disentangled from
the current application. (Immobility)
Martin states that it would be difficult to demonstrate that a piece of software that exhibits none of those traits, i.e. it is flexible, robust, and reusable, and that it also fulflls all its requirements, has a bad design. Thus, we can use these three traits as a way to unambiguously decide if a design is "good" or "bad".
Following this, Martin explains that it is the interdependence of the modules within that design that is the root cause of "bad design".
He illustrates with an example of a "Copy" Program that has two subprograms, "Read Keyboard" and "Write Printer" and shows how the Copy module is not reusable in any situation that does not involve a keyboard and a printer.
This leads to Martin's "Dependency Inversion Principle":
A) HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES -- BOTH SHOULD DEPEND UPON ABSTRACTIONS
B) ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS -- DETAILS SHOULD DEPEND UPON ABSTRACTIONS
This leads us to the abstraction interfaces:
This arrangement allows us to plug ANY kind of "Reader" into the abstract READER interface, along with ANY kind of Writer into the WRITER interface, and achieve the kind of usability we want from software. This Copy class does not depend upon the Keyboard Reader nor the Printer Writer at all. Thus the dependencies have been inverted; the Copy class Writer/Reader depends upon abstractions, and the detailed readers and writers depend upon the same abstractions. Hence, "dependency inversion".
This all came about because I'm on a new job, where there happen to be people who are at least as smart, and probably smarter, than I. How refreshing!
They say that in general, classes are the preferred construct for exposing abstractions, the logical basis of this being that once you ship an interface, the set of members is baked forever - any additions would break existing types that implement the interface.
Classes are much more flexible - you can add members to classes that have already shipped, and as long as the method has a default implementation, existing derived classes continue to function undisturbed.
They provide an example (although not a very good one) of how difficult it would be to add timeout support for streams. All of the options have substantial development cost and usability issues.
It seems that one of the primary arguments for interfaces is that they allow separating contract from implementation. However, this incorrectly assumes that you can't separate contracts from implementation using classes. Abstract classes residing in a separate assembly from their concrete implementations are an excellent way to achieve this kind of separation.
Bottom line, "DO" favor defining classes over interfaces. Of course this does not address the concept of being able to dynamically load an assembly with a class that implements a specified interface and cast the object to an instance of the I<Whatever> interface it is, and make method calls on it.
Inversion of Dependency
The Inversion of Dependency concept comes from work by Robert Martin.
First he defines "Bad Design", before getting on to his concept. To Martin, "Bad Design" equals the following three concepts:
1.It is hard to change because every change affects too many other parts of the sys-
tem. (Rigidity)
2.When you make a change, unexpected parts of the system break. (Fragility)
3.It is hard to reuse in another application because it cannot be disentangled from
the current application. (Immobility)
Martin states that it would be difficult to demonstrate that a piece of software that exhibits none of those traits, i.e. it is flexible, robust, and reusable, and that it also fulflls all its requirements, has a bad design. Thus, we can use these three traits as a way to unambiguously decide if a design is "good" or "bad".
Following this, Martin explains that it is the interdependence of the modules within that design that is the root cause of "bad design".
He illustrates with an example of a "Copy" Program that has two subprograms, "Read Keyboard" and "Write Printer" and shows how the Copy module is not reusable in any situation that does not involve a keyboard and a printer.
This leads to Martin's "Dependency Inversion Principle":
A) HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES -- BOTH SHOULD DEPEND UPON ABSTRACTIONS
B) ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS -- DETAILS SHOULD DEPEND UPON ABSTRACTIONS
This leads us to the abstraction interfaces:
COPY
READER (abstract) WRITER (abstract)
Keyboard Reader Printer Writer
This arrangement allows us to plug ANY kind of "Reader" into the abstract READER interface, along with ANY kind of Writer into the WRITER interface, and achieve the kind of usability we want from software. This Copy class does not depend upon the Keyboard Reader nor the Printer Writer at all. Thus the dependencies have been inverted; the Copy class Writer/Reader depends upon abstractions, and the detailed readers and writers depend upon the same abstractions. Hence, "dependency inversion".
This all came about because I'm on a new job, where there happen to be people who are at least as smart, and probably smarter, than I. How refreshing!
The dynamic assembly loading into a variable declared as type I(whatever) works just fine with interfaces. Declare the variable to be of type AbstractClass, and assign the subclass to it. Works fine. You just dont have access to any new members from the subclass (you dont in the interface model either)
ReplyDeleteThe real problem with the abstract model that leads you to the interface route is multiple inheritance.