complex:
Composition in programming is just like composition in music: combining multiple components together and using them together to get a complete work.
In Objective-C, composition is achieved by including an object pointer as an instance variable.
In software development, programmers may use a Pedal object and a Tire object to create a virtual Unicycle.
A virtual unicycle should have a pointer to a Pedal object and a pointer to a Tire object. Code is as follows:
@interface Unicycle: NSObject
{
Pedal * pedal;
Tire * tire;
}
@end // Unicycle
Pedal and Tire formed Unicycle in a composite manner.
Car program
Code:
#import <Foundation / Foundation.h>
@interface Tire: NSObject
@end // Tire
@implementation Tire
-(NSString *) description {
return (@ "I am a tire. I last a while.");
} // description
@end // Tire
If you do not include any instance variables in the class, you can omit the curly braces in the interface definition.
Custom NSLog ()
NSLog () can output objects using the% @ format specifier. When NSLog () processes the% @ specifier, it will query the corresponding object in the parameter list for a description of this object. Technically, that is, NSLog () sends a description message to this object, and then the object's description method generates an NSString and returns it. NSLog () will include this string in the output. Provide a description method in the class to customize how NSLog () will output the object.
In Cocoa, the NSArray class manages a collection of objects, and its description method provides information about the array itself.
Engine Code
@interface Engine: NSObject
@end // Engine
@implementation Engine
-(NSString *) description
{
return (@ "I am an engine. Vrooom!");
} // description
@end // Engine
Car Code
@interface Car: NSObject
{
Engine * engine;
Tire * tires [4];
}
-(void) print;
@end // Car
Because engine and tires are Car instance variables, they are composite. Each Car object will allocate pointers to the engine and tires. But what is really contained in Car is not the engine and tires variables, just the reference pointers of other objects that exist in memory. When allocating memory for the newly created Car object, these pointers will be initialized to nil (zero value), that is, the car now has no engine and no tires. It can be imagined as a car frame that is still being assembled on the assembly line.
Car class implementation
The first is an init method that initializes instance variables. This method creates an engine variable and 4 tire variables for assembly for the car. When using new to create a new object, the system actually performs two steps in the background: the first step is to allocate memory for the object, that is, the object obtains a memory block for storing instance variables; the second step is to automatically call the init method, The object becomes available.
@implementation Car
-(id) init
{
if (self = [super init]) {
engine = [Engine new];
tires [0] = [Tire new];
tires [1] = [Tire new];
tires [2] = [Tire new];
tires [3] = [Tire new];
}
return (self);
} // init
-(void) print
{
NSLog (@ "% @", engine);
NSLog (@ "% @", tires [0]);
NSLog (@ "% @", tires [1]);
NSLog (@ "% @", tires [2]);
NSLog (@ "% @", tires [3]);
} // print
@end // Car
Remember,% @ simply calls the description method of each object and displays the results.
main () function:
int main (int argc, const char * argv [])
{
Car * car;
car = [Car new];
[car print];
return (0);
} // main
Access method
An accessor method is a method used to read or change the properties of an object. For example, setFillColor is an access method. Because it assigns values to variables in an object, this type of accessor is called a setter method. (mutator is used to change object state)
Another access method is the getter method. The getter method provides code with a way to access object properties through the object itself.
Add setter and getter methods for Car. Code is as follows:
@interface Car: NSObject
{
Engine * engine;
Tire * tires [4];
}
-(Engine *) engine;
-(void) setEngine: (Engine *) newEngine;
-(Tire *) tireAtIndex: (int) index;
-(void) setTire: (Tire *) tire atIndex: (int) index;
-(void) print;
@end // Car
Access methods always come in pairs, one to set the value of a property (such as setTire), and one to read the value of a property (tireAtIndex);
But sometimes it is reasonable to have only one setter or getter.
Cocoa has its own convention for naming access methods.
The setter method is named after the name of the property it changes, and is prefixed with set. For example: setEngine, setStringValue.
The getter method is named after the property name it returns. So the getter method corresponding to the above setter method should be engine, stringValue. Do not use get as a prefix for getter methods. Such as getStringValue violates the naming convention.
Set the access method of the engine property
Implementation code for two methods
-(Engine *) engine
{
return (engine);
} // engine
-(void) setEngine: (Engine) newEngine
{
engine = newEngine;
} // setEngine
The getter method engine returns the current value of the instance variable engine. Remember that in Objective-C, all objects interact through pointers, so the method engine returns a pointer to the engine object in Car.
Similarly, the setEngine method setEngine: assigns the value of the instance variable engine to the value pointed to by the parameter. What is actually copied is not the engine variable, but the pointer value to the engine. In other words, after calling the setEngine: method in the object Car, there is still only one engine instead of two.
Access method to set the tires property
Code:
-(void) setTire: (Tire *) tire atIndex: (int) index
{
if (index <0 || index> 3) {
NSLog (@ "bad index (% d) in setTire: atIndex:", index);
exit (1);
}
tires [index] = tire;
} // setTire: atIndex:
-(Tire *) tireAtIndex: (int) index
{
if (index <0 || index> 3) {
NSLog (@ "bad index (% d) in tireAtIndex:", index);
exit (1);
}
return (tires [index]);
} // tireAtIndex:
The tire access method uses generic code to check the array index of the tires instance variable to ensure that it is a valid value. This code is called defensive programming.
Tire * tire = [Tire new];
[car setTire: tire atIndex: 2];
NSLog (@ "tire number two is% @", [car tireAtIndex: 2]);
The main function is changed to:
int main (int argc, const char * argv [])
{
Car * car = [Car new];
Engine * engine = [Engine new];
[car setEngine: engine];
for (int i = 0; i <4; i ++) {
Tire * tire = [Tire new];
[car setTire: tire atIndex: i];
}
[car print];
return (0);
} // main
Extended CarParts program
At this point we can add new classes.
Such as: New Engine subclass
Tire class
main method changed to
Here we add two new classes when the car class is not modified.
When to use inheritance, when to use composite
The relationship established between inherited classes is "is a". If you can say "X is a Y", you can use inheritance.
The relationship established between the composite classes is "has a". If you can say "X has a Y," then you can use compound.
When you create a new object, take the time to figure out when you should use inheritance and when you should use composition.
Composition is a fundamental concept of OOP, and this technique is used to create objects that reference other objects.
Source file organization
Split interface and implementation
The source code for Objective-C classes is divided into two parts. One part is the interface, which is used to show the structure of the class. The interface contains all the information needed to use the class. The compiler can only use the objects of the class, call class methods, compose the objects into other classes, and create subclasses after compiling the @interface part.
Another part of the source code is the implementation. The @implementation part tells the Objective-C compiler how to make the class work. This part of the code implements the methods declared by the interface.
So the class code is usually placed in two files. A file stores the code of the interface part: @interface directives for classes, public struct definitions, enum constants, #defines and extern global variables, etc. Because Objective-C inherits the characteristics of C, the above code is usually placed in the header file. The header file name is the same as the class name, except that it is suffixed with .h.
All implementation content (such as the class's @implementation directive, global variable definitions, private structs, etc.) is placed in a file with the same name as the class but with a .m suffix (sometimes called an .m file).
Code:
Tire.h
#import <Foundation / Foundation.h>
@interface Tire: NSObject
@end
Tire.m
#import "Tire.h" // Import the @interface header file and inform the compiler of this information so that proper code can be generated.
@implementation Tire
-(NSString *) description
{
return (@ "I am a tire. I last a while");
} // description
@end // Tire
Use cross file dependencies
Dependency is a relationship between two entities. Dependencies can also exist between two or more files.
Importing a header file is to establish a close dependency between the header file and the source file. If there is any change to the header file, all files that depend on it must be recompiled. If you write 100 .m files and import them into the same file all.h, once you modify all.h, all 100 .m will be regenerated, which is very time consuming.
Not only that, because dependencies are passed, header files can also depend on each other, so the problem of recompilation will be more serious.
Notes for recompilation:
Objective-C provides a way to reduce the negative effects of recompilation caused by dependencies.
The cause of the dependency problem is that the Objective-C compiler needs some information to work.
Objective-C introduces the keyword @class to tell the compiler: this is a class, so it will only be referenced by a pointer. This way the compiler doesn't need to know more about this class, just know that it is referenced by pointers.
In Car.h,
#import <Cocoa / Cocoa.h>
@interface Car: NSObject
-(void) setEngine: (Engine *) newEngine;
-(Engine *) engine;
-(void) setTire: (Tire *) tire atIndex: (int) index;
-(Tire *) tireAtIndex: (int) index;
-(void) print;
If you want to use this header file, an error will be reported. The error message may be: error: expected a type "Tire". We have two methods to solve this error problem. The first is to import Tire.h and Engine.h with #import statements, so that the compiler will get a lot of information about these two classes.
There is a better way. If you look closely at the interface of the Car class, you will find that it only refers to Tire and Engine through pointers. This is what @class can do, here is the content of Car.h file with @class code added.
#import <Cocoa / Cocoa.h>
@class Tire;
@class Engine;
@interface Car: NSObject
-(void) setEngine: (Engine *) newEngine;
-(Engine *) engine;
-(void) setTire: (Tire *) tire atIndex: (int) index;
-(Tire *) tireAtIndex: (int) index;
-(void) print;
This is enough to tell the compiler all the information it needs to handle the @interface portion of the Car class.
We also need to import Tire.h and Engine.h in the Car.m file, and also paste the @implementation part into the Car.m file, so that we can run Car.
Import and inherit
We create a class Slant6 in the original Car class file, which inherits the Engine class. Because it inherits from other classes and not through pointers to other classes, you cannot use @class statements in header files. We can only use the #import "Engine.h" statement in Slant6.h file.
So why not use @class statements here? Because the compiler needs to know all the information about the superclass before it can successfully compile the @interface part for its subclasses. It needs to know the configuration information (data type, size, and ordering) of the instance variables in the superclass. When subclasses add instance variables, they are appended to the superclass instance variables. The compiler then uses this information to calculate where in memory it can find these instance variables, and each method looks for it through its own hidden pointer. To be able to accurately calculate the location of instance variables, the compiler must first understand everything about the class.
Then create the files Slant6.h and Slant6.m in Xcode.
Slant6.h
#import "Engine.h"
@interface Slant6: Engine
@end // Slant6
This file is not imported into <Cocoa / Cocoa.h> because it is already imported in Engine.h, so there is no need to import it again, but if you add it, #import will not import the imported file repeatedly
Implemented code:
#import "Slant6.h"
@implementation Slant6
-(NSString *) description
{
return (@ "I am a slant-6. VROOOm!");
} // description
@end // Slant6
Learn more about Xcode
Change company name
Select the project in the Xcode compiler navigator panel and make sure the project name is selected under the Project column in the compiler panel. Select the project in the inspector panel on the right, and modify the Organization text box under the Project Document column.
Xcode's code completion (code completion), the colored box next to the code name indicates the type of this symbol: E means enumeration symbol, f means function, # means #define instruction, m means method, C means class and many more.
Navigation of the code
emace:
debugging:
Brute force test (caveman debugging), write the output statement (NSLog) in the program to output the program's control flow and some data values.
In the wide bar to the left of the focus bar, you can see a blue arrow-shaped object, which is the new breakpoint. You can drag the breakpoint out of the sidebar to delete it.