1.7 Abstraction

Abstraction

When designing an object-oriented application, you'll discover that the objects are made up of many objects and classes. Because object-oriented development is programming by exception, the idea is to find as many common methods and properties for these classes as possible. This allows us to create common parent classes that define interfaces and have code that is inherited by all the subclasses.

The process of discovering the common parent classes is called abstraction. This process adds a lot of power to object-oriented applications.

It is important to remember that abstraction is not trivial, and it requires good design. If we just start coding, we won't discover those common parts and we'll lose most of the advantages of object-oriented programming. For this reason, I devoted Section 3 of this book to the analysis and design process to help with abstraction.

Building class hierarchies

Building class hierarchies is one of the most important tasks when creating an object-oriented application. How to build class hierarchies has been and still is an ongoing discussion. I don't think there is one right answer. "It depends" seems to fit here. Some prefer deep class hierarchies, where they build many levels of subclassing. Others prefer broad inheritance structures, where there are lots of subclasses from a single parent class, but the subclassing goes down only one or two levels. You might look at these two different ways of doing things as frameworks (deep structure) and as components (broad structure).

I don't think one-dimensional structures (either broad or deep) are a good way to go. Object-oriented design is not that simplistic after all. I think the idea of components is a good starting point, but I don't buy into the idea of being able to plug all kinds of objects together. Objects have to know about each other. They have to interact in order to do something useful. This seems to work best in a two-dimensional inheritance model. Going this route, you can still plug all the components together. But because they support standard interfaces (see below) and follow basic rules, they will be able to accomplish huge tasks.

I usually like to compare this to the electronics business. If someone wants to build something complex (like a computer), he has lots of standard parts he can use. All these parts seem to be stand-alone objects. But if you look at it in more detail, they all follow general rules and standards.

Abstract vs. concrete classes

Abstract classes serve only the purpose of building class hierarchies. They are never instantiated. (Repeat after me: "Abstract classes are never instantiated.") They define interfaces, provide common behavior, or are simply added for possible adaptations later. Abstract classes always have concrete child classes.

Concrete classes are the ones that will be instantiated. In a good design, concrete classes have abstract parent classes to make later changes easy and straightforward. Of course, concrete classes can also be subclassed further. In simple scenarios, concrete classes could be the first level of classes in the inheritance structure.

A typical example of abstract and concrete classes is the Screen class in the sample above. We might have an abstract Screen class that defines everything a typical screen would have, but we would never use this class directly. We'd rather create specialized subclasses for individual screens. We could have a class for the screen with balcony seating, or one class for the big screen, and so on. We'd use these classes in our application.

As you can see in this example, we might have some reasons to directly use the abstract Screen class in very simple scenarios. In this case, the Screen class might be a concrete class as well as an abstract class, depending on the point of view. However, we would no longer refer to it as an abstract class as soon as we directly used it once.

Figure 3 shows a simple diagram of abstract and concrete classes.

Figure 3. Abstract and concrete classes.

In this example, we defined an abstract Screen class (abstract classes are usually drawn using italic fonts). The Screen class that has been subclassed from AbstractScreen has no new properties or methods. It might have overwritten ones, but we couldn't tell from this kind of diagram. The Screen class has another subclass called BalconyScreen, which is also a concrete class. It also has one new property.

Hiding information

In order to provide clean classes and interfaces, we need a mechanism that allows the programmer to hide complexity inside an object.

There are several good reasons for hiding information. First, you don't want somebody to be able to change internals of your class, because this is a potential source for bugs. Perhaps the object encapsulates sensitive data, in which case you'd want to provide access only to certain properties and methods. After all, it wouldn't make sense to require a password when someone could simply look at the password property.

