Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Symbian OS Explained - Effective C++ Programming For Smartphones (2005) [eng].pdf
Скачиваний:
60
Добавлен:
16.08.2013
Размер:
2.62 Mб
Скачать

MEMBER DATA AND FUNCTIONAL ABSTRACTION

309

20.4 Member Data and Functional Abstraction

In this discussion on the API of your class, I’ve discussed the definition of the member functions of your class, but not really touched on the member data. There’s a very simple rule, which is that you should not make class data public, and there’s a good reason for this – encapsulation.

The benefit of keeping your member data private to the class is that you can control access to it. First, you can decide whether to expose the data at all; if you choose to do so, you can provide member functions in your class to expose the data. If you follow the guidelines above, you can control precisely the type of access allowed. If you return const references, const pointers or a value, the caller has read-only access, while returning a reference or a pointer allows the caller to modify the data, as illustrated by the following:

const TExample& ReadOnlyReference();

const TExample* ReadOnlyPointer();

TExample ReadOnlyValue();

TExample& ReadWriteReference();

TExample* ReadWritePointer();

An additional benefit of keeping class member data private is a degree of functional abstraction. By providing methods to Set() and Get(), the variable is not exposed directly. Should you later decide to change the implementation of the class, you can do so without requiring your clients to update their code.

For example, consider this rather contrived class which stores a password and compares it with a password typed in later, providing a re-usable class for applications to protect their user files. In version 1.0, the unencrypted password is, rather naively, stored within the object. The code is released and everyone is happy for a while, including a few hackers. A code review before version 2.0 highlights the problem. If the class has been declared as follows, any attempt to add more security to the class forces clients of the class to change their code. Incidentally, I discuss how to maintain compatibility for client code in Chapter 18.

// Version 1.0

class CPassword : public CBase

{

public:

... // Other functions omitted for clarity public:

//Bad! There are no access functions – the member data is public

//The caller can set and get the password directly

HBufC8* iPassword; };

310

EXPOSE A COMPREHENSIVE AND COMPREHENSIBLE API

The same problem arises in a second definition (version 1.1) below, but for different reasons. This class is a step in the right direction, because the password data is at least private. To set and get the password, the caller must call a method explicitly rather than modify the contents of the object directly.

// Version 1.1

class CPassword : public CBase

