The really stupid thing, by the way, about most code that I review is how few people know about object oriented development. Yes, yes, yes; they say they know all about object oriented development–but when you then review their code (say, in an iOS application with a table) do they practice proper encapsulation? Nooooooooooo…
All too often I see something like this:
MyTableViewCell.h
@interface MyTableViewCell: UITableViewCell @property (strong) IBOutlet UILabel *leftLabel; @property (strong) IBOutlet UILabel *rightLabel; @end
MyTableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyTableViewCell *c = (MyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"MyTableViewCell" forIndexPath:indexPath]; c.leftLabel.text = [NSString stringWithFormat:@"Left %d",indexPath.row]; c.rightLabel.text = @"Right Label"; return c; }
The text in red above: no, no, no, no, no.
Haven’t you even heard of encapsulation? No? Well, that’s probably because (a) most developers have no idea what they’re doing beyond copying someone else’s models, and (b) they’ve been taught some bad habits by other developers who also have no idea what they’re doing.
Of course this isn’t helped by Apple, whose own UITableViewCell by default exposes the fields contained within.
What is object encapsulation?
The idea of object encapsulation is central to the idea of object oriented programming. Essentially it refers to the idea of creating “objects”–chunks of data associated with functions designed to work on that data.
To understand how useful this is we need to dive into a pre-OOP language, such as C.
Back in the “bad old days” of C, you had functions, and you had structures. And that was it:
struct MyRecord { int a; int b; }; int AddFields(struct MyRecord x) { return x.a + x.b; }
While this sort of procedural programming has it’s place–and languages such as C are extremely good at embedded development or in Kernel programming (where execution efficiency is important), it falls short with developing user interfaces, simply because with user interfaces we manipulate things like buttons and text fields and table view cells.
In fact, it turns out that object-oriented programming is tied to user interface development, by abstracting the idea of user interface elements into a new concept of an “object” as a self-contained unit that combines the idea of a structure or record with the idea of functions or procedures that operate on that record.
In C++, we can express this idea as a class:
class MyRecord { int a; int b; int AddFields(); }; int MyRecord::AddFields() { return a + b; }
Notice that this expresses the same idea as our C snippet above, which adds the contents of the two fields in MyRecord. Except now, AddFields is associated with a record. This means if we have a record and we want the sum of the fields, instead of writing
MyRecord a; int sum = AddFields(a);
we write
MyRecord a; int sum = a.AddFields();
That is, we apply the message against the object.
Now we haven’t really done anything new yet. In fact, if you were to write in C++ the method ‘AddFields()’ from our C example, it would still work with our C++ declaration of MyRecord.
But C++ gives us a new tool: a way of marking fields “private”–that is, only accessible from the methods that are associated with the class.
Thus:
class MyRecord { private: int a; int b; public: int AddFields(); };
We’ve hidden a and b from view. Now the only way you can change a and b or get their values is through methods which are then made public with MyRecord.
Encapsulation is the process of creating self-contained objects: objects which provide a clear interface for manipulating the object, but which hide the details as to how the object does it’s work.
Now there was no need to actually use the new features of C++ to provide this sort of data hiding. In C, we can take advantage of the fact that things declared within a single C file stay within that file: we could declare a pointer to a structure in our header file, but hide the details by declaring them in the C file that contains the implementation. C++ makes this easier for us by giving us better tools to manipulate access to the contents of the object.
Why is encapsulation important?
Simply put, encapsulation allows us to separate the “what” from the “how”; separate what we want an object to do from how the object actually does the work.
This becomes important for two reasons.
First, it means that we have an object which has a clearly defined “purpose.” For example we can define an object which represents a button on the screen: a rectangular region the user can tap on or click with their mouse, which then responds to that tap by visually changing appearance and by firing an event which represents the response to that tap or button press.
And second, tied to the first, we can isolate all of the code which handles the button’s behavior within the button itself. A user of the button doesn’t need to know the details of how a button works to put one on the screen, nor does the user need to know how a button receives click or tap events, or how it processes those events. A user doesn’t even need to know the details of how a button draws itself: they don’t need to know how the button handles details such as switching text alignment for languages which read right to left instead of left to right.
And because the details are isolated away, it means those details can change: instead of firing an event on the down click of the mouse the event can be fired when the mouse click is released. The button’s appearance can change–or even be changeable depending on the skin the user selects. The button can even be handled as a spectrum of button-like objects. None of this matter to the user of that button: drop one on the screen, set the text, and wire up the event for the response, and you’re done.
How we can change our object above to respect proper encapsulation
This idea of encapsulation is one that we can–and should follow in our own code. That way if we later have to change the details of how we implement a thing, it doesn’t require us to hunt through all of our code and change the details everywhere else. Change the object, not all the callers manipulating the object.
So, for our UITableViewCell example above, the change is simple. First, hide the details how our table view cell is implemented:
MyTableViewCell.h
@interface MyTableViewCell: UITableViewCell - (void)setLeftText:(NSString *)left rightText:(NSString *)right; @end
MyTableViewCell.m
@implementation MyTableViewCell @property (strong) IBOutlet UILabel *leftLabel; @property (strong) IBOutlet UILabel *rightLabel; - (void)setLeftText:(NSString *)left rightText:(NSString *)right { self.leftLabel.text = left; self.rightLabel.text = right; } @end
And in our caller code:
MyTableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyTableViewCell *c = (MyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"MyTableViewCell" forIndexPath:indexPath];c.leftLabel.text = [NSString stringWithFormat:@"Left %d",indexPath.row];c.rightLabel.text = @"Right Label";[c setLeftText:[NSString stringWithFormat:@"Left %d",indexPath.row] rightText:@"Right Label"]; return c; }
The original offending code was wrong because it confused the “how” to do something (setting the internal structure of the table cell) with the “what”: set the left and right text.
And notice that now we’ve hidden the details inside the table view. So later, if for some reason we change our implementation of the UITableViewCell:
MyTableViewCell.m
@implementation MyTableViewCell @property (copy) NSString *leftLabel; @property (copy) NSString UILabel *rightLabel; - (void)setLeftText:(NSString *)left rightText:(NSString *)right { self.leftLabel = left; self.rightLabel = right; [self setNeedsDisplay]; } - (void)drawRect:(CGRect)r { CGRect textRect = CGRectMake(10, 0, 146, 44); { NSString* textContent = self.leftLabel; UIFont* textFont = [UIFont fontWithName: @"HelveticaNeue-Light" size: UIFont.labelFontSize]; [UIColor.blackColor setFill]; [textContent drawInRect: CGRectOffset(textRect, 0, (CGRectGetHeight(textRect) - [textContent sizeWithFont: textFont constrainedToSize: textRect.size lineBreakMode: UILineBreakModeWordWrap].height) / 2) withFont: textFont lineBreakMode: UILineBreakModeWordWrap alignment: UITextAlignmentLeft]; } CGRect text2Rect = CGRectMake(164, 0, 146, 44); { NSString* textContent = self.rightLabel; UIFont* text2Font = [UIFont fontWithName: @"HelveticaNeue-Light" size: UIFont.labelFontSize]; [UIColor.blueColor setFill]; [textContent drawInRect: CGRectOffset(text2Rect, 0, (CGRectGetHeight(text2Rect) - [textContent sizeWithFont: text2Font constrainedToSize: text2Rect.size lineBreakMode: UILineBreakModeWordWrap].height) / 2) withFont: text2Font lineBreakMode: UILineBreakModeWordWrap alignment: UITextAlignmentRight]; } } @end
Notice that we don’t have to change a single thing in MyTableViewController.m, simply because it never knew how the table view drew itself; it only knew how to ask.