But even if you are the only user of your classes, there are good reasons to hide information. The first reason is to keep everything simple and easy to use. After all, you still want to be able to use your classes a year after creating them, don't you? If you look at 100 different methods and properties, reuse might be hard. But all you might need to do is to send a handful of messages. If all the implementation-specific methods are hidden, it's very hard to use the object incorrectly. Avoiding mistakes must be one of the highest priorities when developing an object-oriented program. Modern-day business applications have grown so complex, that it's simply impossible to remember every little detail about every single object. Setting a property that is crucial for a certain method can break huge amounts of code. So the simpler an object is to reuse, the higher the code quality and the lower the maintenance costs will be.

Visual FoxPro supports two different ways of hiding properties and methods: protecting and hiding. Let's see how these mechanisms work and how they differ.

Protecting properties and methods

Protecting a property or method means hiding it from the outside world. It can be accessed only from a method belonging to the same class. To the outside world, the property or method is totally invisible. If someone tried to access it, she would receive a message that the property or method didn't exist.

In Visual FoxPro, we use the PROTECTED keyword to create protected properties and methods, as in the following example:

DEFINE CLASS ProtectedDemo AS Custom
PROTECTED Property1

PROTECTED FUNCTION Method1
THIS.Property1 = "Test"
ENDFUNC

FUNCTION Method2
THIS.Method1()

ENDFUNC
ENDDEFINE

In this example, the property Property1 has been defined as protected. This means that the following call would result in an error:

oProtectedDemo.Property1 = "Test"

However, Method1 can still access that property because it belongs to the same class. The method itself is also protected, which means it can't be accessed from the outside, either. But again: Method2 can access it because it belongs to the same class and it isn't protected therefore it's visible to everybody.

We could even create a subclass of that class like so:

DEFINE CLASS ProtectedDemoSubclass AS ProtectedDemo
FUNCTION Method2
DoDefault()
WAIT WIND THIS.Property1

ENDFUNC
ENDDEFINE

This would still work, because this class would also inherit the protected property. The property would therefore belong to this class, and all the methods in the class could access its protected properties and methods.

Here is a sample that uses protected properties to encapsulate sensitive data:

DEFINE CLASS SecureData AS Custom
PROTECTED Password, CustomerCreditLimit

Password = "foxpro"
CustomerCreditLimit = 1000000

FUNCTION GetCreditLimit( Password )
IF THIS.Password == Password
RETURN THIS.CustomerCreditLimit
ELSE
RETURN -1
ENDIF
ENDFUNC
ENDDFINE

This class tells us about a customer's credit limit. Of course, not everybody should have access to this kind of information, so we have a function that allows only authorized users to access it. In order to read the credit limit, we have to supply a password.

Of course, it would be really stupid if just anyone could access these properties. But because they are protected, it looks like they don't even exist. Also keep in mind that these properties might be assigned on the fly, and on top of that, one doesn't necessarily have the source code for this class.

Note that a property must first be declared protected before you can assign initial values. This works similarly to declaring variables where you would use LOCAL, PRIVATE, and so on, instead of PROTECTED.

Hiding properties and methods

As you saw above, protected properties and methods are still visible in subclasses. Depending on the circumstances, this might not be what you need. In the sample above, we might simply create a subclass of SecureData, add methods to retrieve the data, and our security would be useless.

But there is another way to limit access to that data. Using the Visual FoxPro keyword HIDDEN, we can actually hide properties and methods not only from the outside world, but also from subclasses. Here is a sample that shows how HIDDEN works:

DEFINE CLASS HiddenDemo AS Custom
HIDDEN Property1

HIDDEN FUNCTION Method1
THIS.Property1 = "Test"
ENDFUNC

FUNCTION Method2
THIS.Method1()
ENDFUNC
ENDDEFINE

DEFINE CLASS HiddenSubclassDemo AS HiddenDemo
FUNCTION Method2
DoDefault()
WAIT WIND THIS.Property1 && This line will fail

ENDFUNC
ENDDEFINE

In this example, Method2 in the class HiddenSubclassDemo would fail, because it wouldn't have access to the hidden Property1. The DoDefault() would still work, because the program focus would move to the parent class (HiddenDemo), and the hidden property is visible at this level.