{

public:

...

IMPORT_C void SetPasswordL(const TDesC8& aPassword); inline HBufC8* Password(); // Better! But not great

private:

HBufC8* iPassword; // Better. Password is private };

inline HBufC8* CPassword::Password()

{

return (iPassword);

}

//SetPasswordL() is a leaving method because allocation

//of iPassword may leave if there is insufficient memory EXPORT_C void CPassword::SetPasswordL(const TDesC8& aPassword)

{

HBufC8* newPassword = aPassword.AllocL(); delete iPassword;

iPassword = newPassword;

}

To access the data, the caller must now call Password(), which provides read/write access to iPassword. Sure, it returns a constant heap descriptor, but from Chapters 5 and 6, you know how to call Des() on that pointer to get a modifiable pointer which can be used to update the contents of the buffer. This is far from ideal; the password should only be modifiable through the SetPasswordL() method, otherwise it’s confusing for the client to know which to use. To get read-only access, the method should either return a const HBufC8* or, preferably, return a constant reference to the more generic descriptor type, TDesC8, as follows:

// Version 1.2

class CPassword : public CBase

{

public:

...

// Better! Now a const method and return value inline const TDesC8& Password() const;

private:

MEMBER DATA AND FUNCTIONAL ABSTRACTION

311

HBufC8* iPassword; };

inline const TDes8C& CPassword::Password() const

{

return (*iPassword);

}

In fact, for access to the password, this class definition is not much of an improvement on the original one, because the method exposes the class implementation directly. It’s questionable whether there’s much benefit over the original version, since it doesn’t do anything additional to version 1.0, other than requiring a function call to retrieve the password. In fact, by implementing the accessor in an inline method, the additional overhead of a function call is removed at compile time. This is not necessarily a good thing in this case because its implementation is compiled into the caller’s code. It isn’t possible to update Password() without forcing clients of the class to recompile to receive the benefit of the new implementation, as I discussed in Chapter 18.

Here’s a better definition of the class (version 1.3). Notice that the data member is private; there is a single method to set the password and, rather than returning the password, a method is provided to compare an input password with the stored value, returning a TBool result to indicate a match. This method is not inlined:

// Version 1.3

class CPassword : public CBase

{

public:

IMPORT_C TBool ComparePassword(const TDesC8& aInput) const; // Implemented as shown previously

IMPORT_C void SetPasswordL(const TDesC8& aPassword); private:

HBufC8* iPassword; };

EXPORT_C TBool CPassword::ComparePassword(const TDesC8& aInput) const

{

return (0==iPassword->Compare(aInput));

}

A more secure storage mechanism is used in version 2.0, which doesn’t require the definition of class CPassword to change, but updates the internals of SetPasswordL() and ComparePassword() to store the password as a hash value, rather than ”in the clear”. By abstracting the functionality into the class, when version 2.0 is released existing clients of the library are unaffected by the upgrade.

312

EXPOSE A COMPREHENSIVE AND COMPREHENSIBLE API

// Version 2.0

EXPORT_C void CPassword::SetPasswordL(const TDesC8& aPassword)

{

TBuf8<KMaxHashSize> hashed;

//Fill hashed with a hash of aPassword using an appropriate method

//...

HBufC8* newPassword = hashed.AllocL(); delete iPassword;

iPassword = newPassword;

}

EXPORT_C TBool CPassword::ComparePassword(const TDesC8& aInput) const

{

TBuf8<KMaxHashSize> hashed;

//Fill hashed with a hash of aInput using an appropriate method

//...

return (0==iPassword->Compare(hashed));

}

Of course, there are still problems with this class, not least the fact that there is no error checking to see whether iPassword has actually been set by a call to SetPasswordL(). A better implementation would perhaps remove SetPasswordL() and require the password to be passed to a NewL() or NewLC() two-phase construction method, as described in Chapter 4. This would also limit when, in the object’s lifetime, the password could change, for better security. I’ll leave that implementation as an exercise for the reader.

Where possible, use const on parameters, return values, ”query” methods, and other methods that do not modify the internals of a class. It indicates invariance to other programmers and ensures that the compiler enforces it.

20.5Choosing Class, Method and Parameter Names

The title of this chapter includes the word ”comprehensible”, and this is an important issue when it comes to naming your class and the methods of its API. In all cases, you should strive for clear and distinct names, without making them so long as to be burdensome or, worse, abbreviating each to an acronym of your own choosing. Likewise, the names of the parameters in each method should be clear and descriptive. If you can, make it possible for your clients to write comprehensible code too. For example, in methods that take a number of initialization values, it’s a good idea to use custom enumerations with clear names. Not only does this make it easy for your client to work out which settings to choose, but

CHOOSING CLASS, METHOD AND PARAMETER NAMES

313

it’s easier when looking back at the code to understand what it means. For example, take the following function declaration in class CGpSurgery:

class CGpSurgery : public CBase

{

public:

void MakeHospitalAppointment(TBool aExistingPatient,

TBool aUrgency, TBool aTestData);

... // Other functions omitted for clarity

};

Without looking at the declaration of MakeHospitalAppointment(), it’s not immediately clear how to call the method, for example to make an urgent hospital appointment for a new patient who already has some test data available. Unless you name your parameters carefully, your client may well have to consult your documentation too, to find out the appropriate boolean value for each variable.

MakeHospitalAppointment(EFalse, ETrue, ETrue);

A far clearer approach is to use enumeration values whose name clearly indicates their purpose. However, if you define them in global scope, you or your clients may well find that a name clash arises with code in other header files, particularly if you choose short, simple names such as TSize or TColor. Even if your code doesn’t get a clash, this (ab)use of the global scope means that code which includes your header may get a conflict later down the line. It’s preferable to define them inside the class to which they apply. The caller then uses the class scope to identify them. This is a useful approach for class-specific enumerations, typedefs and constants. If you prefer to keep them out of your class scope, you could alternatively use a C++ namespace to prevent spilling your definitions into the global scope.

class CGpSurgery : public CBase

{

public:

enum TPatientStatus { EExistingPatient, ENewPatient }; enum TAppointmentUrgency { EUrgent, ERoutine };

enum TTestData { ETestResultsPending, ETestResultsAvailable }; public:

void MakeHospitalAppointment(TPatientStatus aExistingRecords, TAppointmentUrgency aUrgency, TTestData aTestData);

};

Looking at the function call, it is now significantly clearer what each parameter refers to; in effect, the code has documented itself.

MakeHospitalAppointment(CGpSurgery::ENewPatient, CGpSurgery::EUrgent,

CGpSurgery::ETestResultsAvailable);

314

EXPOSE A COMPREHENSIVE AND COMPREHENSIBLE API

The use of enumerations rather than boolean values also provides extensibility in the future; in the case of the CGpSurgery class, additional levels of appointment urgency, patient status or test data availability can be introduced without the need to change the signature of MakeHospitalAppointment(). This avoids breaking compatibility – which is the subject of Chapter 18.

20.6Compiler-Generated Functions

I’ve discussed some of the things to consider when defining your class, but before concluding this chapter, it’s worth briefly describing the functions that the compiler generates for you if you don’t add them yourself. If you have not declared a copy constructor, assignment operator or destructor, the compiler generates them implicitly for a class in case they need to be invoked. If there are no constructors declared, it declares a default constructor too.

The implicitly-generated functions are public; the constructor and destructor are simply placeholders for the compiler to add the code required to create and destroy an object of the class (for example, to set up or destroy the virtual function table). The destructor does not perform any cleanup code. The compiler-generated copy constructor and assignment operator perform a copy or assignment on each member of your class, invoking the copy constructor or assignment operator if there is one defined, or applying the rule for the members of the encapsulated object if not. Built-in types, pointers and references are copied or assigned using a shallow bitwise copy. This is problematic for pointers to objects on the heap, since a bitwise copy is rarely desirable, opening up opportunities for dangling pointers; these can result in a memory leak or multiple deletion through the same pointer, which raises a panic (USER 44).

This is particularly true for CBase-derived classes which should be constructed through the NewL() and NewLC() functions, which are guaranteed to initialize the object correctly using two-phase construction (as described in Chapter 4). They should not be copy constructed or assigned to, because this bypasses the two-phase construction and makes shallow copies of any pointers to dynamic memory.

To prevent accidental copies, CBase declares a private copy constructor and assignment operator (as you’ll see from the class definition in e32base.h). Declaring, but not defining, a private copy constructor and assignment operator prevents calling code from performing invalid copy operations using compiler-generated code. If your C class does need a copy constructor, you must explicitly declare and define one publicly – or provide a CloneL() or CopyL() method, which allows you to make leaving calls, which are not possible in a copy constructor.