Streaming is available in most browsers,
and in the WWDC app.
Protocol-Oriented Programming in Swift
At the heart of Swift's design are two incredibly powerful ideas: protocol-oriented programming and first class value semantics. Each of these concepts benefit predictability, performance, and productivity, but together they can change the way we think about programming. Find out how you can apply these ideas to improve the code you write.
Dave Abrahams: Hi, everybody. My name is Dave Abrahams, and I'm the technical lead for the Swift standard library, and it is truly my privilege to be with you here today. It is great to see all of you in this room. The next 40 minutes are about putting aside your usual way of thinking about programming.
What we're going to do together here won't necessarily be easy, but I promise you if you stick with me, that it'll be worth your time. I'm here to talk to you about themes at the heart of Swift's design, and introduce you to a way of programming that has the potential to change everything.
But first, let me introduce you to a friend of mine.
This is Crusty.
Now you've probably all worked with some version of this guy. Crusty is that old-school programmer who doesn't trust debuggers, doesn't use IDEs. No, he favors an 80 x 24 terminal window and plain text, thank you very much.
And he takes a dim view of the latest programming fads. Now I've learned to expect Crusty to be a little bit cynical and grumpy, but even so it sometimes takes me by surprise. Like last month we were talking about app development, and said flat out, 'I don't do object-oriented.' I could hardly believe my ears. I mean, object-oriented programming has been around since the 1970s, so it's not exactly some new-fangled programming fad. And, furthermore, lots of the amazing things that we've all built together, you and I and the engineers on whose shoulders we stand, were built with objects.
'Come on,' I said to him as I walked over to his old-school chalkboard. 'OOP is awesome. Look what you can do with classes.' Yes. So first you can group related data and operations.
And then we can build walls to separate the inside of our code from the outside, and that's what lets us maintain invariants.
Then we use classes to represent relatable ideas, like window or communication channel. They give us a namespace, which helps prevent collisions as our software grows.
They have amazing expressive syntax. So we can write method calls and properties and chain them together. We can make subscripts.
We can even make properties that do computation.
Last, classes are open for extensibility. So if a class author leaves something out that I need, well, I can come along and add it later.
And, furthermore, together, these things, these things let us manage complexity and that's really the main challenge in programming. These properties, they directly address the problems we're trying to solve in software development.
At that point, I had gotten myself pretty inspired, but Crusty just snorted and .
He let all the air out of my balloon.
And if that wasn't bad enough, a moment later he finished the sentence. Because it's true, in Swift, any type you can name is a first class citizen and it's able to take advantage of all these capabilities. So I took a step back and tried to figure out what core capability enables everything we've accomplished with object-oriented programming.
Obviously, it has to come from something that you can only do with classes, like inheritance.
And this got me thinking specifically about how these structures enable both code sharing and fine-grained customization.
So, for example, a superclass can define a substantial method with complex logic, and subclasses get all of the work done by the superclass for free.
They just inherit it.
But the real magic happens when the superclass author breaks out a tiny part of that operation into a separate customization point that the subclass can override, and this customization is overlaid on the inherited implementation. That allows the difficult logic to be reused while enabling open-ended flexibility and specific variations. And now, I was sure, I had him. 'Ha,' I said to Crusty. 'Obviously, now you have to bow down before the power of the class.' 'Hold on just a darn tootin' minute,' he replied. 'First of all, I do customization whatchamacallit all the time with structs, and second, yes, classes are powerful but let's talk about the costs. I have got three major beefs with classes,' said Crusty. And he started in on his list of complaints.
'First, you got your automatic sharing.' Now you all know what this looks like.
A hands B some piece of perfectly sober looking data, and B thinks, 'Great, conversation over.' But now we've got a situation where A and B each have their own very reasonable view of the world that just happens to be wrong.
Because this is the reality: eventually A gets tired of serious data and decides he likes ponies instead, and who doesn't love a good pony? This is totally fine until B digs up this data later, much later, that she got from A and there's been a surprise mutation.
B wants her data, not A's ponies.
Well, Crusty has a whole rant about how this plays out.
'First,' he says, 'you start copying everything like crazy to squash the bugs in your code. But now you're making too many copies, which slows the code down. And then one day you handle something on a dispatch queue and suddenly you've got a race condition because threads are sharing a mutable state, so you start adding locks to protect your invariants. But the locks slow the code down some more and might even lead to deadlock. And all of this is added complexity, whose effects can be summed up in one word, bugs.' But none of this is news to Cocoa programmers. It's not news. We've been applying a combination of language features like @property(copy) and coding conventions over the years to handle this. And we still get bitten.
For example, there's this warning in the Cocoa documentation about modifying a mutable collection while you're iterating through it.
Right? And this is all due to implicit sharing of mutable state, which is inherent to classes. But this doesn't apply to Swift. Why not? It's because Swift collections are all value types, so the one you're iterating and the one you're modifying are distinct.
Okay, number two on Crusty's list, class inheritance is too intrusive. First of all, it's monolithic. You get one and only one superclass.
So what if you need to model multiple abstractions? Can you be a collection and be serialized? Well, not if collection and serialized are classes.
And because class inheritance is single inheritance, classes get bloated as everything that might be related gets thrown together. You also have to choose your superclass at the moment you define your class, not later in some extension. Next, if your superclass had stored properties, well, you have to accept them.
You don't get a choice.
And then because it has stored properties, you have to initialize it. And as Crusty says, 'designated convenience required, oh, my.' So you also have to make sure that you understand how to interact with your superclass without breaking its invariants. Right? And, finally, it's natural for class authors to write their code as though they know what their methods are going to do, without using final and without accounting for the chance that the methods might get overridden.
So, there's often a crucial but unwritten contract about which things you're allowed to actually override and, like, do you have to chain to the superclass method? And if you're going to chain to the superclass method, is it at the beginning of your method, or at the end, or in the middle somewhere? So, again, not news to Cocoa programmers, right? This is exactly why we use the delegate pattern all over the place in Cocoa. Okay, last on Crusty's list, classes just turn out to be a really bad fit for problems where type relationships matter.
So if you've ever tried to use classes to represent a symmetric operation, like Comparison, you know what I mean.
For example, if you want to write a generalized sort or binary search like this, you need a way to compare two elements. And with classes, you end up with something like this. Of course, you can't just write Ordered this way, because Swift demands a method body for precedes. So, what can we put there? Remember, we don't know anything about an arbitrary instance of Ordered yet.
So if the method isn't implemented by a subclass, well, there's really nothing we can do other than trap.
Now, this is the first sign that we're fighting the type system. And if we fail to recognize that, it's also where we start lying to ourselves, because we brush the issue aside, telling ourselves that as long as each subclass of Ordered implements precedes, we'll be okay.
Right? Make it the subclasser's problem.
So we press ahead and implement an example of Ordered.
So, here's a subclass. It's got a double value and we override precedes to do the comparison. Right? Except, of course, it doesn't work.
See, "other" is just some arbitrary Ordered and not a number, so we don't know that "other" has a value property. In fact, it might turn out to be a label, which has a text property.
So, now we need to down-cast just to get to the right type. But, wait a sec, suppose that "other" turns out to be a label? Now, we're going to trap. Right? So, this is starting to smell a lot like the problem we had when writing the body for precedes in the superclass, and we don't have a better answer now than we did before.
This is a static type safety hole.
Why did it happen? Well, it's because classes don't let us express this crucial type relationship between the type of self and the type of other. In fact, you can use this as a "code smell." So, any time you see a forced down-cast in your code, it's a good sign that some important type relationship has been lost, and often that's due to using classes for abstraction.
Okay, clearly what we need is a better abstraction mechanism, one that doesn't force us to accept implicit sharing, or lost type relationships, or force us to choose just one abstraction and do it at the time we define our types; one that doesn't force us to accept unwanted instance data or the associated initialization complexity. And, finally, one that doesn't leave ambiguity about what I need to override. Of course, I'm talking about protocols.
Protocols have all these advantages, and that's why, when we made Swift, we made the first protocol-oriented programming language. So, yes, Swift is great for object-oriented programming, but from the way for loops and string literals work to the emphasis in the standard library on generics, at its heart, Swift is protocol-oriented.
And, hopefully, by the time you leave here, you'll be a little more protocol-oriented yourself. So, to get you started off on the right foot, we have a saying in Swift. Don't start with a class. Start with a protocol.
So let's do that with our last example.
Okay, first, we need a protocol, and right away Swift complains that we can't put a method body here, which is actually pretty good because it means that we're going to trade that dynamic runtime check for a static check, right, that precedes as implemented. Okay, next, it complains that we're not overriding anything. Well, of course we're not. We don't have a baseclass anymore, right? No superclass, no override. And we probably didn't even want number to be a class in the first place, because we want it to act like a number. Right? So, let's just do two thing at once and make that a struct. Okay, I want to stop for a moment here and appreciate where we are, because this is all valid code again.
Okay, the protocol is playing exactly the same role that the class did in our first version of this example. It's definitely a bit better. I mean, we don't have that fatal error anymore, but we're not addressing the underlying static type safety hole, because we still need that forced down-cast because "other" is still some arbitrary Ordered.
Okay. So, let's make it a number instead, and drop the type cast.
Well, now Swift is going to complain that the signatures don't match up.
To fix this, we need to replace Ordered in the protocol signature with Self.
This is called a Self-requirement.
So when you see Self in a protocol, it's a placeholder for the type that's going to conform to that protocol, the model type. So, now we have valid code again.
Now, let's take a look at how you use this protocol. So, this is the binary search that worked when Ordered was a class.
And it also worked perfectly before we added that Self-requirement to Ordered.
And this array of ordered here is a claim.
It's a claim that we're going to handle a heterogeneous array of Ordered. So, this array could contain numbers and labels mixed together, right? Now that we've made this change to Ordered and added the Self-requirement, the compiler is going to force us to make this homogeneous, like this.
This one says, 'I work on a homogeneous array of any single Ordered type T.' Now, you might think that forcing the array to be homogeneous is too restrictive or, like, a loss of functionality or flexibility or something. But if you think about it, the original signature was really a lie. I mean, we never really handled the heterogeneous case other than by trapping. Right? A homogeneous array is what we want.
So, once you add a Self-requirement to a protocol, it moves the protocol into a very different world, where the capabilities have a lot less overlap with classes. It stops being usable as a type.
Collections become homogeneous instead of heterogeneous. An interaction between instances no longer implies an interaction between all model types.
We trade dynamic polymorphism for static polymorphism, but, in return for that extra type information we're giving the compiler, it's more optimizable. So, two worlds.
Later in the talk, I'll show you how to build a bridge between them, at least one way.
Okay. So, I understood how the static aspect of protocols worked, but I wasn't sure whether to believe Crusty that protocols could really replace classes and so I set him a challenge, to build something for which we'd normally use OOP, but using protocols.
I had in mind a little diagramming app where you could drag and drop shapes on a drawing surface and then interact with them.
And so I asked Crusty to build the document and display model.
And here's what he came up with.
First, he built some drawing primitives. Now, as you might imagine, Crusty really doesn't really do GUI's. He's more of a text man. So his primitives just print out the drawing commands you issue, right? I grudgingly admitted that this was probably enough to prove his point, and then he created a Drawable protocol to provide a common interface for all of our drawing elements.
Okay, this is pretty straightforward. And then he started building shapes like Polygon. Now, the first thing to notice here about Polygon is it's a value type, built out of other value types. It's just a struct that contains an array of points.
And to draw a polygon, we move to the last corner and then we cycle through all the corners, drawing lines. Okay, and here's a Circle.
Again, Circle is a value type, built out of other value types. It's just a struct that contains a center point and a radius.
Now to draw a Circle, we make an arc that sweeps all the way from zero to two pi radians.
So, now we can build a diagram out of circles and polygons.
'Okay,' said Crusty, 'let's take her for a spin.' So, he did. This is a diagram.
A diagram is just a Drawable. It's another value type.
Why is it a value type? Because all Drawables are value types, and so an array of Drawables is also a value type. Let's go back to that.
Wow. Okay, there. An array of Drawables is also a value type and, therefore, since that's the only thing in my Diagram, the Diagram is also a value type. So, to draw it, we just loop through all of the elements and draw each one. Okay, now let's take her for a spin.
So, we're going to test it. So, Crusty created a Circle with curiously specific center and radius. And then, with uncanny Spock-like precision, he added a Triangle.
And finally, he built a Diagram around them, and told it to draw.
'Voila,' said Crusty, triumphantly. 'As you can plainly see, this is an equilateral triangle with a circle, inscribed inside a circle.' Well, maybe I'm just not as good at doing trigonometry in my head, as Crusty is, but, 'No, Crusty,' I said, 'I can't plainly see that, and I'd find this demo a whole lot more compelling if I was doing something actually useful for our app like, you know, drawing to the screen.' After I got over my annoyance, I decided to rewrite his Renderer to use CoreGraphics.
And I told him I was going to this and he said, 'Hang on just a minute there, monkey boy.
If you do that, how am I going to test my code?' And then he laid out a pretty compelling case for the use of plaintext in testing. If something changes in what we're doing, we'll immediately see it in the output.
Instead, he suggested we do a little protocol-oriented programming.
So he copied his Renderer and made the copy into a protocol.
Yeah, and then you have to delete the bodies, okay. There it is. And then he renamed the original Renderer and made it conform. Now, all of this refactoring was making me impatient, like, I really want to see this stuff on the screen.
I wanted to rush on and implement a Renderer for CoreGraphics, but I had to wait until Crusty tested his code again.
And when he was finally satisfied, he said to me, 'Okay, what are you going to put in your Renderer?' And I said, 'Well, a CGContext. CGContext has basically everything a Renderer needs." In fact, within the limits of its plain C interface, it basically is a Renderer. 'Great,' said Crusty. 'Gimme that keyboard.' And he snatched something away from me and he did something so quickly I barely saw it.
'Wait a second,' I said. 'Did you just make every CGContext into a Renderer?' He had. I mean, it didn't do anything yet, but this was kind of amazing. I didn't even have to add a new type. 'What are you waiting for?' said Crusty. 'Fill in those braces.' So, I poured in the necessary CoreGraphics goop, and threw it all into a playground, and there it is. Now, you can download this playground, which demonstrates everything I'm talking about here in the talk, after we're done. But back to our example.
Just to mess with me, Crusty then did this. Now, it took me a second to realize why Drawing wasn't going into an infinite recursion at this point, and if you want to know more about that, you should go to this session, on Friday.
But it also didn't change the display at all.
Eventually, Crusty decided to show me what was happening in his plaintext output. So it turns out that it was just repeating the same drawing commands, twice. So, being more of a graphics-oriented guy, I really wanted to see the results. So, I built a little scaling adapter and wrapped it around the Diagram and this is the result. And you can see this in the playground, so I'm not going to go into the scaling adapter here.
But that's kind of a demonstration that with protocols, we can do all the same kinds of things that we're used to doing with classes. Adapters, usual design patterns. Okay, now I'd like to just reflect a second on what Crusty did with TestRenderer though, because it's actually kind of brilliant.
See, by decoupling the document model from a specific Renderer, he's able to plug in an instrumented component that reveals everything that we do, that our code does, in detail. And we've since applied this approach throughout our code.
We find that, the more we decouple things with protocols, the more testable everything gets. This kind of testing is really similar to what you get with mocks, but it's so much better.
See, mocks are inherently fragile, right? You have to couple your testing code to the implementation details of the code under test.
And because of that fragility, they don't play well with Swift's strong static type system. See, protocols give us a principled interface that we can use, that's enforced by the language, but still gives us the hooks to plug in all of the instrumentation we need.
Okay, back to our example, because now we seriously need to talk about bubbles.
Okay. We wanted this diagramming app to be popular with the kids, and the kids love bubbles, of course. So, in a Diagram, a bubble is just an inner circle offset around the center of the outer circle that you use to represent a highlight. So, you have two circles. Just like that.
And when I put this code in context though, Crusty started getting really agitated.
All the code repetition was making him ornery, and if Crusty ain't happy, ain't nobody happy.
'Look, they're all complete circles,' he shouted. 'I just want to write this.' I said, 'Calm down, Crusty. Calm down. We can do that.
All we need to do is add another requirement to the protocol. All right? Then of course we update our models to supply it. There's test Renderer.
And then the CGContext.' Now, at this point Crusty's got his boot off and he's beating it on the desk, because here we were again, repeating code.
He snatched the keyboard back from me, muttering something about having to do everything his own self, and he proceeded to school me using a new feature in Swift. This is a protocol extension.
This says 'all models of Renderer have this implementation of circleAt.' Now we have an implementation that is shared among all of the models of Renderer. So, notice that we still have this circleAt requirement up there. You might ask, 'what does it means to have a requirement that's also fulfilled immediately in an extension?' Good question. The answer is that a protocol requirement creates a customization point.
To see how this plays out, let's collapse this method body and add another method to the extension. One that isn't backed by a requirement. And now we can extend Crusty's TestRenderer to implement both of these methods. And then we'll just call them. Okay. Now, what happens here is totally unsurprising. We're directly calling the implementations in TestRenderer and the protocol isn't even involved, right? We'd get the same result if we removed that conformance.
But now, let's change the context so Swift only knows it has a Renderer, not a TestRenderer. And here's what happens.
So because circleAt is a requirement, our model gets the privilege of customizing it, and the customization gets called.
That one. But rectangleAt isn't a requirement, so the implementation in TestRenderer, it only shadows the one in the protocol and in this context, where you only know you have a Renderer and not a TestRenderer, the protocol implementation is called. Which is kind of weird, right? So, does this mean that rectangleAt should have been a requirement? Maybe, in this case, it should, because some Renderers are highly likely to have a more efficient way to draw rectangles, say, aligned with a coordinate system.
But, should everything in your protocol extension also be backed by a requirement? Not necessarily. I mean, some APIs are just not intended as customization points. So, sometimes the right fix is to just not shadow the requirement in the model, not shadow the method in the model. Okay. So, this new feature, incidentally, it's revolutionized our work on the Swift Standard Library.
Sometimes what we can do with protocol extensions, it just feels like magic.
I really hope that you'll enjoy working with the latest library as much as we've enjoyed applying this to it and updating it. And I want to put our story aside for a second, so I can show you some things that we did in Standard Library with protocol extensions, and few other tricks besides.
So, first, there's a new indexOf method.
So, this just walks through the indices of the collection until it finds an element that's equal to what we're looking for and it returns that index. And if it doesn't find one, it returns nil. Simple enough, right? But if we write it this way, we have a problem.
See the elements of an arbitrary collection can't be compared with equal-equal. So, to fix that, we can constrain the extension. This is another aspect of this new feature.
So, by saying this extension applies when the element type of the collection is Equatable, we've given Swift the information it needs to allow that equality comparison. And now that we've seen a simple example of a constrained extension, let's revisit our binary search.
And let's use it on an array of Int.
Hmm. Okay, Int doesn't conform to Ordered.
Well that's a simple fix, right? We'll just add a conformance.
Okay, now what about Strings? Well, of course, this doesn't work for Strings, so we do it again. Now before Crusty starts banging on his desk, we really want to factor this stuff out, right? The less-than operator is present in the Comparable protocol, so we could do this with an extension to comparable. Like this.
Now we're providing the precedes for those conformances. So, on the one hand, this is really nice, right? When I want a binary search for Doubles, well, all I have to is add this conformance and I can do it. On the other hand, it's kind of icky, because even if I take away the conformance, I still have this precedes function that's been glommed onto Doubles, which already have enough of an interface, right? We maybe would like to be a little bit more selective about adding stuff to Double. So, and even though I can do that, I can't binarySearch with it. So it's really, that precedes function buys me nothing.
Fortunately, I can be more selective about what gets a precedes API, by using a constrained extension on Ordered. So, this says that a type that is Comparable and is declared to be Ordered will automatically be able to satisfy the precedes requirement, which is exactly what we want. I'm sorry, but I think that's just really cool. We've got the same abstraction.
The same logical abstraction coming from two different places, and we've just made them interoperate seamlessly. Thank you for the applause, but I just, I think it's cool. Okay, ready for a palate cleanser? That's just showing it work. Okay. This is the signature of a fully generalized binarySearch that works on any Collection with the appropriate Index and Element types. Now, I can already hear you guys getting uncomfortable out there. I'm not going to write the body out here, because this is already pretty awful to look at, right? Swift 1 had lots of generic free functions like this.
In Swift 2, we used protocol extensions to make them into methods like this, which is awesome, right? Now, everybody focuses on the improvement this makes at the call site, which is now clearly chock full of method-y goodness, right? But as the guy writing binarySearch, I love what it did for the signature. By separating the conditions under which this method applies from the rest of the declaration, which now just reads like a regular method.
No more angle bracket blindness.
Thank you very much. Okay, last trick before we go back to our story. This is a playground containing a minimal model of Swift's new OptionSetType protocol.
It's just a struct with a read-only Int property, called rawValue. Now take a look at the broad Set-like interface you actually get for free once you've done that.
All of this comes from protocol extensions.
And when you get a chance, I invite you to take a look at how those extensions are declared in the Standard Library, because several layers are working together to provide this rich API.
Okay, so those are some of the cool things that you can do with protocol extensions.
Now, for the piece de resistance, I'd like to return to our diagramming example.
Always make value types equatable.
Why? Because I said so. Also, eat your vegetables. No, actually, if you want to know why, go to this session on Friday, which I told you about already.
It's a really cool talk and they're going to discuss this issue in detail. Anyway, Equatable is easy for most types, right? You just compare corresponding parts for equality, like this.
But, now, let's see what happens with Diagram. Uh-oh. We can't compare two arrays of Drawable for equality.
All right, maybe we can do it by comparing the individual elements, which looks something like this.
Okay, I'll go through it for you. First, you make sure they have the same number of elements, then you zip the two arrays together. If they do have the same number of elements, then you look for one where you have a pair that's not equal. All right, you can take my word for it. This isn't the interesting part of the problem. Oops, right? This is, the whole reason we couldn't compare the arrays is because Drawables aren't equatable, right? So, we didn't have an equality operator for the arrays. We don't have an equality operator for the underlying Drawables. So, can we just make all Drawables Equatable? We change our design like this.
Well, the problem with this is that Equatable has Self-requirements, which means that Drawable now has Self-requirements.
And a Self-requirement puts Drawable squarely in the homogeneous, statically dispatched world, right? But Diagram really needs a heterogeneous array of Drawables, right? So we can put polygons and circles in the same Diagram. So Drawable has to stay in the heterogeneous, dynamically dispatched world.
And we've got a contradiction. Making Drawable equatable is not going to work.
We'll need to do something like this, which means adding a new isEqualTo requirement to Drawable.
But, oh, no, we can't use Self, right? Because we need to stay heterogeneous. And without Self, this is just like implementing Ordered with classes was. We're now going to force all Drawables to handle the heterogeneous comparison case.
Fortunately, there's a way out this time. Unlike most symmetric operations, equality is special because there's an obvious, default answer when the types don't match up, right? We can say if you have two different types, they're not equal. With that insight, we can implement isEqualTo for all Drawables when they're Equatable. Like this. So, let me walk you through it.
The extension is just what we said. It's for all Drawables that are Equatable.
Okay, first we conditionally down-cast other to the Self type. Right? And if that succeeds, then we can go ahead and use equality comparison, because we have an Equatable conformance.
Otherwise, the instances are deemed unequal.
Okay, so, big picture, what just happened here? We made a deal with the implementers of Drawable. We said, 'If you really want to go and handle the heterogeneous case, be my guest. Go and implement isEqualTo. But if you just want to just use the regular way we express homogeneous comparison, we'll handle all the burdens of the heterogeneous comparison for you.' So, building bridges between the static and dynamic worlds is a fascinating design space, and I encourage you to look into more. This particular problem we solved using a special property of equality, but the problems aren't all like that, and there's lots of really cool stuff you can do. So, that property of equality doesn't necessarily apply, but what does apply almost universally? Protocol-based design.
Okay, so, I want to say a few words before we wrap up about when to use classes, because they do have their place.
Okay? There are times when you really do want implicit sharing, for example, when the fundamental operations of a value type don't make any sense, like copying this thing. What would a copy mean? If you can't figure out what that means, then maybe you really do want it to be a reference type. Or a comparison. The same thing. That's another fundamental part of being a value. So, for example, a Window. What would it mean to copy a Window? Would you actually want to see, you know, a new graphical Window? What, right on top of the other one? I don't know. It wouldn't be part of your view hierarchy.
Doesn't make sense. So, another case where the lifetime of your instance is tied to some external side effect, like files appearing on your disk.
Part of this is because values get created very liberally by the compiler, and created and destroyed, and we try to optimize that as well as possible. It's the reference types that have this stable identity, so if you're going to make something that corresponds to an external entity, you might want to make it a reference type. A class. Another case is where the instances of the abstraction are just "sinks." Like, our Renderers, for example.
So, we're just pumping, we're just pumping information into that thing, into that Renderer, right? We tell it to draw a line.
So, for example, if you wanted to make a TestRenderer that accumulated the text to output of these commands into a String instead of just dumping them to the console, you might do it like this. But notice a couple of things about this. First, it's final, right? Second, it doesn't have a base class. That's still a protocol. I'm using the protocol for the abstraction. Okay, a couple of more cases.
So, we live in an object-oriented world, right? Cocoa and Cocoa Touch deal in objects.
They're going to give you baseclasses and expect you to subclass them. They're going to expect objects in their APIs. Don't fight the system, okay? That would just be futile.
But, at the same time, be circumspect about it. You know, nothing in your program should ever get too big, and that goes for classes just as well as anything else. So, when you're refactoring and factoring something out of class, consider using a value type instead. Okay, to sum up. Protocols, much greater than superclasses for abstraction.
Second, protocol extensions, this new feature to let you do almost magic things. Third, did I mention you should go see this talk on Friday? Go see this talk on Friday. Eat your vegetables.
Be like Crusty.
Thank you very much.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.