At other times, a white-box framework will be a finished product. A useful design strategy is to begin with a white-box approach. White-box frameworks, as a result of their internal informality, are usually relatively easy to design. As the system evolves, the designer can then see if additional internal structure emerges. Barbara Liskov, in a keynote address given at the OOPSLA '87 conference in Orlando, distinguished between inheritance as an implementation aid which she dismissed as unimportant and inheritance for extending the abstract functionality of an object [Liskov ].
Liskov claims that in the later case, only the abstract specification, and not the internal representation of the parent object, should be inherited. She was in effect advocating that only the black-box framework style should be employed. Such a perspective ignores the value of white-box frameworks. Prohibiting white-box frameworks ignores both their value in their own rights, and their value as the progenitors of mature components.
Finding new abstractions is difficult. In general, it seems that an abstraction is usually discovered by generalizing from a number of concrete examples. An experienced designer can sometimes invent an abstract class from scratch, but only after having implemented concrete versions for several other projects. This is probably unavoidable. Humans think better about concrete examples then about abstractions.
We can think well about abstractions such as integers or parsers only because we have a lot of experience with them. However, new abstractions are very important. A designer should be very happy whenever a good abstraction is found, no matter how it was found.
Design methodology. The product of an object-oriented design is a list of class definitions. Each class has a list of operations that it defines and a list of objects with which its instances communicate. In addition, each operation has a list of other operations that it will invoke. A design is complete when every object that is referenced has been defined and every operation is defined. The design process incrementally extends an incomplete design until it is complete.
Object-oriented design starts with objects. Booch suggests that the designer start with a natural language description of the desired system and use the nouns as a starting point for the classes of objects to be designed [Booch ]. Each verb is an operation, either one implemented by a class when the class is the direct object or one used by the class when the class is the subject.
The resulting list of classes and operations can be used as the start of the design process. Operations on an object are always thought about from the object's point of view. Thus, instead of displaying an object, an object is asked to display itself. Booch's design methodology defines classes for objects in the problem domain. However, classes are often needed for operations in the problem domain.
For example, compiling a program can be thought of as an operation on programs. However, because compilation is so complex, it is best to have separate compiler objects to represent compilation. The compile operation on programs would make a new compiler object and use it to make a compiled version of the program.
It can be difficult to decide whether an operation should be implemented as a method in a class or as a separate class. In general, there is no absolute way to decide, but positive answers to the following questions all indicate that a new class should be created. The compiler example shows that it is possible to make an operation both a class and a method by having the method make an object of the class.
This separates the implementation of the operation from that of the class and makes it more reusable, but permits the user to continue to think of the operation as a method. It can also be difficult to decide which class should implement an operation. Operations with several arguments can frequently be implemented as methods in the classes of any of its arguments. The rules listed above can also be used to make this decision. For example, if an operation does not send messages to an object or access its instance variables then it should not be in the object's class.
We are not implying that classes can be reorganized mechanically. A class should represent a well-defined abstraction, not just a bundle of methods and variable definitions. Human judgement is needed to decide when and how a class hierarchy is to be reorganized. Nevertheless, the following rules will frequently point out the need for a reorganization and suggest how it is to be accomplished. It is very important that the design process result in standard protocols.
In other words, many of the classes should have nearly identical external interfaces and there should be sets of operations that many classes implement. Standard protocols are developed by choosing names carefully.
The need for standard protocols is one reason why it takes a long time to become an expert Smalltalk programmer. Thus, the only way to learn these protocols is by experience. There are a number of rules of thumb that will help develop standard protocols. A programmer practicing these rules is more likely to keep from giving different names to the same operation in different classes. These rules help minimize the number of different names and maximize the number of names shared by a set of classes.
If one class communicates with a number of other classes, its interface to each of them should be the same. If an operation X is implemented by performing a similar operation on the components of the receiver, then that operation should also be named X. Even if the name of the operation has to be changed to add more arguments, Smalltalk message names indicate the number of arguments to the message.
The result is that a method for a message sends that same message to other objects. If the other objects are in the same class as the sender then the method is recursive.
Even if no real recursion exists, the method appears recursive, so we call this rule recursion introduction. Recursion introduction can help decide the class in which an operation should be a method. Consider the problem of converting a parse tree into machine language.
In addition to an object representing the parse tree, there will be an object representing the final machine language procedure. However, the best design is to implement the generate code message in the parse tree class, since a parse tree will consist of many parse nodes, and a parse node will generate machine code for itself by recursively asking its subtrees to generate code for themselves. It is almost always a mistake to explicitly check the class of an object. Code of the form.
Methods will have to be created in the various possible classes of the object to respond to the message, and each method will contain one of the cases that is being replaced. Case analysis of the values of variables is usually a bad idea, too. For example, a parse tree might contain nodes that represent instance variables, global variables, method arguments, and temporary variables.
The Smalltalk compiler uses one class to represent all these kinds of variables and differentiates between them on the value of an instance variable. It would be better to have a separate class for each kind of variable.
Eliminating case analysis is more difficult when the cases are accessing instance variables, but it is no less important. If instance variables are being accessed then self will need to be an argument to the message and more messages may need to be defined to access the instance variables.
Messages with half a dozen or more arguments are hard to read. Except for instance creation messages, a message with this many arguments should be redefined. When a message has a smaller number of arguments it is more likely to be similar to some other message, thus increasing the possibility of giving them the same name.
The number of arguments can be reduced by breaking a message into several smaller messages or by creating a new class that represents a group of arguments. Frequently there will be several kinds of messages that pass the same set of objects around. This set of objects is essentially a new object, and the design can be changed to reflect that fact by replacing the set of objects with an object that contains them.
Well-designed Smalltalk methods are almost always small. It is easier to subclass a class with small methods, since its behavior can be changed by redefining a few small methods instead of modifying a few large methods. A thirty line method is large and probably needs to be broken into pieces.
Often a method in a superclass is split when a subclass is made. Most of the inherited method is correct, but one part needs to be changed. Instead of rewriting the entire method, it is split into pieces and the one piece that has changed is redefined. This change leaves the superclass even easier to subclass. These design rules are all related, since eliminating cases reduces the size of methods, breaking a method into pieces is likely to reduce the number of arguments that any one method needs, and reducing the number of arguments is likely to create more methods with the same name.
A well developed class hierarchy should be several layers deep. A class hierarchy consisting of one superclass and 27 subclasses is much too shallow.
A shallow class hierarchy is evidence that change is needed, but does not give any idea how to make that change. An obvious way to make a new superclass is to find some sibling classes that implement the same message and try to migrate the method to a common superclass. Of course, the classes are likely to provide different methods for the message, but it is often possible to break a method into pieces and place some of the pieces in the superclass and some in the subclasses.
For example, displaying a view consists of displaying its border, displaying its subviews, and displaying its contents. The last part must be implemented by each subclass, but the others are inherited from View. Inheritance for generalization or code sharing usually indicates the need for a new subclass. If class B overrides a method x that it inherits from class A then it might be better to move the methods in A that B does inherit to C, a new superclass of A, as shown in Figure 1.
C will probably be abstract. B can then become a subclass of C, and will not have to redefine any methods. Instance variables or methods defined in A that are used by B should be moved to C. Figure 1 Rule 7: Minimize accesses to variables.
Since one of the main differences between abstract and concrete classes is the presence of data representation, classes can be made more abstract by eliminating their dependence on their data representation.
One way this can be done is to access all variables by sending messages. The data representation can be changed by redefining the accessing messages. Specialization is the ideal that is usually described, where the elements of the subclass can all be thought of as elements of the superclass. Usually the subclass will not redefine any of the inherited methods, but will add new methods.
For example, a two dimensional array is a subclass of Array in which all the elements are arrays. It might have new messages that use two indexes, instead of just one. An important special case of specialization is making concrete classes. Since an abstract class is not executable, making a subclass of an abstract class is different from making a subclass of a concrete class.
The abstract class requires its subclasses to define certain operations, so making a concrete class is similar to filling in the blanks in a program template. An abstract class may define some operations in an overly general fashion, and the subclass may have to redefine them. For example, the size operation in class Collection is implemented by iterating over the collection and counting its elements. Most subclasses of Collection have an instance variable that contains the size, so size is redefined in those subclasses to return that instance variable.
There are a couple of ways that a designer can tell whether a subclass is a specialization of a superclass. An abstract definition is that anywhere the superclass is used, the subclass can be used. Thus, a subclass has a superset of the behavior of its superclass. Of course, it is difficult to check type compatibility rules in an untyped language like Smalltalk, but they can be checked by hand.
In the middle of a project, it may be useful for a designer to make subclasses that are not specializations of their superclasses. If the superclasses are poorly designed, it might take a great deal of work to determine the proper design of the class hierarchy.
It is better to forge ahead and then to later reorganize the classes. Thus, a subclass might be more general than its superclass or might have little relationship to its superclass other than borrowing code from it.
Large classes are frequently broken into several small classes as they grow, leading to a new framework. A collection of small classes can be easier to learn and will almost always be easier to reuse than a single large class.
A collection of class hierarchies provides the ability to mix and match components while a single class hierarchy does not. Thus, breaking a compiler into a parsing phase and a code generation phase permits a new language to be implemented by building only a new parser, and a new machine to be supported by building only a new code generator.
A class is supposed to represent an abstraction. If a class has 50 to methods then it must represent a complicated abstraction. It is likely that such a class is not well defined and probably consists of several different abstractions. Large classes should be viewed with suspicion and held to be guilty of poor design until proven innocent. If some subclasses implement a method one way and others implement it another way then the implementation of that method is independent of the superclass.
It is likely that it is not an integral part of the subclasses and should be split off into the class of a component. Multiple inheritance can also be used to solve this problem. However, if an algorithm or set of methods is independent of the rest of the class then it is cleaner to encapsulate it in a separate component. A class should almost always be split when half of its methods access half of its instance variables and the other half of its methods access the other half of its variables.
This sometimes occurs when there are several different ways to view objects in the class. For example, a complex graphical object may cache its image as a bitmap, but the image is derived from the complex structure of the object, which consists of a number of simple graphical objects.
When the object is asked to display itself, it displays its cached image if it is valid. If the image is not valid, the object recalculates the image and displays it. However, the graphical object can also be considered a collection of graphical objects that can be added or removed.
Changing the collection invalidates the image. This graphical object could be implemented as a subclass of bitmapped images, or it could be a subclass of Collection. A system with multiple inheritance might make both be superclasses. However, it is best to make both the bitmap and the collection of graphical objects be components, since each of them could be implemented in a number of different ways, and none of those ways are critical to the implementation of the graphical object.
Separating the bitmap class will make it easier to port the graphical object to a system with different graphics primitives, and separating the collection class will make it easier to make the graphical object be efficient even when very large. An inheritance-based framework can be converted into a component-based framework black box structure by replacing overridden methods by message sends to components.
Examples of such frameworks in conventional systems are sorting routines that take procedural parameters. Programs should be factored in this fashion whenever possible. Reducing the coupling between framework components so that the framework works with any plug-compatible object increases its cohesion and generality. Sometimes it is hard to split a class into two parts because methods that should go in different classes access the same instance variable.
This can happen because the instance variable is being treated as a global variable when it should be passed as a parameter between methods. Changing the methods to explicitly pass the parameter will make it easier to split the class later.
There are at least two new kinds of tools needed for the style of programming we have just described. One kind helps the programmer reorganize class hierarchies and the other helps the programmer build applications from frameworks. Other tools would also be helpful, such as tools to help a programmer find components in libraries, but these will not be discussed.
It takes a great deal of inspiration to construct a good class hierarchy. However, it is possible to build tools that would let a programmer know that a class hierarchy had problems. These tools would be like the English style tools in the Unix writer's workbench. They would complain about perceived problems but would let the programmer decide whether the complaints were valid and how to fix them. Other tools could help reorganize the class hierarchy once a problem was diagnosed. For example, if a method of a superclass is ignored by its subclass then some abstractions in the superclass are not being inherited by the subclass.
This is probably a case of subclassing for generalization or code sharing. It might be best to break the superclass into two classes, one a subclass of the other. The new subclass would have all the methods that are unused by the old subclasses. Similarly, a sign of inheritance for code sharing is that many of a superclass's methods are redefined. Perhaps some of these redefined methods should be in a concrete subclass, making the superclass abstract.
You would typically want to use virtual methods to implement run-time polymorphism or late binding. Note that a virtual method is one that is declared as virtual in the base class and you can allow the subclasses of the type to override the virtual method s. The following code snippet shows two classes -- the base class named Logger that contains a virtual method called Log and a derived class named FileLogger that extends the Logger class and overrides the Log method of the base class.
This is an example of method overriding. Both the base and the derived classes have the same method with identical signatures. We use method overriding to implement run time polymorphism or late binding. The following code snippet shows how the Log method can be called using a reference of the base class. When you execute the above code snippet, the Log method of the derived class, i.
Since this binding occurs late at run-time, this type of polymorphism is known as run-time polymorphism or late binding. Net, as well as a speaker and author of several books and articles. He has more than 20 years of experience in IT including more than 16 years in Microsoft. Net and related technologies. Here are the latest Insider stories. More Insider Sign Out.
Sign In Register. Sign Out Sign In Register. Latest Insider. Check out the latest Insider stories here. More from the IDG Network. Exploring virtual and abstract methods in C.
With this approach, a class can be updated or maintained without worrying about the methods using them. Once the class is changed, it automatically updates the methods accordingly. Encapsulation also ensures that your data is hidden from external modification.
Encapsulation is also known as Data-Hidden. Encapsulation can be viewed as a shield that protects data from getting accessed by outside code.
Polymorphism means existing in many forms. Variables, functions, and objects can exist in multiple forms in Java and Python. There are two types of polymorphism which are run time polymorphism and compile-time polymorphism. Run time can take a different form while the application is running and compile-time can take a different form during compilation.
An excellent example of Polymorphism in Object-oriented programing is a cursor behavior. A cursor may take different forms like an arrow, a line, cross, or other shapes depending on the behavior of the user or the program mode.
With polymorphism, a method or subclass can define its behaviors and attributes while retaining some of the functionality of its parent class. This means you can have a class that displays date and time, and then you can create a method to inherit the class but should display a welcome message alongside the date and time.
The goals of Polymorphism in Object-oriented programming is to enforce simplicity, making codes more extendable and easily maintaining applications. Inheritance allows you to create class hierarchies, where a base class gives its behavior and attributes to a derived class. You are then free to modify or extend its functionality. Program codes would run differently in a different operating system. The ability of program code exhibiting different behaviors across the operating system is known as polymorphism in OOP.
So polymorphism would allow the animals to move but in different forms based on the physical features. Polymorphism has ensured that these animals are all moving but in different forms. How the programs would behave would not be known until run time.
Abstraction in Java and Python is a programming methodology in which details of the programming codes are hidden away from the user, and only the essential things are displayed to the user.
Abstraction is concerned with ideas rather than events. Abstraction is achieved in either Abstract classes or interface in Java and Python. In essence, abstraction displays the essential details for the user alone. The main idea behind Object Oriented Programming is simplicity, code reusability, extendibility, and security. These are achieved through Encapsulation, abstraction, inheritance, and polymorphism.
0コメント