Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 83 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
83
Dung lượng
389,87 KB
Nội dung
Chapter 7 Object-Oriented Programming “The fruit is too well known to need any description of its external characteristics.” – From entry “Apple”, Encyclopaedia Britannica (11th edition) This chapter introduces a particularly useful way of structuring stateful pro- grams called object-oriented programming. It introduces one new concept over the last chapter, namely inheritance, which allows to define ADTs in incremen- tal fashion. However, the computation model is the same stateful model as in the previous chapter. We can loosely define object-oriented programming as programming with encapsulation, explicit state, and inheritance. It is often sup- ported by a linguistic abstraction, the concept of class, but it does not have to be. Object-oriented programs can be written in almost any language. From a historical viewpoint, the introduction of object-oriented programming made two major contributions to the discipline of programming. First, it made clear that encapsulation is essential. Programs should be organized as collec- tions of ADTs. This was first clearly stated in the classic article on “information hiding” [142], reprinted in [144]. Each module, component, or object has a “se- cret” known only to itself. Second, it showed the importance of building ADTs incrementally, using inheritance. This avoids duplicated code. Object-oriented programming is one of the most successful and pervasive ar- eas in informatics. From its timid beginnings in the 1960’s it has invaded every area of informatics, both in scientific research and technology development. The first object-oriented language was Simula 67, developed in 1967 as a descendant of Algol 60 [130, 137, 152]. Simula 67 was much ahead of its time and had little immediate influence. Much more influential in making object-oriented program- ming popular was Smalltalk-80, released in 1980 as the result of research done in the 1970’s [60]. The currently most popular programming languages, Java and C++, are object-oriented [186, 184]. The most popular “language-independent” design aids, the Unified Modeling Language (UML) and Design Patterns, both implicitly assume that the underlying language is object-oriented [58, 159]. With Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 494 Object-Oriented Programming all this exposure, one might feel that object-oriented programming is well under- stood (see the chapter quote). Yet, this is far from being the case. Structure of the chapter The purpose of this chapter is not to cover all of object-oriented programming in 100 pages or less. This is impossible. Instead, we give an introduction that emphasizes areas where other programming books are weak: the relationship with other computation models, the precise semantics, and the possibilities of dynamic typing. The chapter is structured as follows: • Motivations (Section 7.1). We give the principal motivation for object- oriented programming, namely to support inheritance, and how its features relate to this. • An object-oriented computation model (Sections 7.2 and 7.3). We define an object system that takes advantage of dynamic typing to combine simplicity and flexibility. This allows us to explore better the limits of the object-oriented abstraction and situate existing languages within them. We single out three areas: controlling encapsulation, single and multiple inheritance, and higher-order programming techniques. We give the object system syntactic and implementation support to make it easier to use and more efficient. • Programming with inheritance (Section 7.4). We explain the basic principles and techniques for using inheritance to construct object-oriented programs. We illustrate them with realistic example programs. We give pointers into the literature on object-oriented design. • Relation to other computation models (Section 7.5). From the view- point of multiple computation models, we show how and when to use and not use object-oriented programming. We relate it to component-based pro- gramming, object-based programming, and higher-order programming. We give additional design techniques that become possible when it is used to- gether with other models. We explain the pros and cons of the oft-repeated principle stating that every language entity should be an object. This prin- ciple has guided the design of several major object-oriented languages, but is often misunderstood. • Implementing the object system (Section 7.6). We give a simple and precise semantics of our object system, by implementing it in terms of the stateful computation model. Because the implementation uses a compu- tation model with a precise semantics, we can consider it as a semantic definition. Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 7.1 Motivations 495 • The Java language (Section 7.7). We give an overview of the sequential part of Java, a popular object-oriented programming language. We show how the concepts of Java fit in the object system of the chapter. • Active objects (Section 7.8). An active object extends a port object of Chapter 5 by using a class to define its behavior. This combines the abilities of object-oriented programming with message-passing concurrency. After reading this chapter, you will have a better view of what object-oriented programming is about, how to situate it among other computation models, and how to use the expressiveness it offers. Object-Oriented Software Construction For more information on object-oriented programming techniques and principles, we recommend the book Object-Oriented Software Construction, Second Edition, by Bertrand Meyer [122]. This book is especially interesting for its detailed discussion of inheritance, including multiple inheritance. 7.1 Motivations 7.1.1 Inheritance As we saw in the previous chapter, stateful abstract data types are a very useful concept for organizing a program. In fact, a program can be built in a hierarchical structure as ADTs that depend on other ADTs. This is the idea of component- based programming. Object-oriented programming takes this idea one step further. It is based on the observation that components frequently have much in common. Take the example of sequences. There are many different ADTs that are “sequence-like”. Sometimes we want them to behave like stacks (adding and deleting at the same end). Sometimes we want them to behave like queues (adding and deleting at opposite ends). And so forth, with dozens of possibilities. All of these sequences share the basic, linear-order property of the concept of sequence. How can we implement them without duplicating the common parts? Object-oriented programming answers this question by introducing the addi- tional concept of inheritance. An ADT can be defined to “inherit” from other ADTs, that is, to have substantially the same functionality as the others, with possibly some modifications and extensions. Only the differences between the ADT and its ancestors have to be specified. Such an incremental definition of an ADT is called a class. Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 496 Object-Oriented Programming Stateful model with inheritance Inheritance is the essential difference between object-oriented programming and most other kinds of stateful programming. It is important to emphasize that inheritance is a programming technique; the underlying computation model of object-oriented programming is simply the stateful model (or the shared-state concurrent model, for concurrent object-oriented programming). Object-oriented languages provide linguistic support for inheritance by adding classes as a lin- guistic abstraction. Caveats It turns out that inheritance is a very rich concept that can be rather tricky. There are many ways that an ADT can be built by modifying other ADTs. The primary approach used in object-oriented programming is syntactic:anew ADT is defined by doing simple syntactic manipulations of an existing ADT. Because the resulting changes in semantics are not always easy to infer, these manipulations must be done with great care. The component approach to building systems is much simpler. A component groups together any set of entities and treats them as a unit from the viewpoint of use dependency. A component is built from subcomponents, respecting their specifications. Potential Despite the difficulties of using inheritance, it has a great potential: it increases the possibilities of factoring an application, i.e., to make sure that each abstrac- tion is implemented just once. Having more than one implementation of an abstraction does not just make the program longer. It is an invitation to dis- aster: if one implementation is changed, then the others must also be changed. What’s more, the different implementations are usually slightly different, which makes nonobvious the relationships among all the changes. This “code duplica- tion” of an abstraction is one of the biggest sources of errors. Inheritance has the potential to remove this duplication. The potential to factor an application is a two-edged sword. It comes at the price of “spreading out” an ADT’s implementation over large parts of the program. The implementation of an ADT does not exist in one place; all the ADTs that are part of it have to be considered together. Even stronger, part of the implementation may exist only as compiled code, with no access to the source code. Early on, it was believed that inheritance would solve the problem of software reuse. That is, it would make it easier to build libraries that can be distributed to third parties, for use in other applications. This has not worked out in prac- tice. The failure of inheritance as a reuse technique is clear from the success of Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 7.1 Motivations 497 other techniques such as components, frameworks, and design patterns. Inheri- tance remains most useful within a single application or closely-related family of applications. Inheritance is not an unmixed blessing, but it takes its place next to higher- order programming as one of the most important techniques for structuring a program. 7.1.2 Encapsulated state and inheritance The combination of encapsulating explicit state and inheritance has led to the field of object-oriented programming, which is presented in this chapter. This field has developed a rich theory and practice on how to write stateful programs with inheritance. Unfortunately, this theory tends to consider everything as be- ing an object and to mix the notions of state and encapsulation. The advantages to be gained by considering other entities than objects and by using encapsula- tion without state are often ignored. Chapters 3 and 4 explain well how to use these two ideas. The present chapter follows the object-oriented philosophy and emphasizes how to build ADTs with both explicit state and inheritance. Most object-oriented programming languages consider that ADTs should have explicit state by default. For example, Smalltalk, C++, and Java all consider variables to be stateful, i.e., mutable, by default. In Java it is possible to make variables immutable by declaring them as final, but it is not the default. This goes against the rule of thumb given in Section 4.7.6, and in our view it is a mistake. Explicit state is a complex notion which should not be the first one that students are taught. There are simpler ways to program, e.g., using variable identifiers to refer to values or dataflow variables. These simpler ways should be considered first before moving to explicit state. 7.1.3 Objects and classes An object is an entity that encapsulates a state so that it can only be accessed in a controlled way from outside the object. The access is provided by means of methods, which are procedures that are accessible from the outside and that can directly access the internal state. The only way to modify the state is by calling the methods. This means that the object can guarantee that the state always satisfies some invariant property. A class is an entity that specifies an object in an incremental way, by defining the classes that the object inherits from (its direct ancestors) and defining how the class is different from the direct ancestors. Most modern languages support classes as a linguistic abstraction. We will do the same in this chapter. To make the concepts precise we will add a simple yet powerful class construct. This chapter only talks about objects that are used sequentially, i.e., that are used in a single thread. Chapter 8 explains how to use objects in a concurrent Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 498 Object-Oriented Programming class Counter attr val meth init(Value) val:=Value end meth browse {Browse @val} end meth inc(Value) val:=@val+Value end end Figure 7.1: An example class Counter (with class syntax) setting, when multiple threads use the objects. In particular, object locking is explained there. 7.2 Classes as complete ADTs The heart of the object concept is controlled access to encapsulated data. The behavior of an object is specified by a class. In the most general case, a class is an incremental definition of an ADT, that defines the ADT as a modification of other ADTs. There is a rich set of concepts for defining classes. We classify these concepts into two sets, according as they permit the class to define an ADT completely or incrementally: • Complete ADT definition. These are all the concepts that permit a class, taken by itself, to define an ADT. There are two sets of concepts: – Defining the various elements that make up a class (Section 7.2.3), namely methods, attributes, and properties. Attributes can be initial- ized in several ways, per object or per class (Section 7.2.4). – Taking advantage of dynamic typing. This gives first-class messages (Section 7.2.5) and first-class attributes (Section 7.2.6). This allows powerful forms of polymorphism that are difficult or impossible to do in statically-typed languages. This increased freedom comes with an increased responsibility of the programmer to use it correctly. • Incremental ADT definition. These are all the concepts related to in- heritance, that is, they define how a class is related to existing classes. They are given in Section 7.3. Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 7.2 Classes as complete ADTs 499 local proc {Init M S} init(Value)=M in (S.val):=Value end proc {Browse2 M S} {Browse @(S.val)} end proc {Inc M S} inc(Value)=M in (S.val):=@(S.val)+Value end in Counter=c(attrs:[val] methods:m(init:Init browse:Browse2 inc:Inc)) end Figure 7.2: Defining the Counter class (without syntactic support) 7.2.1 An example To see how classes and objects work in the object system, let us define an example class and use it to create an object. We assume that the language has a new construct, the class declaration. We assume that classes are first-class values in the language. This lets us use a class declaration as either statement or expression, in similar manner to a proc declaration. Later on in the chapter, we will see how to define classes in the kernel language of the stateful model. This would let us define class as a linguistic abstraction. Figure 7.1 defines a class referred to by the variable Counter. This class has one attribute, val, that holds a counter’s current value, and three methods, init, browse,andinc, for initializing, displaying, and incrementing the counter. The attribute is assigned with the := operator and accessed with the @ operator. This seems quite similar to how other languages would do it, modulo a different syntax. But appearances can be deceiving! The declaration of Figure 7.1 is actually executed at run time, i.e., it is a statement that creates a class value and binds it to Counter. Replace “Counter” by “ $” and the declaration can be used in an expression. Putting this declaration at the head of a program will declare the class before executing the rest, which is familiar behavior. But this is not the only possibility. The declaration can be put anywhere that a statement can be. For example, putting the declaration inside a procedure will create a new and distinct class each time the procedure is called. Later on we will use this possibility to make parameterized classes. Let us create an object of class Counter and do some operations with it: C={New Counter init(0)} {C inc(6)} {C inc(6)} {C browse} This creates the counter object C with initial value 0, increments it twice by 6, Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 500 Object-Oriented Programming fun {New Class Init} Fs={Map Class.attrs fun {$ X} X#{NewCell _} end} S={List.toRecord state Fs} proc {Obj M} {Class.methods.{Label M} M S} end in {Obj Init} Obj end Figure 7.3: Creating a Counter object and then displays the counter’s value. The statement {C inc(6)} is called an object application. The message inc(6) is sent to the object, which invokes the corresponding method. Now try the following: local X in {C inc(X)} X=5 end {C browse} This displays nothing at all! The reason is that the object application {C inc(X)} blocks inside the method inc. Can you see exactly where? Now try the following variation: declare S in local X in thread {C inc(X)} S=unit end X=5 end {Wait S} {C browse} Things now work as expected. We see that dataflow execution keeps its familiar behavior when used with objects. 7.2.2 Semantics of the example Before going on to describe the additional abilities of classes, let us give the semantics of the Counter example. It is a simple application of higher-order programming with explicit state. The semantics we give here is slightly simplified; it leaves out the abilities of class that are not used in the example (such as inheritance and self). Section 7.6 gives the full semantics. Figure 7.2 shows what Figure 7.1 does by giving the definition of the class Counter in the stateful model without any class syntax. We can see that according to this definition, a class is simply a record containing a set of attribute names and a set of methods. An attribute name is a literal. A method is a procedure that has two arguments, the message and the object state. In each method, assigning to an attribute (“ val:=”) is done with a cell assignment and accessing an attribute (“ @val”) is done with a cell access. Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 7.2 Classes as complete ADTs 501 statement ::= class variable{classDescriptor} { meth methHead [ ´=´ variable ] ( inExpression|inStatement ) end } end | lock [ expression then ] inStatement end |expression ´:=´ expression |expression ´,´ expression | expression ::= class ´$´ {classDescriptor} { meth methHead [ ´=´ variable ] ( inExpression|inStatement ) end } end | lock [ expression then ] inExpression end |expression ´:=´ expression |expression ´,´ expression | ´@´ expression | self | classDescriptor ::= from {expression}+ | prop {expression}+ | attr {attrInit}+ attrInit ::= ( [ ´!´ ] variable|atom|unit | true | false ) [ ´:´ expression ] methHead ::= ( [ ´!´ ] variable|atom|unit | true | false ) [ ´(´ {methArg}[ ´ ´ ] ´)´ ] [ ´=´ variable ] methArg ::= [ feature ´:´ ](variable|´_´ | ´$´ )[´<=´ expression ] Table 7.1: Class syntax Figure 7.3 defines the function New which is used to create objects from classes. This function creates the object state, defines a one-argument procedure Obj that is the object, and initializes the object before returning it. The object state S is a record holding one cell for each attribute. The object state is hidden inside Obj by lexical scoping. 7.2.3 Defining classes A class is a data structure that defines an object’s internal state (attributes), its behavior (methods), the classes it inherits from, and several other properties and operations that we will see later on. More generally, a class is a data structure that describes an ADT and gives its partial or total implementation. Table 7.1 gives the syntax of classes. There can be any number of objects of a given class. They are called instances of the class. These objects have different identities Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. 502 Object-Oriented Programming and can have different values for their internal state. Otherwise, all objects of a given class behave according to the class definition. An object Obj is called with the syntax {Obj M},whereM is a record that defines the message. Calling an object is also called sending a message to the object. This terminology exists for historical reasons; we do not recommend it since it is easily confused with sending a message on a communication channel. An object invocation is synchronous, like a procedure’s. The invocation returns only when the method has completely executed. A class defines the constituent parts that each instance will have. In object- oriented terminology, these parts are often called members. There are three kinds of members: • Attributes (declared with the keyword “ attr”). An attribute, is a cell that contains part of the instance’s state. In object-oriented terminology, an attribute is often called an instance variable. The attribute can contain any language entity. The attribute is visible only in the class definition and all classes that inherit from it. Every instance has a separate set of attributes. The instance can update an attribute with the following operations: – An assignment statement: expr 1 :=expr 2 . This assigns the result of evaluating expr 2 to the attribute whose name is obtained by evalu- ating expr 1 . – An access operation: @expr. This accesses the attribute whose name is obtained by evaluating expr. The access operation can be used in any expression that is lexically inside the class definition. In particular, it can be used inside of procedures that are defined inside the class. – An exchange operation. If the assignment expr 1 :=expr 2 is used as an expression, then it has the effect of an exchange. For example, consider the statement expr 3 =expr 1 :=expr 2 .Thisfirstevaluates the three expressions. Then it it unifies expr 3 with the content of the attribute expr 1 and atomically sets the new content to expr 2 . • Methods (declared with the keyword “ meth”). A method is a kind of procedure that is called in the context of a particular object and that can access the object’s attributes. The method consists of a head and body. The head consists of a label, which must be an atom or a name, and a set of arguments. The arguments must be distinct variables, otherwise there is a syntax error. For increased expressiveness, method heads are similar to patterns and messages are similar to records. Section 7.2.5 explains the possibilities. • Properties (declared with the keyword “ prop”). A property modifies how an object behaves. For example: Copyright c 2001-3 by P. Van Roy and S. Haridi. All rights reserved. [...]... instance of C can call method A in any other instance of C Method A is invisible to subclass definitions This is a kind of horizontal visibility It corresponds to the concept of private method as it exists in C++ and Java (but not in Smalltalk) As Figure 7. 7 shows, private in C++ and Java is very different from private in Smalltalk and Oz In Smalltalk and Oz, private is relative to an object and its classes,... Programming with inheritance All the programming techniques of stateful programming and declarative programming are still possible in the object system of this chapter Particularly useful are techniques that are based on encapsulation and state to make programs modular See the previous chapter, and especially the discussion of componentbased programming, which relies on encapsulation This section focuses... possible by objectoriented programming All these techniques center around the use of inheritance: first, using it correctly, and then, taking advantage of its power 7. 4.1 The correct use of inheritance There are two ways to view inheritance: Copyright c 200 1-3 by P Van Roy and S Haridi All rights reserved 7. 4 Programming with inheritance Account VerboseAccount AccountWithFee Figure 7. 11: A simple hierarchy... clone of self and binds it to X The clone is a new object with the same class and the same values of attributes • toChunk(X) binds to X a protected value (a “chunk”) that contains the current values of the attributes • fromChunk(X) sets the object state to X, where X was obtained from a previous call of toChunk Copyright c 200 1-3 by P Van Roy and S Haridi All rights reserved 523 524 Object-Oriented Programming. .. new class and in the preexisting classes It is done with static and dynamic binding and the concept of self • The third is encapsulation control (Section 7. 3.3), which defines what part of a program can see a classes’ attributes and methods In addition, the model can use first-class messages to implement delegation, a completely different way to define ADTs incrementally (see Section 7. 3.4) 7. 3.1 Inheritance... messages as values, and the dynamic creation of classes We start with forwarding since it is the simplest Copyright c 200 1-3 by P Van Roy and S Haridi All rights reserved 518 Object-Oriented Programming Forwarding An object can forward to any other object In the object system of this chapter, this can be implemented with the otherwise(M) method (see Section 7. 2.5) The argument M is a first-class message... Section 7. 5 7. 3 Classes as incremental ADTs As explained before, the main addition that object-oriented programming adds to component-based programming is inheritance Object-oriented programming allows to define a class incrementally, by extending existing classes It is not enough to say which classes are extended; to properly define a new ADT more concepts are needed Our model includes three sets of concepts:... protocols were originally invented in the context of the Common Lisp Object System (CLOS) [100, 140] They are an active area of research in object-oriented programming Method wrapping A common use of meta-object protocols is to do method wrapping, that is, to intercept each method call, possibly performing a user-defined operation before and after the call and possibly changing the arguments to the call... advantage of the fact that objects are one-argument procedures For example, let us write a tracer to track the behavior of an object-oriented program The tracer should display the method label whenever we enter a method and exit a method Here is a version of New that implements this: fun {TraceNew Class Init} Obj={New Class Init} Copyright c 200 1-3 by P Van Roy and S Haridi All rights reserved 7. 3 Classes... spirit of [54], we will try to bring order to this chaos Copyright c 200 1-3 by P Van Roy and S Haridi All rights reserved 7. 3 Classes as incremental ADTs 513 = C Class hierarchy Region of visibility for object I3: all private attributes in this region are visible to I3 ‘‘private’’ according to Smalltalk and Oz SubC SubSubC Instances I1 I2 I3 In ‘‘private’’ according to C++ and Java Figure 7. 7: The . object-oriented design. • Relation to other computation models (Section 7. 5). From the view- point of multiple computation models, we show how and when to use and not use object-oriented programming. . component-based pro- gramming, object-based programming, and higher-order programming. We give additional design techniques that become possible when it is used to- gether with other models. We. 200 1-3 by P. Van Roy and S. Haridi. All rights reserved. 7. 1 Motivations 495 • The Java language (Section 7. 7). We give an overview of the sequential part of Java, a popular object-oriented programming