CSCE 121: Introduction to Program Design and Concepts
Lab Exercise Twelve
Objective
This lab involves object-oriented programming: you'll need to make your own
classes and instantiate objects to represent data. Because objects have methods
(functions) as well as data fields (variables), we sometimes say they possess
behavior. This lab will give you some practice with those things and
also give you some feeling for two new things: the C++ vector
class as well as overloading of operators. (We'll not cover many of the details
of these two, but you should have some exposure to them because they'll
be particularly important in subsequent Computer Science courses.)
But you should definitely focus on three key aspects as they're important fundamentals underlying object-oriented programming in general. They'll be covered in practical form in the lab, but are the following:
- Encapsulation— Classes define the data and functionality associated with a certain entity. They should be as cohesive as possible, usually capturing one idea.
- Inheritance— Usually one tries to tease apart aspects of the problem being solved. Common traits should be factored out and inheritance enables us to implement repeated functionality without repeating (much) code.
- Polymorphism— When referring to objects, it is possible treat elements in a common way even if the elements being referred to are not identical. (There'll be a bit more explanation for this concept below.)
Approach
For each of the three items above, start by doing the particular practical thing that you're asked to do below. But once you've managed that, try to reflect on the general value of this idea for programs more generally. It may be helpful to think about previous lab questions and to ask yourself: "How would I do that differently if I were to use objects and classes?"
A Custom Media Manager
Recently you've been frustrated by your phone's poor shuffling when listening to music. You decide to write your own application to manage your playlists so that you'll have maximal control over your media.
After thinking about things, you decide to organize your project into multiple classes. You settle on the classes shown in the diagram below. (General hint: nouns are good candidates for classes?) Some classes inherit properties from others, forming a hierarchy, and arrows in the figure show this.
For each of these classes it is best to have separate header and source files. Here's a summary of the design:
-
You have a class to encapsulate a playlist, called
Playlist
. It will have a way to store multiple entries, for which you define a class calledPlaylistEntry
, used to describe audio files and video media. - You have
AudioEntry
andVideoEntry
classes for those two broad types of media. For audio, you might be interested in whether it is music or voice; for video you might be more interested in resolution, or similar aspects. These classes allow you to draw out those aspects. - Next, you look at the files on your computer and decide to focus on your
.mp3
and.wav
files as a starting point.
The elements shown in blue are provided for you as starting code; you can grab
it all here.
There is also a file called mediajuke.cpp
which contains
the main program.
Starting out: AudioEntry
, Mp3Entry
, and WavEntry
The first steps are to look at the code provided, compile it, and to see what it does. You'll need to provide it an example playlist. Here are two: 1 and 2. You need to pass the filename to the executable as a command line argument.
You'll see that there are blocks of code surrounded by preprocessor code. Something like this:
#ifdef DEBUG
cout << "A Message" << endl;
#endif
By default, the code to print this message will be skipped by the compiler. But if you pass
g++
the -DDEBUG
flag, then all that code will become
active. It should give you lots of output, and help you to see what is going on.
Once you're able to run the program, trace through it to see what is defined where.
With multiple files and inheritance, it can take a bit of exploring to find
out exactly which class implements the bits of functionality you're seeing.
You should be able to work out how it operates. The Playlist
constructor
takes in the filename, opens the file, and reads it a line at a time.
It splits each line on the basis of the character that is the first space.
The first bit is treated as a code to tell you what sort of media
file is being used; the rest of the line is passed to the appropriate
constructor for it to initialize the object.
Step 1: Using the other two audio formats as a guide, define and implement a class
for .ogg
files.
|
|
The Playlist
class in a bit more detail
The header file describing the Playlist
class uses some
pieces of syntax we've not seen before. These are both handy features,
so let's look at the each in turn.
The vector container:
Notice that there is a new header called vector
which has been
included. Since it gets a bit tedious to have to make (and manage) linked-lists
for all the resizable data we have, C++ provides support for various types of collections.
The vector
is perhaps the simplest and most useful.
Here is a simple example showing how we can
make and use vectors to store data of different types.
The less than and greater than signs act as brackets and specify
what sorts of elements go into the container. So a
vector<int>
is a list of int
s, while a
vector<string>
is a list of string
s.
(Because we specify some
other type to give the full details of the vector itself, it is an example
of generic programming.)
Within Playlist
, the C++ vector
is used to define a
private field called the_entries
. It is a list of
pointers to items in the playlist, i.e., (PlaylistEntry *)
elements.
The syntax is a bit more involved, but it is directly analogous.
Play around a bit with the vector<int>
example to get comfortable with it.
You can see that it is used in a very straightforward fashion for the
code here.
An <<
operator of our own:
If you look in the main
function in mediajuke.cpp
, you'll see that the whole
playlist is printed by doing this:
cout << pl << endl;
Of course, since we've defined the Playlist
type ourselves, if
we want to make our class behave like any builtin type in C++, we need to
describe what must be done to print our class. The function
ostream& operator<<(ostream& os, const Playlist& pl)
will do this for us. (It may not look like a function, at first sight,
but the keyword operator
says that the next characters are actually
an exotic name for a function.) The function takes a reference
to an ostream
—that is an output stream—
and a reference to our type, Playlist
, and will return a new
ostream
reference. Strange syntax aside, you can see that the
implementation of this function is pretty straightforward.
Incidently, it is defined on an ostream
because that
<<
we defined for Playlist
s will
allow us to write to files as well. How can that be? It
turns out that the streams are classes themselves. They're also arranged in a
hierarchy, so if we implement functionality for an ostream
, then
the reuse that the object-oriented approach enables, allows
cout
and other streams, which inherit from ostream
, to have that functionality too. For free.
One more thing that needs explanation is that friend
keyword.
The <<
operator is just an exotically named function.
It is not a member of the class. Notice that the definition doesn't say
Playlist::operator<<…
. Because of this, the function
would not be able to read the private data in the class. That makes it hard to
print the particular playlist. Instead the
friend
keyword says that this function has special access to the
data in the class. That allows it to behave as if it were a member function.
Polymorphism: Object-oriented languages that support inheritance allow
us to make subtypes. For example, we say that the WavEntry
is a subtype of
AudioEntry
as the former inherits properties form the latter.
In the Playlist
class, we have a list (implemented as a vector
) of
PlaylistEntry
s. There actually aren't any objects that are just
instances of PlaylistEntry
, but we can treat the other subtypes
as if they were nothing other than PlaylistEntry
s. We treat them
in a unified way, though they can actually be quite different underneath.
This is yet another example of information hiding as a design principle.
The PlaylistEntry
class is an abstract class
In the preceding, I pointed out that there aren't any objects that are
just instances of PlaylistEntry
. They're always some element
further down the class hierarchy. If you look at
PlaylistEntry
in detail, you'll see that it has no implementation
of the play
method. The "= 0;
" in the declaration
tells the compiler that there will not be any implementation.
Classes that do not provide an implementation of some methods are called abstract classes; they are intended to be extended by subclasses which will fill in the requisite detail. You'll quickly discover that a class is abstract if you try to create an instance of it; the C++ compiler will raise an error. To try it for yourself, do the following:
PlaylistEntry *tester = new PlaylistEntry("foo");
Are there other abstract classes in the source code you were provided?
Implementing some extra functionality
Step 2: A useful bit of practice is to design and implement
classes of movies. The VideoEntry
class should
contain aspects specific to video clips, such as their
resolution (e.g., 640
× 480 ) and color depth (e.g.,
8- or
16-bit color).
The AviEntry
and FlvEntry
subclasses could have
aspects particular to those formats; such as version numbers, etc.
If things are properly designed you should only need to add a
few lines to playlist.cpp
to have it recognize this
new format and call the appropriate code in the virtual methods you've
defined. Thus, we notice that Playlist
is decoupled
from the details of the particular formats.
Step 3: Implement a method double Playlist::total_duration()
that computes the time needed to play the whole playlist.
Add the following code to your main function, after the playlist has been
printed out:
cout << "Total play time = " << pl.total_duration() << "s" << endl;
A Playlist
as an PlaylistItem
Now suppose that you decide that you want to allow playlists that contain
other playlists. The idea of recursion in this way seems attractive and
interesting. This can be achieved by actually making
Playlist
a subclass of PlaylistItem
. The diagram on
the right shows the updated hierarchy.
Here's an example of a playlist that includes another playlist as an entry in the playlist.
Step 4: Add code to make Playlist
a subclass of
PlaylistItem
. (You'll need to make the appropriate call
in the constructor and implement a play()
function).
Compare the output of your program with mine for the playlist above.