|
Page 5 of 8 Choosing composition vs. inheritance
Both composition and inheritance place subobjects inside your new class. Both use the constructor initializer list to construct these subobjects. You may now be wondering what the difference is between the two, and when to choose one over the other. Composition is generally used when you want the features of an existing class inside your new class, but not its interface. That is, you embed an object to implement features of your new class, but the user of your new class sees the interface you’ve defined rather than the interface from the original class. To do this, you follow the typical path of embedding private objects of existing classes inside your new class. Occasionally, however, it makes sense to allow the class user to directly access the composition of your new class, that is, to make the member objects public. The member objects use access control themselves, so this is a safe thing to do and when the user knows you’re assembling a bunch of parts, it makes the interface easier to understand. A Car class is a good example: //: C14:Car.cpp // Public composition
class Engine { public: void start() const {} void rev() const {} void stop() const {} };
class Wheel { public: void inflate(int psi) const {} };
class Window { public: void rollup() const {} void rolldown() const {} };
class Door { public: Window window; void open() const {} void close() const {} };
class Car { public: Engine engine; Wheel wheel[4]; Door left, right; // 2-door };
int main() { Car car; car.left.window.rollup(); car.wheel[0].inflate(72); } ///:~ Because the composition of a Car is part of the analysis of the problem (and not simply part of the underlying design), making the members public assists the client programmer’s understanding of how to use the class and requires less code complexity for the creator of the class. With a little thought, you’ll also see that it would make no sense to compose a Car using a “vehicle” object – a car doesn’t contain a vehicle, it is a vehicle. The is-a relationship is expressed with inheritance, and the has-a relationship is expressed with composition. Subtyping
Now suppose you want to create a type of ifstream object that not only opens a file but also keeps track of the name of the file. You can use composition and embed both an ifstream and a string into the new class: //: C14:FName1.cpp // An fstream with a file name #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std;
class FName1 { ifstream file; string fileName; bool named; public: FName1() : named(false) {} FName1(const string& fname) : fileName(fname), file(fname.c_str()) { assure(file, fileName); named = true; } string name() const { return fileName; } void name(const string& newName) { if(named) return; // Don't overwrite fileName = newName; named = true; } operator ifstream&() { return file; } };
int main() { FName1 file("FName1.cpp"); cout << file.name() << endl; // Error: close() not a member: //! file.close(); } ///:~ There’s a problem here, however. An attempt is made to allow the use of the FName1 object anywhere an ifstream object is used by including an automatic type conversion operator from FName1 to an ifstream&. But in main, the line file.close(); will not compile because automatic type conversion happens only in function calls, not during member selection. So this approach won’t work. A second approach is to add the definition of close( ) to FName1: void close() { file.close(); } This will work if there are only a few functions you want to bring through from the ifstream class. In that case you’re only using part of the class, and composition is appropriate. But what if you want everything in the class to come through? This is called subtyping because you’re making a new type from an existing type, and you want your new type to have exactly the same interface as the existing type (plus any other member functions you want to add), so you can use it everywhere you’d use the existing type. This is where inheritance is essential. You can see that subtyping solves the problem in the preceding example perfectly: //: C14:FName2.cpp // Subtyping solves the problem #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std;
class FName2 : public ifstream { string fileName; bool named; public: FName2() : named(false) {} FName2(const string& fname) : ifstream(fname.c_str()), fileName(fname) { assure(*this, fileName); named = true; } string name() const { return fileName; } void name(const string& newName) { if(named) return; // Don't overwrite fileName = newName; named = true; } };
int main() { FName2 file("FName2.cpp"); assure(file, "FName2.cpp"); cout << "name: " << file.name() << endl; string s; getline(file, s); // These work too! file.seekg(-200, ios::end); file.close(); } ///:~ Now any member function available for an ifstream object is available for an FName2 object. You can also see that non-member functions like getline( ) that expect an ifstream can also work with an FName2. That’s because an FName2 is a type of ifstream; it doesn’t simply contain one. This is a very important issue that will be explored at the end of this chapter and in the next one. private inheritance
You can inherit a base class privately by leaving off the public in the base-class list, or by explicitly saying private (probably a better policy because it is clear to the user that you mean it). When you inherit privately, you’re “implementing in terms of;” that is, you’re creating a new class that has all of the data and functionality of the base class, but that functionality is hidden, so it’s only part of the underlying implementation. The class user has no access to the underlying functionality, and an object cannot be treated as a instance of the base class (as it was in FName2.cpp). You may wonder what the purpose of private inheritance is, because the alternative of using composition to create a private object in the new class seems more appropriate. private inheritance is included in the language for completeness, but if for no other reason than to reduce confusion, you’ll usually want to use composition rather than private inheritance. However, there may occasionally be situations where you want to produce part of the same interface as the base class and disallow the treatment of the object as if it were a base-class object. private inheritance provides this ability. Publicizing privately inherited members When you inherit privately, all the public members of the base class become private. If you want any of them to be visible, just say their names (no arguments or return values) along with the using keyword in the public section of the derived class: //: C14:PrivateInheritance.cpp class Pet { public: char eat() const { return 'a'; } int speak() const { return 2; } float sleep() const { return 3.0; } float sleep(int) const { return 4.0; } };
class Goldfish : Pet { // Private inheritance public: using Pet::eat; // Name publicizes member using Pet::sleep; // Both overloaded members exposed };
int main() { Goldfish bob; bob.eat(); bob.sleep(); bob.sleep(1); //! bob.speak();// Error: private member function } ///:~ Thus, private inheritance is useful if you want to hide part of the functionality of the base class. Notice that giving exposing the name of an overloaded function exposes all the versions of the overloaded function in the base class. You should think carefully before using private inheritance instead of composition; private inheritance has particular complications when combined with runtime type identification (this is the topic of a chapter in Volume 2 of this book, downloadable from www.BruceEckel.com).
|