CSC/ECE 517 Fall 2012/ch2b 2w69 as: Difference between revisions
m (→Motivation) |
|||
(37 intermediate revisions by 2 users not shown) | |||
Line 7: | Line 7: | ||
''Software entities (Classes, Modules, Functions, etc.) should be open for extension, but closed for modification.'' | ''Software entities (Classes, Modules, Functions, etc.) should be open for extension, but closed for modification.'' | ||
At first it might sound like a contradiction in terms, but it's not. All it means is that you should structure an application so that you can add new functionality with minimal modification to existing code. All systems change during their life cycles. One should keep this in mind when designing systems that are expected to last longer than the initial version. What you want to avoid is to have one simple change ripple through the various classes of your application. That makes the system fragile, prone to regression problems, and expensive to extend. To isolate the changes, you want to write classes and methods in such a way that they never need to change once they are written<ref name = microsoft />. | |||
Open/Closed principle is one of the five principles basic of [http://en.wikipedia.org/wiki/OOD object-oriented design(OOD)] which are defined as the [http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29 S.O.L.I.D.]. The principles of SOLID are guidelines that can be applied while working on software to remove [http://en.wikipedia.org/wiki/Code_smell code smells] by causing the programmer to refactor the software's source code until it is both legible and extensible. It is typically used with [http://en.wikipedia.org/wiki/Test-driven_development test-driven development], and is part of an overall strategy of [http://en.wikipedia.org/wiki/Agile_software_development agile] and [http://en.wikipedia.org/wiki/Adaptive_Software_Development adaptive programming]<ref name = openclosed_video />. | Open/Closed principle is one of the five principles basic of [http://en.wikipedia.org/wiki/OOD object-oriented design(OOD)] which are defined as the [http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29 S.O.L.I.D.]. The principles of SOLID are guidelines that can be applied while working on software to remove [http://en.wikipedia.org/wiki/Code_smell code smells] by causing the programmer to refactor the software's source code until it is both legible and extensible. It is typically used with [http://en.wikipedia.org/wiki/Test-driven_development test-driven development], and is part of an overall strategy of [http://en.wikipedia.org/wiki/Agile_software_development agile] and [http://en.wikipedia.org/wiki/Adaptive_Software_Development adaptive programming]<ref name = openclosed_video />. | ||
Line 15: | Line 15: | ||
design. The program becomes fragile, rigid, unpredictable and unreusable. The open-closed principle attacks this in a very straightforward way. Open-closed principle states that the code should be designed in such a way that it should not change. Whenever requirements change, the existing code should be extended by adding new code and leave the old working code intact. | design. The program becomes fragile, rigid, unpredictable and unreusable. The open-closed principle attacks this in a very straightforward way. Open-closed principle states that the code should be designed in such a way that it should not change. Whenever requirements change, the existing code should be extended by adding new code and leave the old working code intact. | ||
Most of the times it easier to write all new code rather | Most of the times it easier to write all new code rather than making changes to existing code. Modifying old code adds the risk of breaking existing functionality. With new code you generally only have to test the new functionality. When you modify old code you have to both test your changes and then perform a set of regression tests to make sure you did not break any of the existing code. | ||
The Open Close Principle states that the design and writing of the code should be done in a way that new functionality should be added with minimum changes in the existing code. The design should be done in a way to allow the adding of new functionality as new classes, keeping as much as possible existing code unchanged. Thus following open-closed principle leads to good software design. | The Open Close Principle states that the design and writing of the code should be done in a way that new functionality should be added with minimum changes in the existing code. The design should be done in a way to allow the adding of new functionality as new classes, keeping as much as possible existing code unchanged. Thus following open-closed principle leads to good software design. | ||
== Description == | == Description == | ||
Every module obeying open-closed principle have the following key attributes: | |||
* <b>They are “Open For Extension”.</b> This means that the behavior of the module can be extended. That we can make the module behave in new and different ways as the requirements of the application change, or to meet the needs of new applications. | |||
* <b>They are “Closed for Modification”.</b> The source code of such a module is inviolate. No one is allowed to make source code changes to it. | |||
== | At starting, these two points seem to be very contradictory to each other. How can the module be extensible when it is not supposed to me modified? The key idea to go about this is [http://en.wikipedia.org/wiki/Abstraction_(computer_science) abstraction]<ref name = abstraction_wiki> </ref>. | ||
==== Abstraction ==== | |||
In object oriented design, it is possible to create abstractions that are fixed and yet represent an unbounded group of possible behaviors. The abstractions are abstract base classes, and the unbounded group of possible behaviors is represented by all the possible derivative classes. It is possible for a module to manipulate an abstraction. Such a module can be closed for modification since it depends upon an abstraction that is fixed. Yet the behavior of that module can be extended by creating new derivatives of the abstraction. | |||
===== Abstraction Example ===== | |||
struct Square | |||
{ | |||
ShapeType itsType; | |||
double itsSide; | |||
Point itsTopLeft; | |||
};<br> | |||
// | |||
// These functions are implemented elsewhere | |||
//<br> | |||
void DrawSquare(struct Square*) | |||
void DrawCircle(struct Circle*); | |||
typedef struct Shape *ShapePointer; | |||
void DrawAllShapes(ShapePointer list[], int n) | |||
{ | |||
int i; | |||
for (i=0; i<n; i++) | |||
{ | |||
struct Shape* s = list[i]; | |||
switch (s->itsType) | |||
{ | |||
case square: | |||
DrawSquare((struct Square*)s); | |||
break; <br> | |||
case circle: | |||
DrawCircle((struct Circle*)s); | |||
break; | |||
} | |||
} | |||
} | |||
In the above example, the function DrawAllShapes does not conform to the open-closed principle because it cannot be closed against new kinds of shapes. If we wanted to extend this function to be able to draw a list of shapes that included triangles, we would have to modify the function. In fact, we would have to modify the function for any new type of shape that we needed to draw. Now let us look at a more better solution for this problem. | |||
class Shape | |||
{ | |||
public: | |||
virtual void Draw() const = 0; | |||
}; <br> | |||
class Square : public Shape | |||
{ | |||
public: | |||
virtual void Draw() const; | |||
};<br> | |||
class Circle : public Shape | |||
{ | |||
public: | |||
virtual void Draw() const; | |||
}; <br> | |||
void DrawAllShapes(Set<Shape*>& list) | |||
{ | |||
for (Iterator<Shape*>i(list); i; i++) | |||
(*i)->Draw(); | |||
} | |||
Note that in the above solution, if we want to extend the behavior of the DrawAllShapes function in to draw a new kind of shape, all we need do is add a new derivative of the Shape class. The DrawAllShapes function does not need to change. Thus DrawAllShapes conforms to the open-closed principle. Its behavior can be extended without modifying it. | |||
==== Strategic Closure ==== | |||
It should be clear that no significant program can be 100% closed. For example, consider the above DrawAllShapes example. What would happen to the DrawAllShapes function if we decided that all Circles should be drawn before any Squares. The DrawAllShapes function is not closed against a change like this. In general, no matter how “closed” a module is, there will always be some kind of change against which it is not closed. Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close his design. This takes a certain amount of prescience derived from experience. The experienced designer knows the users and the industry well enough to judge the probability of different kinds of changes. He then makes sure that the open-closed principle is invoked for the most probable changes. | |||
==== Conventions to follow ==== | |||
In order to obey open-closed principle it is good to follow some conventions<ref name=pdf></ref>. | |||
* <b>Make all Member Variables Private:</b> Member variables of classes should be known only to the methods of the class that defines them. Member variables should never be known to any other class, including derived classes. Thus they should be declared private, rather than public or protected. In light of the open-closed principle, the reason for this convention ought to be clear. When the member variables of a class change, every function that depends upon those variables must be changed. Thus, no function that depends upon a variable can be closed with respect to that variable. <br> | |||
* <b>No global variables:</b> The argument against global variables is similar to the argument against pubic member variables. No module that depends upon a global variable can be closed against any other module that might write to that variable. Any module that uses the variable in a way that the other modules don’t expect, will break those other modules. It is too risky to have many modules be subject to the whim of one badly behaved one. <br> | |||
* <b>Avoid run time type identification:</b> Another very common proscription is the one against dynamic_cast. It is often claimed that dynamic_cast, or any form of run time type identification is intrinsically dangerous and should be avoided to comply with the open-closed principle. | |||
== Example == | |||
Suppose you are writing a module to approve personal loans and before doing that you want to validate the personal information, code wise we can depict the situation as: | Suppose you are writing a module to approve personal loans and before doing that you want to validate the personal information, code wise we can depict the situation as: | ||
Line 44: | Line 118: | ||
So far so good. As we have already discussed, the requirements always change and now we are required to approve vehicle loans as well. So one approach to solve this requirement is to: | So far so good. As we have already discussed, the requirements always change and now we are required to approve vehicle loans as well. So one approach to solve this requirement is to: | ||
public class LoanApprovalHandler | public class LoanApprovalHandler | ||
{ | { | ||
public void approvePersonalLoan (PersonalLoanValidator validator) | public void approvePersonalLoan (PersonalLoanValidator validator) | ||
{ | { | ||
Line 61: | Line 135: | ||
} | } | ||
// Method for approving other loans. | // Method for approving other loans. | ||
} | } | ||
public class PersonalLoanValidator | public class PersonalLoanValidator | ||
{ | { | ||
public boolean isValid() | public boolean isValid() | ||
{ | { | ||
//Validation logic | //Validation logic | ||
} | } | ||
} | } | ||
public class VehicleLoanValidator | public class VehicleLoanValidator | ||
{ | { | ||
public boolean isValid() | public boolean isValid() | ||
{ | { | ||
//Validation logic | //Validation logic | ||
} | } | ||
} | } | ||
We have edited the existing class to accommodate the new requirement and in the process we ended up changing the name of the existing method and also adding new methods for different types of loan approval. This clearly violates the Open/Closed principle. Lets try to implement the requirement in a different way | |||
/** | |||
* Interface Validator class Extended to add different validators for different loan type | |||
*/ | |||
public interface Validator | |||
{ | |||
public boolean isValid(); | |||
} | |||
/** | |||
* Personal loan validator | |||
*/ | |||
public class PersonalLoanValidator | |||
implements Validator | |||
{ | |||
public boolean isValid() | |||
{ | |||
//Validation logic. | |||
} | |||
} | |||
/** | |||
* Vehicle loan validator | |||
*/ | |||
public class VehicleLoanValidator | |||
implements Validator | |||
{ | |||
public boolean isValid() | |||
{ | |||
//Validation logic. | |||
} | |||
} | |||
/* | |||
* Similarly any new type of validation can | |||
* be accommodated by creating a new subclass | |||
* of Validator | |||
*/ | |||
Now using the above validators we can write a LoanApprovalHandler to use the Validator abstraction. | |||
public class LoanApprovalHandler | |||
{ | |||
public void approveLoan(Validator validator) | |||
{ | |||
if ( validator.isValid()) | |||
{ | |||
//Process the loan. | |||
} | |||
} | |||
} | |||
So to accommodate any type of loan validators we would just have create a subclass of Validator and then pass it to the approveLoan method. That way the class is CLOSED for modification but OPEN for extension. | |||
== Conclusion == | == Conclusion == | ||
Line 89: | Line 215: | ||
*[http://en.wikipedia.org/wiki/Liskov_substitution_principle Liskov Substitution Principle ] | *[http://en.wikipedia.org/wiki/Liskov_substitution_principle Liskov Substitution Principle ] | ||
*[http://en.wikipedia.org/wiki/SOLID_(object-oriented_design) SOLID - Object Oriented Design] | *[http://en.wikipedia.org/wiki/SOLID_(object-oriented_design) SOLID - Object Oriented Design] | ||
*[http://css.dzone.com/articles/openclosed-principle-real A real world example of Open/Closed Principle] | |||
== References == | == References == | ||
Line 97: | Line 224: | ||
<ref name = openclosed_video> http://ruby-toolbox.com/categories/testing_frameworks.html </ref> | <ref name = openclosed_video> http://ruby-toolbox.com/categories/testing_frameworks.html </ref> | ||
<ref name = journal> http://www2.sys-con.com/ITSG/VirtualCD_Spring05/Java/archives/0702/knoernschild/index.html </ref> | <ref name = journal> http://www2.sys-con.com/ITSG/VirtualCD_Spring05/Java/archives/0702/knoernschild/index.html </ref> | ||
<ref name = abstraction_wiki> [http://en.wikipedia.org/wiki/Abstraction_(computer_science) Abstraction wiki] </ref> | |||
</references> | </references> |
Latest revision as of 20:20, 19 November 2012
The Open/Closed principle
Introduction
In object-oriented programming the Open/Closed principle states<ref name = meyer />,
Software entities (Classes, Modules, Functions, etc.) should be open for extension, but closed for modification.
At first it might sound like a contradiction in terms, but it's not. All it means is that you should structure an application so that you can add new functionality with minimal modification to existing code. All systems change during their life cycles. One should keep this in mind when designing systems that are expected to last longer than the initial version. What you want to avoid is to have one simple change ripple through the various classes of your application. That makes the system fragile, prone to regression problems, and expensive to extend. To isolate the changes, you want to write classes and methods in such a way that they never need to change once they are written<ref name = microsoft />.
Open/Closed principle is one of the five principles basic of object-oriented design(OOD) which are defined as the S.O.L.I.D.. The principles of SOLID are guidelines that can be applied while working on software to remove code smells by causing the programmer to refactor the software's source code until it is both legible and extensible. It is typically used with test-driven development, and is part of an overall strategy of agile and adaptive programming<ref name = openclosed_video />.
Motivation
All software systems are subject to change. Thus designing a system which is stable is a very crucial task. When a single change to a program results in a cascade of changes to dependent modules, that program exhibits the undesirable attributes that we have come to associate with “bad” design. The program becomes fragile, rigid, unpredictable and unreusable. The open-closed principle attacks this in a very straightforward way. Open-closed principle states that the code should be designed in such a way that it should not change. Whenever requirements change, the existing code should be extended by adding new code and leave the old working code intact.
Most of the times it easier to write all new code rather than making changes to existing code. Modifying old code adds the risk of breaking existing functionality. With new code you generally only have to test the new functionality. When you modify old code you have to both test your changes and then perform a set of regression tests to make sure you did not break any of the existing code.
The Open Close Principle states that the design and writing of the code should be done in a way that new functionality should be added with minimum changes in the existing code. The design should be done in a way to allow the adding of new functionality as new classes, keeping as much as possible existing code unchanged. Thus following open-closed principle leads to good software design.
Description
Every module obeying open-closed principle have the following key attributes:
- They are “Open For Extension”. This means that the behavior of the module can be extended. That we can make the module behave in new and different ways as the requirements of the application change, or to meet the needs of new applications.
- They are “Closed for Modification”. The source code of such a module is inviolate. No one is allowed to make source code changes to it.
At starting, these two points seem to be very contradictory to each other. How can the module be extensible when it is not supposed to me modified? The key idea to go about this is abstraction<ref name = abstraction_wiki> </ref>.
Abstraction
In object oriented design, it is possible to create abstractions that are fixed and yet represent an unbounded group of possible behaviors. The abstractions are abstract base classes, and the unbounded group of possible behaviors is represented by all the possible derivative classes. It is possible for a module to manipulate an abstraction. Such a module can be closed for modification since it depends upon an abstraction that is fixed. Yet the behavior of that module can be extended by creating new derivatives of the abstraction.
Abstraction Example
struct Square { ShapeType itsType; double itsSide; Point itsTopLeft; };
// // These functions are implemented elsewhere //
void DrawSquare(struct Square*) void DrawCircle(struct Circle*); typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; i<n; i++) { struct Shape* s = list[i]; switch (s->itsType) { case square: DrawSquare((struct Square*)s); break;
case circle: DrawCircle((struct Circle*)s); break; } } }
In the above example, the function DrawAllShapes does not conform to the open-closed principle because it cannot be closed against new kinds of shapes. If we wanted to extend this function to be able to draw a list of shapes that included triangles, we would have to modify the function. In fact, we would have to modify the function for any new type of shape that we needed to draw. Now let us look at a more better solution for this problem.
class Shape { public: virtual void Draw() const = 0; };
class Square : public Shape { public: virtual void Draw() const; };
class Circle : public Shape { public: virtual void Draw() const; };
void DrawAllShapes(Set<Shape*>& list) { for (Iterator<Shape*>i(list); i; i++) (*i)->Draw(); }
Note that in the above solution, if we want to extend the behavior of the DrawAllShapes function in to draw a new kind of shape, all we need do is add a new derivative of the Shape class. The DrawAllShapes function does not need to change. Thus DrawAllShapes conforms to the open-closed principle. Its behavior can be extended without modifying it.
Strategic Closure
It should be clear that no significant program can be 100% closed. For example, consider the above DrawAllShapes example. What would happen to the DrawAllShapes function if we decided that all Circles should be drawn before any Squares. The DrawAllShapes function is not closed against a change like this. In general, no matter how “closed” a module is, there will always be some kind of change against which it is not closed. Since closure cannot be complete, it must be strategic. That is, the designer must choose the kinds of changes against which to close his design. This takes a certain amount of prescience derived from experience. The experienced designer knows the users and the industry well enough to judge the probability of different kinds of changes. He then makes sure that the open-closed principle is invoked for the most probable changes.
Conventions to follow
In order to obey open-closed principle it is good to follow some conventions<ref name=pdf></ref>.
- Make all Member Variables Private: Member variables of classes should be known only to the methods of the class that defines them. Member variables should never be known to any other class, including derived classes. Thus they should be declared private, rather than public or protected. In light of the open-closed principle, the reason for this convention ought to be clear. When the member variables of a class change, every function that depends upon those variables must be changed. Thus, no function that depends upon a variable can be closed with respect to that variable.
- No global variables: The argument against global variables is similar to the argument against pubic member variables. No module that depends upon a global variable can be closed against any other module that might write to that variable. Any module that uses the variable in a way that the other modules don’t expect, will break those other modules. It is too risky to have many modules be subject to the whim of one badly behaved one.
- Avoid run time type identification: Another very common proscription is the one against dynamic_cast. It is often claimed that dynamic_cast, or any form of run time type identification is intrinsically dangerous and should be avoided to comply with the open-closed principle.
Example
Suppose you are writing a module to approve personal loans and before doing that you want to validate the personal information, code wise we can depict the situation as:
public class LoanApprovalHandler { public void approveLoan(PersonalValidator validator)
{ if ( validator.isValid()) { //Process the loan. } } } public class PersonalLoanValidator { public boolean isValid() { //Validation logic } }
So far so good. As we have already discussed, the requirements always change and now we are required to approve vehicle loans as well. So one approach to solve this requirement is to:
public class LoanApprovalHandler { public void approvePersonalLoan (PersonalLoanValidator validator) { if ( validator.isValid()) { //Process the loan. } } public void approveVehicleLoan (VehicleLoanValidator validator ) { if ( validator.isValid()) { //Process the loan. } } // Method for approving other loans. } public class PersonalLoanValidator { public boolean isValid() { //Validation logic } } public class VehicleLoanValidator { public boolean isValid() { //Validation logic } }
We have edited the existing class to accommodate the new requirement and in the process we ended up changing the name of the existing method and also adding new methods for different types of loan approval. This clearly violates the Open/Closed principle. Lets try to implement the requirement in a different way
/** * Interface Validator class Extended to add different validators for different loan type */ public interface Validator { public boolean isValid(); } /** * Personal loan validator */ public class PersonalLoanValidator implements Validator { public boolean isValid() { //Validation logic. } } /** * Vehicle loan validator */ public class VehicleLoanValidator implements Validator { public boolean isValid() { //Validation logic. } } /* * Similarly any new type of validation can * be accommodated by creating a new subclass * of Validator */
Now using the above validators we can write a LoanApprovalHandler to use the Validator abstraction.
public class LoanApprovalHandler { public void approveLoan(Validator validator) { if ( validator.isValid()) { //Process the loan. } } }
So to accommodate any type of loan validators we would just have create a subclass of Validator and then pass it to the approveLoan method. That way the class is CLOSED for modification but OPEN for extension.
Conclusion
In reality, achieving Open/Closed principle-compliance system-wide is not possible. There will always exist some change that the system is not closed against. Therefore, this principle must be applied judiciously to the areas of the system that are most complex and dynamic. Even partial Open Closed principle compliance results in more resilient systems. Isolating violations of Open Closed principles to specific classes, such as object factories, certainly serves to reduce the overall maintenance efforts.<ref name = journal/>
This principle is at the heart of object oriented design in many ways, it motivated many heuristics associated with object oriented design<ref name = pdf/>. For example, “all member variables should be private”, or “global variables should be avoided”. Conformance to this principle yields some of the great benefits such as the application will be more robust because we are not changing already tested code, flexible because we can easily accommodate new requirements. Yet conformance to this principle is not achieved simply by using an object oriented programming language. Rather, it requires a dedication on the part of the designer to apply abstraction to those parts of the program that the designer feels are going to be subject to change.
See Also
Below are some of the patterns and principles that are used to structure code to isolate changes.
- Single-Responsibility Principle
- Chain of Responsibility Pattern
- Liskov Substitution Principle
- SOLID - Object Oriented Design
- A real world example of Open/Closed Principle
References
<references> <ref name = meyer> Meyer, Bertrand (1988). Object-Oriented Software Construction. Prentice Hall. ISBN 0-13-629049-3. </ref> <ref name = pdf> Martin, R.C. (2000). "Design Principles and Design Patterns." http://www.objectmentor.com </ref> <ref name = microsoft> http://msdn.microsoft.com/en-us/magazine/cc546578.aspx Patterns in Practice: The Open Closed Principle </ref> <ref name = openclosed_video> http://ruby-toolbox.com/categories/testing_frameworks.html </ref> <ref name = journal> http://www2.sys-con.com/ITSG/VirtualCD_Spring05/Java/archives/0702/knoernschild/index.html </ref> <ref name = abstraction_wiki> Abstraction wiki </ref> </references>