This allows us to hide implementation details from subclasses. Imagine using a framework created by somebody else. It would be a hassle to deal with all the properties and methods needed by the creator of the framework. Some methods might be specific to the problems he had to solve and not useful for you at all, such as the properties CharBuffer, InstanceCounter, or something else along those lines. It might even break the whole class to access these methods or change the value of these properties.

Interfaces

The sum of the properties and methods that are visible to the outside world is called the object's interface, or programming interface.

This term should not be confused with user interface. Sometimes it's hard to differentiate between the two, especially when you talk to object gurus. When one object sends messages to another, the sender is also called the using object, or simply the user. But of course, the same term could also refer to a human user, especially when third-party class libraries or COM/OLE interfaces are involved.

Designing good object interfaces is at least as hard as creating good user interfaces, and you should give it a lot of thought. Interfaces should be very clear and easy to use. They should be self-explanatory or well documented. Therefore, they should hide as much complexity as possible. Once an object's interface has been defined, be careful when making changes. Remember that other objects are depending on a certain protocol when "talking" to each other. Whenever an object's interface changes, all the users have to change the way they communicate with the modified object. This can be a big problem, especially when you create classes for other programmers, or for resale.

Interfaces can be used as wrappers. Imagine you have to use a class that is incompatible with your other classes or framework. In this case, you can subclass the class, hide the original functionality and create the same interface that reroutes all the calls to the different internals. This will enable you to plug this class into your application as if it were designed for it.

The interface should provide straightforward ways to accomplish goals. You should avoid having multiple ways to do a task. This makes maintenance a lot easier and guarantees better reuse. Imagine a screen class in the theater example that has five different ways to turn on the lights. Whenever a new light switch is added, you would have to create another method to turn on that light. Then all the objects that used this interface would have to learn about this new method. This requires lots of changes.

A better way to do this is to provide one method to toggle the switches, even if you need five completely different internal methods. Objects can send a parameter to specify what lights they want to turn on. This has a couple of advantages. First of all, an object might try to turn on a light that doesn't even exist in that room. But instead of calling a method that doesn't exist (and therefore causes an error), the interface catches the mistake and can take proper action to avoid problems. Other objects might not know what lights they can turn on. In this case we could create a smart interface that allows the user to query the existing lights in order to get a list of valid actions. (This is how OLE works, by the way.) Or, the interface method could make an educated guess and turn on the most common lights.

No matter what route you take, the simple, well-defined and unified interface provides more power, more flexibility and better code quality than exposing five different methods. It makes classes easier to use (which is a big step toward code reuse) and it reduces the opportunity to make mistakes (which is a big step toward product release). And it's easy and straightforward to create, so you pay very little and gain a lot. (This should be an easy sale )

Interface vs. implementation

A common mistake that programmers make is to confuse the interface with the implementation and vice versa. This usually happens because most people don't differentiate between the two and simply expose implementation methods and properties. This has a couple of disadvantages.

It's pretty hard to keep a clean interface by simply exposing the implementation methods, because objects usually do things that are quite different. When you create business objects, they might have a number of similarities. Actually, they might and should be subclasses of each other. In this case the common interface would be maintained almost automatically, thanks to inheritance. But in larger object systems, an application might deal with totally different objects where the implementation varies greatly. An example of that would be the Start method of a car and the Start method of a movie projector. The actual implementation in those objects will vary greatly. But that shouldn't matter in interface terms. After all, the user should not be concerned with the object's internals.

Common interfaces are often defined in common parent classes, which are abstract classes. Most likely, they don't contain any code because their whole purpose is to provide a standard interface that's inherited by all the subclasses. All the additional methods that contain the implementation code are located in the child classes, and they should be either protected or hidden so as not to pollute the interface.

Another thing to keep in mind is that an interface shouldn't be influenced by coding standards or other personal preferences. Hungarian notation might be a great thing to have, but it shouldn't show in the interface unless you are the only user. This is especially true for class libraries that are sold or given away, or for automation interfaces. How would you like to be stuck with some kind of Delphi or C++ naming convention, just because the programmer of the class liked it that way?

