Chapter 4. Thinking in Objects
A funny thing happened on the way to the future. Computers became much more powerful, and developers decided to take advantage of this potential by making software that did a lot more than just process one instruction at a time on the command line.
They built more sophisticated interfaces that could do a lot of things at once. This allowed software to tackle complex tasks like nonlinear video editing and 3D modeling. But creating programs with such sophisticated user interfaces required a lot of code.
Expert engineers love simplifying code. They’re obsessed with it. Good programmers think of ways to write code more quickly, but great engineers think of ways to write less code to do the same job. There’s a good reason for this. The less code you have, the easier it is to understand and improve.
The functions in earlier chapters do a calculation and return a result. You store the result in a variable, and then pass it into another function, like this:
#include <stdio.h> #include "types.h" main () { char* itemType = "Document"; int itemCount = countForItemType ( itemType ); int itemTotal = totalForCountOfType ( itemCount, itemType ); char* totalString = formattedTotalForType ( itemTotal, itemType ); printf ("The total for %s is %s", itemType, totalString); free ( totalString ); }
C is a procedural language, which means that functions and the data are separate, and the code is fairly linear. You can also say that C is a static language. This means that all functions have to exist before you call them, and when you call a function, it will run. There’s no reinterpretation as the program is running. Also, all variables have to be a specific type, which is called static typing.
The upside of this is that the code is predictable and you have complete control. The downside is that you have manage all of the details manually, which means you end up doing a lot of busywork. This is hard to picture when working with very small programs, but it’s very clear when working with programs that are made up of hundreds of thousands or millions of lines of code.
Tip
I think of this as a CEO of a 2,000-person company answering support calls. He may be good at it, but it’s probably better for everyone if he runs the company instead. You are the CEO of your application. If you spend time on low-level details, you may never be able to take on the bigger tasks. Instead, you delegate the lower-level details to Cocoa.
So engineers started trying to think up better ways to program, and two ideas that took hold are object-oriented programming and dynamic languages. Standard C had the seeds of object-oriented concepts with structs. Each struct definition is a template for a certain kind of thing, like a movie or a document. You create a variable of that struct type, and you have another instance of the struct with its own set of values. From that single struct, you can store data for any number of different movies.
By convention, developers started writing functions that used structs
instead of individual char
and int
variables. You did this with the createSong()
and
displaySong()
functions in the previous chapter.
Objective-C took this convention and formalized it.
Objective-C also added dynamic features, which means that not everything in your code has to be defined when you compile. You can declare “typeless” variables and can use classes and methods that don’t exist yet. This allows the design of the application to be more flexible, making it much easier to implement things like plug-in support. A lot of the thinking behind Cocoa is based on the dynamic features of Objective-C.
Structs and Classes
Because you already know how C works from the earlier chapters, the easiest way to introduce object-oriented concepts is to compare them to structs and functions.
I’m not going into detail about the Objective-C language in this
chapter. I’m just using it as a way to show you object-oriented concepts.
This means you will see parts that don’t make sense yet, such as the
@
symbol and square brackets. I’ll cover these and the
rest of the language in detail in the following chapters. For now, just
focus on the concepts.
The Song.h file from Chapter 3 looks like this:
typedef struct { char* title; int lengthInSeconds; int yearRecorded; } Song; Song createSong ( char* title, int length, int year ); void displaySong ( Song theSong );
In Objective-C, the equivalent Song.h file looks like this:
@interface
Song : NSObject@property
char* title;@property
int lengthInSeconds;@property
int yearRecorded; + (Song *) song; - (void) display; @end
This is called a class, and you may notice that it looks a lot like a struct. This class interface is the part of the class that goes in the header file. It’s a lot like declaring structs and functions in a header file. The class implementation goes in a separate file.
This line declares the class name and defines which class it inherits from (see Inheritance).
Instance variables or properties are very similar to fields in a struct. I’ll describe properties in more detail in the following chapters; for now, just know that the intent of struct fields and class properties is the same: to store a unique set of values for each instance.
Methods are similar to functions, though methods are not freestanding. Each method belongs to a class. That means that two classes can have
display
methods that do completely separate things. Methods have direct access to the class’s properties, so you don’t need to pass any of those values in when calling a method.
I used a char*
C string in this example because
you’re already familiar with it, but you won’t see this in the source code
for most Cocoa applications. You can use C strings in
Objective-C, but Cocoa has its own type for strings, which I’ll describe
in the following chapters. Other than this one detail, though, this is
very much like a class you’d find in any Cocoa application.
Each instance of a class is called an object.
Even though you have only one Song
class (the
blueprint), you can have many Song
objects. Here’s how you make a
Song
struct in C code, compared to making a
Song
object in Objective-C
code:
// creating a Song struct in C.Song mySong;
mySong.title = "Hey Jude"; mySong.lengthInSeconds = 425; mySong.yearRecorded = 1968;displaySong ( mySong );
// creating a Song object in Objective-C.Song* mySong = [Song song];
mySong.title = "Hey Jude"; mySong.lengthInSeconds = 425; mySong.yearRecorded = 1968;[mySong display];
Tip
The rules about including Objective-C class headers are the same as including C headers with struct definitions. If you refer to a class literally in code, you have to include its header file. If you don’t include the file, you will see a build error.
The Song
object variable is a pointer. All object
variables in Objective-C are pointers because they’re always stored in
dynamic memory—just as if you had allocated them with the
malloc()
function. In fact, deep down in Objective-C,
that’s exactly what’s happening.
Designing Classes
At first, classes look like a way to group functions and variables
together. That’s technically true, but not really
the main point. The idea is that if you want to describe something like
a photo, it’s easier to create a Photo
class with all
of the methods and properties related to that concept rather than a
series of structs and functions that simulate a
photo class.
When I first create a class, I don’t think about which methods
(functions) it will have. Instead, I focus on the kind of data it needs
to store. So if I’m designing a Game
class, I know it
will need at least two player names, two scores, and a game duration. So
I just start with that:
@interface Game : NSObject @property char* playerOne; @property char* playerTwo; @property int playerOneScore; @property int playerTwoScore; @property int durationInSeconds; @end
A class isn’t just a big box to put a bunch of things in. Novice programmers may be tempted to do something like this, because it seems convenient to put everything in one class:
@interface Game : NSObject @property char* playerOne; @property char* playerTwo; @property int playerOneScore; @property int playerTwoScore; @property int durationInSeconds;// these are not part of a 'Game' so they're not appropriate for this class.
@property char* computerName;
@property int cpuCount;
@property char* userName;
@property char* userPassword;
@end
The problem with this is that the computer name, CPU count, user
name, and user password have nothing to do with the
Game
class. Even if you use them in the same
application, they don’t belong in the same
class. Instead, create a separate class for each
distinct kind of data:
@interfaceGame
: NSObject @property char* playerOne; @property char* playerTwo; @property int playerOneScore; @property int playerTwoScore; @property int durationInSeconds; @end @interfaceComputer
: NSObject @property char* name; @property int cpuCount; @end @interfaceUser
: NSObject @property char* name; @property char* password; @end
If each kind of data is separate from the others, the overall
design of your application is much more flexible. You can change how the
Computer
class works without affecting the
Game
class or the User
class. This
is very important when creating Mac and iPhone apps.
Accessors
In the previous chapter, I described how encapsulation is a way to
separate the what from the how.
In one example, I showed you how adding a printf()
call
inside the createSong()
function
allowed you to automatically display the Song
instance
when it’s created.
Accessors are “gatekeepers” for properties. If you use accessors,
the details of how the data is stored is separate
from what the data is. For example, here is a pair of
accessor methods for a title
property of the
Photo
class:
- setTitle: newTitle { title = newTitle; } - title { return title; }
The setTitle:
accessor method takes an input
value and assigns it to the instance variable (the actual variable that
stores the property data). This is called a setter
accessor method. The title
method is the
getter accessor method, and it simply returns the
instance variable’s value.
Tip
In other object-oriented languages, it’s more common to have a
getValue
method
and a setValue
method, but Objective-C methods rarely
have get
as a prefix. It’s used only when returning a
value through indirection.
Now imagine after you wrote this code, you decided to store all your data in a database. Instead of going and changing every part of the program that sets or gets a photo’s title, you can just change the accessors in one place:
- setTitle: newTitle { saveTitleValueToDatabase(newTitle); } - title { return titleValueFromDatabase(); }
Software changes a lot. One of the best things you can do as a programmer is make it easy to change the code. Even though you start out with an idea of how your program will work, you will probably change your mind later. It’s often hard to “see” the whole thing in your head before you start (though you’ll get better at this).
Good code is written with a pencil: easily changed. Bad code is written in ink: difficult to change. If you need to change good code, you can just erase a few things and continue on. If you need to change bad code, you’re often better off just throwing it out and starting with a blank slate.
Inheritance
One of the key concepts in object-oriented programming is
inheritance, which allows you to pull in properties
and methods from a “parent” class, also known as a superclass. For example, if you
have both a Song
class and a Movie
class in your application, it might make sense to create a
Media
superclass that both Song
and
Movie
inherit from, because they’re both logically an
extension of a more generic “media” type. They can then share instance
variables and methods:
@interface Media : NSObject @property int duration; @property char* format; - (void) play; - (void) pause; - (void) rewind; @end
All of these things make sense for both Song
and
Movie
objects, but each class can also add its own
functionality. For example, Movie
might add properties
like aspectRatio
and
framesPerSecond
, as well as a method called
enableSubtitles
. The new class, including inherited
methods, would look like this:
@interface Movie :Media
@property float aspectRatio; @property int framesPerSecond; - (void) enableSubtitles;// inherited from Media.
@property int duration;
@property char* format;
- (void) play;
- (void) pause;
- (void) rewind;
@end
Tip
You usually don’t redeclare methods or properties that you inherit from a superclass. I added the declarations here for clarity.
In this example, Movie
is a
subclass of Media
. Another class
could be a subclass of Movie
, bringing in everything
from both the Movie
class and the
Media
class. The whole tree is called a
class hierarchy. Most frameworks have a “root”
object that all other classes inherit from. Cocoa actually has two root
classes: NSObject
and NSProxy
, but
almost all of your classes will inherit from
NSObject
.
Tip
Some object-oriented languages have multiple inheritance, which allows you to inherit from more than one class. Not everyone agrees on whether this a good or bad thing, though, and the creators of Objective-C decided not to include it in the language.
In addition to adding methods, subclasses can
also override methods from a superclass. It’s
common to override methods when you want to customize existing behavior.
For example, if you want to use a standard Mac OS X button in your
application, but would like to change the clicking behavior, you could
create a subclass of NSButton
and override the methods
related to clicking.
You don’t use superclasses just to share code, though. A subclass
should only inherit from a class that it’s a logical extension of. For
example, you shouldn’t create a subclass of Media
called Camera
, because a camera is not a kind of media.
Instead, you could make a generic Device
class, and
create subclasses for Camera
, Phone
,
and so on.
Composition
Classes can have properties that refer to instances of other classes. This is called composition, because you’re combining different kinds of objects together to accomplish a task:
@interface User : NSObject @property char* name; @property char* password; @end @interfaceGame
: NSObject @propertyUser* playerOne;
@propertyUser* playerTwo;
@property int playerOneScore; @property int playerTwoScore; @property int durationInSeconds; @end
When you want to use methods and properties from another class, but it logically doesn’t make sense to inherit from that class (a camera isn’t a kind of media), you can create a property for the other kind of object instead:
@interface Device : NSObject
@property char* name;
@property int minFocalDistance;
@end
@interface Media : NSObject
@property int duration;
@property char* format;
@property Device* recordingDevice;
@end
This way, a Media
object can
use a Device
object, but it
doesn’t have to be a Device
object, which makes much more sense. Cocoa encourages you to use
composition much more frequently than subclassing, because subclassing can
add complexity, which means it’s harder to create great software.
Object Lifetime
The last building block concept is object lifetimes. Just like in C, you need to clean up the memory of objects when you’re no longer using them. The actual process of reserving and freeing memory in Objective-C is easier than in C, but the trade-off is that there are usually more cleanup tasks.
In Objective-C and most other object-oriented languages, a class
almost always has an initialization method. If you
don’t add one to your class, you typically inherit it from the root
object. You create an initialization method when you want to set up
default values for things. For example, you might want to set the initial
title
value for a Photo
object to
“Untitled Photo” so it’s not just blank:
- init { title = "Untitled Photo"; }
The counterpart to the initialization method is a “cleanup” method, which you create if you want to clean up memory for any data you have, or maybe display a message in the console that the object is shutting down:
- dealloc { printf ("Deallocating Photo\n"); }
There are some more advanced techniques in Cocoa that we will use the cleanup methods for, but you need to learn about Objective-C before we can go into that in detail.
Built-in Classes
Most of Cocoa is available to you as classes. Cocoa has hundreds of different classes that do things like display web pages, record video, create PDFs, and search for files on the user’s computer. The process of becoming a better Cocoa programmer is really about learning how these classes work. There are some C functions and structs, too, but Cocoa is designed as an object-oriented framework.
For example, I can make a window object using the
NSWindow
class (I’ll tell you more about windows in
Chapter 8). In three lines of code, I can create a
window object, set its size and position, and display it on the
screen:
id myWindow = [[NSWindow alloc] init]; [myWindow setFrame:NSMakeRect(100, 100, 400, 300) display:YES]; [myWindow orderFront:nil];
I can also set properties on the window, like the title:
myWindow.title = @"An Empty Space";
Once I’ve done that, I can see the window on the screen (Figure 4-1).
A lot of the code you write in Objective-C is very similar to this. You create an object, set some properties on it, and then the user can start interacting with the object on screen. I’ll introduce you to many different Cocoa classes as you continue through the book.
Get Cocoa and Objective-C: Up and Running now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.