Polymorphism revisited

As you can imagine by now, polymorphism makes sense only if you have well-abstracted classes and well-defined interfaces. In the sample above, a manager object would be able to handle all the screens in the theater only if every screen object had the same set of methods and properties. If one object violates this interface, the whole idea of polymorphism doesn't work anymore projects grow too complex and disasters are guaranteed. This shows how important it is to properly design object-oriented applications. Changing interfaces late in development can introduce major problems and is a potential source for bugs.

Earlier, I described how to implement a special screen class that allows customers to buy tickets for balcony seats. I described this by passing another parameter to the ReserveSeats() method. This is an easy and straightforward way to go, but it introduces a couple of problems. We are using polymorphism, but we changed the interface by requiring a second parameter. In order to do that, the object that sends the ReserveSeats() method has to know about two different interfaces. That's not good. In this case we might as well create a second method to buy tickets for the balcony area. This would reduce the risk of confusing two methods that have the same name but different interfaces. Calling the wrong method with the wrong number of parameters would make it hard to find bugs, because everything would look okay in the debugger and the programmer has to remember that he created two different interfaces. Imagine a scenario where a person calls the ReserveSeats() method of a regular screen and passes two parameters in order to reserve a balcony seat. This might easily occur because he might not be aware that the screen he is talking to doesn't feature balcony seating. He would start the debugger and wonder why the method was called with an invalid number of parameters, not knowing that the calling object expected balcony seating. But when the object actually sent a separate ReserveBalconySeats() method, he could figure out immediately what was wrong and why this message was sent. So from a functional point of view, it wouldn't really make a difference because both scenarios would fail at runtime. Even though they would both fail, it's important to remember that it is harder to debug the single-method scenario.

This leads to the question of how to resolve this problem properly once a person found the bug and isolated the problem. There are different ways to do so. The common way seems to be to use a property in the message receiver that determines what the next message is supposed to do, but this method has several problems. Not only do we still have a different interface for this object, but we also have to take two steps. Another way would be to pass an object as a parameter, rather than a single parameter. The properties of the object would specify what kind of ticket we wanted to buy. This would leave the interface clean but would complicate the implementation. Between the two, messing with implementation is always preferred to polluting the design.

There is another way to look at this. It might not be a coding problem after all. Let's think through the scenario one more time: Suppose some manager object makes calls to screen objects in order to make seat reservations. To reserve seats in special areas that are only featured by some screens, the manager object has to know about these features. So we might as well create a second method to make those special reservations. This might again be done in a polymorphic way by providing a ReserveSpecialSeat() method in the example. But in this case I'd recommend using separate method names in order to avoid further confusion.

Don't make the mistake of creating Swiss-army-knife methods. Having clean and generic interfaces is powerful in many scenarios. But no manager object will be able to reserve a balcony seat if it doesn't know these seats exist. As you've seen, it might be better to enhance the interface to avoid confusion and make it hard to find bugs. But this doesn't mean you shouldn't try to reuse code. The special method to reserve balcony seats might just set a couple of parameters and then call the original method.

As you can see, the decision whether to enhance the interface or to enhance existing functionality depends on the particular scenario. Usually, you'll find these gray areas in the analysis and design phase. Yet another good reason to use proper design

Naming conventions

Of course, using polymorphism requires the programmer to have naming conventions. It doesn't make sense to call the method to store data "save" in one class and "SaveDocument" in the other. This might be an obvious example, but it gets trickier in more specific scenarios. For this reason, naming conventions are required in order to avoid name mix-ups that would ruin the idea of polymorphism for no good reason.

I describe some naming conventions in Chapter 6. These conventions are just suggestions. Feel free to use them, modify them or come up with your own conventions. It doesn't matter what conventions you use, as long as you use them consistently.



Advanced Object Oriented Programming with Visual FoxPro 6. 0
Advanced Object Oriented Programming with Visual FoxPro 6.0
ISBN: 0965509389
EAN: 2147483647
Year: 1998
Pages: 113
Authors: Markus Egger

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