Don’t use subclasses! Swift is protocol-oriented at its core

Don’t use subclasses! Swift is protocol-oriented at its core

The core of Swift

We can understand Swift through the transitivity of equality:

  • The core of Swift is protocol-oriented programming.
  • The core of protocol-oriented programming is abstraction and simplicity.
  • So the core of Swift is abstraction and simplification.

You may be surprised by my title. I am not saying that subclasses have no value, especially when using single inheritance, classes and subclasses are certainly powerful tools. However, I am saying that the problem of daily iOS development is the overuse of classes and inheritance. As object-oriented programmers (OOP programmers, object-oriented programming is abbreviated as OOP), we always naturally tend to use reference types and classes to solve problems, but I personally think that we should turn it around and use value types instead of reference types. We still have to write modular, scalable and reusable code, and this will not change. The powerful value types in Swift can help us achieve this goal without having too strong a reliance on reference types. I think not only protocol oriented programming (POP) can help us achieve this, but also two other types of programming, which have the core idea of ​​abstraction and simplification: value-oriented programming (VOP) and functional programming.

Let me make it clear that I am by no means an expert in these types of programming (POP, VOP and functional programming). Like you, I have been an OOP programmer since the days of MMM (manual memory management). Through self-study, I have attached great importance to the idea of ​​value abstraction and simplification since the beginning. I didn't realize that I was an OOP programmer who was inclined to functional programming, and I often used VOP and POP ideas. This is probably why I jumped on the Swift bandwagon on the first day. During the whole week of WWDC, I was filled with the feeling that the core concepts of Swift were so consistent with how I thought programming should be done. Through this article, I hope to help you (OOP programmer) open your mind and think about how to solve problems in a more Non-OOP way.

The Problem with OOP (and Why I Had to Learn It)

I'll be the first to say it: it's hard to make an iOS app without OOP. Cocoa is OOP at its core. You can't write an iOS app without OOP. Sometimes I fantasize that this isn't true. If you think differently, prove me wrong. I really need this, please, prove me wrong!

No matter what, you will always encounter a situation where you have to use objects, use reference types to solve a problem, and then because of the rules of Cocoa, you are forced to use classes. In this case, you encounter the problems we all know and love:

  • Passing class instances seems to have an uncanny ability to make the state of an instance different from what you expect when you use it. (This is due to mutable state, where another owner of your object can change the properties of the object when it sees fit.)
  • Without multiple inheritance, deriving from a great class to gain its extended functionality prevents you from using more features of other great classes and adds complexity. (For example, try combining two UITextField subclasses to create a super UITextField that has the features of both.)
  • Another problem with the above is that it can lead to unexpected behavior. If you encounter a situation similar to the one described above, you have fallen into a dependency problem: you have linked the characteristics of two superclasses, and a change to one superclass may have adverse effects on the other superclass. This is the problem caused by the well-known tight coupling between classes.
  • Mocking in unit testing. Some classes are so tightly coupled to the state of the environment in the system that fully testing them requires you to create fake representations of each class. I don't even need to tell you that you're not actually testing the class, you're just pretending to test it. I'm not even going to mention that many mocking libraries use runtime tricks to create fake classes.
  • Concurrency issues. This is a concomitant issue with the mutable state mentioned above. If you change a reference from multiple threads at the same time, this will cause an exception in the synchronization between objects at runtime. I really don't need to tell you this.
  • This can easily lead to anti patterns like God classes (which take on all responsibilities for important high-level code that many subclasses need), Blobs (classes with too much authority), Lava Flow (classes that no one dares to touch because they contain too much illegal code), etc.

[[143680]]

POP Protocol Oriented Programming

It's so easy to fall into OOP anti-patterns. Most of the time we (including me) are just too lazy to click File>New File. As a result, we don't want to create a new class from scratch because it's so easy to add a function to an existing class. If you keep doing this, and are too lazy to subclass a "very important" class, you'll end up with a God class/Death Star class. I actually did this before: I gave every view controller in an app the ability to present an error view that pointed to the navigationBar of the navigationController. Ugh, stupid me. When it came time to change the behavior of the Error God class, I had to change the entire app. Not a smart move, you should really look at the bugs.

One class that controls everything = bugs everywhere

If POP is used, this Error God class can be easily abstracted to a large extent, and it is also convenient to improve it in the future. (By the way, if you want to learn POP, I strongly recommend you to watch this video.) It's funny to think about it, because in this video Apple itself said:

"Start with a protocol, not a class." - Dave Abrahams: The professor who ruins your worldview

Here’s an example that shows how brutal this is:

  1. class PresentErrorViewController: UIViewController {
  2. var errorViewIsShowing: Bool = false  
  3. func presentError(message: String = "Error!", withArrow shouldShowArrow: Bool = false , backgroundColor: UIColor = ColorSalmon, withSize size: CGSize = CGSizeZero, canDismissByTappingAnywhere canDismiss: Bool = true ) {
  4. //Wrote complex, fragile code  
  5. }
  6. }
  7. //Say, there are 100 classes that inherit this class  
  8. EveryViewControllerInApp: PresentErrorViewController {}

As the project progressed it quickly became clear that not every UIViewController needed this error logic, or really needed every feature this class provided. Anyone on my team could easily change something in this superclass and affect the entire app. This made the code fragile. It also made the code polymorphic. While it should be up to the subclass to determine its own behavior, the superclass here helped decide. Here’s how we can better structure this code in Swift 2.0 using POP:

  1. protocol ErrorPopoverRenderer {
  2. func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool)
  3. }
  4. extension UIViewController: ErrorPopoverRenderer { //Make all UIViewControllers that comply with the ErrorPopoverRenderer protocol have a default implementation of presentError  
  5. func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool) {
  6. //Add the default implementation for rendering the error view  
  7. }
  8. }
  9. class KrakenViewController: UIViewController, ErrorPopoverRenderer { //Drop the God class and make KrakenViewController conform to the new ErrorPopoverRenderer Protocol.  
  10. func methodThatHasAnError() {
  11. //…  
  12. //Throws an error because the Kraken will feel uncomfortable eating humans today.  
  13. presentError( /*blah blah blah lots of parameters*/ )
  14. }
  15. }

See, something cool is happening here. Not only have we eliminated the God class, but we've also made our code more modular and extensible. By creating an ErrorPopoverRenderer protocol, any class that conforms to it will have the ability to present an ErrorView. Not only that, our KrakenViewController class doesn't have to implement presentError because we extended UIViewController to provide a default implementation.

But wait! There’s a problem! We have to implement each parameter every time we want to render an ErrorView. This is a bit annoying because we can’t provide default values ​​for parameters in the protocol function declaration.

I like these parameters! What’s worse is that we’ve introduced complexity in the process of making the code more modular. Let’s go ahead and use a little trick added in Swift 2.0 to compensate for it a bit:

  1. protocol ErrorPopoverRenderer {
  2. func presentError()
  3. }
  4. extension ErrorPopoverRenderer where Self: UIViewController {
  5. func presentError() {
  6. //Add the default implementation here and provide the default parameters for ErrorView.  
  7. }
  8. }
  9. class KrakenViewController: UIViewController, ErrorPopoverRenderer {
  10. func methodThatHasAnError() {
  11. //…  
  12. //Throws an error because the Kraken will feel uncomfortable eating humans today.  
  13. presentError() //Woohoo! No more parameters! We now have a default implementation!  
  14. }
  15. }

Okay, now it's looking pretty good. Not only did we get rid of those annoying parameters, but we also used the new features of Swift 2.0 to give presentError a default implementation using Self at the protocol level. Using Self means that this extension will only be effective if and only if the protocol conformer inherits from UIViewController. This allows us to treat ErrorPopoverRenderer as a real UIViewController without even having to extend the latter! Even better, from now on, the Swift runtime calls the presentError() method with static dispatch rather than dynamic dispatch. Basically, we have enhanced the performance of the presentError() method at the function call point.

Alas, there is still a problem. Our journey of POP has come to an end here, but we will not stop improving it. Our question is what if we only want to use default values ​​for some parameters and not for the rest? POP can't help us in this regard, but we can find another way. Now, let's use VOP.

#p#

VALUE-ORIENTED PROGRAMMING

You see, POP and VOP always go together. In the WWDC video link above, Crusty makes some bold claims: we can do everything that classes can do with struct and enum types. I agree with this to a large extent, but not to such an extreme. In my opinion, protocols are essentially the glue that holds VOP together, and I agree with Crusty on this point. In fact, since we are talking about the core concept of Swift and VOP, I want to show you a great interview with Andy Matuschak about VOP in Swift.

An excellent picture taken from the topic:

It can be seen that in Swift's standard library, only 4 classes and the remaining 95 struct and enum instances together construct the core of Swift's functions.

Andy elaborated: When programming in Swift we have to think about using a thin layer of objects, and a thick layer of value types. Classes have their place, but I try to think of them as being at a very high level in the object layer, where we manage various behaviors by manipulating the logic in the value type layer.

"Separate logic from behavior" - Andy Matuschak

As you know, when a value type is assigned to a variable or constant, or passed to a function as an argument, its value is copied. This allows value types to have only one owner at any time, thus reducing complexity. Contrary to reference types, reference types can have many owners during the assignment process, some of which you may not even be aware of. Using a reference at any point in time can cause side effects: the owner of the reference can do mischief and change the reference behind your back. Class = high complexity, value = low complexity.

By taking advantage of the simplicity of value types, let's implement the default parameter design mentioned earlier. We use Brian Gesiak's value options paradigm method:

  1. struct Color {
  2. let red: Double
  3. let green : Double
  4. let blue: Double
  5. init(red: Double = 0.0 , green: Double = 0.0 , blue: Double = 0.0 ) {
  6. self.red = red
  7. self.green = green
  8. self.blue = blue
  9. }
  10. }
  11. struct ErrorOptions {
  12. let message: String
  13. let showArrow: Bool
  14. let backgroundColor: UIColor
  15. let size: CGSize
  16. let canDismissByTap: Bool
  17. init(message: String = "Error!" , shouldShowArrow: Bool = true , backgroundColor: Color = Color(), size: CGSize = CGSizeZero, canDismissByTappingAnywhere canDismiss: Bool = true ) {
  18. self.message = message
  19. self.showArrow = shouldShowArrow
  20. self.backgroundColor = backgroundColor
  21. self.size = size
  22. self.canDismissByTap = canDismiss
  23. }
  24. }
Using the above option type struct (value type!) gives our POP some VOP color, as follows:
  1. protocol ErrorPopoverRenderer {
  2. func presentError(errorOptions: ErrorOptions)
  3. }
  4. extension ErrorPopoverRenderer where Self: UIViewController {
  5. func presentError(errorOptions = ErrorOptions()) {
  6. //Add the default implementation here and provide the default parameters for ErrorView.  
  7. }
  8. }
  9. class KrakenViewController: UIViewController, ErrorPopoverRenderer {
  10. func failedToEatHuman() {
  11. //…  
  12. //Throws an error because the Kraken will feel uncomfortable eating humans today.  
  13. presentError(ErrorOptions(message: "Oh noes! I didn't get to eat the Human!" , size: CGSize(width: 1000.0 , height: 200.0 ))) //Woohoo! No more parameters! We now have a default implementation!  
  14. }
  15. }

As you can see, we have given it a completely abstract, scalable and modular way to use view controllers for error handling, without forcing all view controllers to inherit from a god class. This is especially helpful when you have a god class with different functionalities. In addition, when implementing other functionalities like the error functionality above in this way, you can put the code implementing the functionality anywhere without doing too much refactoring or changing the code framework.

[[143682]]

Functional Programming

Let's solve this. I'm new to functional programming as well, but I do know this: the paradigm calls for a style of programming that encourages programmers to avoid mutable data and changing state. Similar to mathematical functions, functional programming consists of functions whose output depends only on their input parameters, and whose output is not affected by dependencies outside of the function. This is known as "data in, data out", meaning that every time a value is passed in, it should always be the same when it goes out as when it came in. Think of unit testing!

If we write code with a functional mindset, we can combine VOP with functional programming and take advantage of its many advantages, including but not limited to:

  • Completely thread-safe code (value type variables are copied when assigned in concurrent code, meaning another thread cannot change variables in a parallel thread).

  • More detailed unit testing

  • No more mocks in unit tests (value variables eliminate the need to recreate an environment where you must use mock objects just to test a small portion of functionality. Essentially, you can recreate anything you want by instantiating a feature that is abstracted from any dependencies.)

  • The code is cleaner (and, let’s be honest, as polished as porcelain ).

  • Surprise your friends

  • Very cool

  • Let Kraken worship you crazily

When to use subclasses

When should you use subclassing? The answer is when you have no choice. For example:

  • When the system requires it. Many Cocoa APIs require you to use classes, and you shouldn't use value types to fight the system. UIViewController is a subclass, otherwise your app will be useless. Don't fight the system!
  • When you need something to help you manage value-type variables between other class instances, and you also need to communicate with these value-type variables. For this situation, Andy Matuschak gave a good example: use a class to get the value calculated by a value-type drawing system and pass it to a Cocoa class to draw the drawing system to the screen.
  • When you need or want to do implicit sharing between many owners. An example of this is Core Data. Data persistence is fickle, and with Core Data, it is effective to use subclasses to synchronize the many owners that need synchronization. But be careful of concurrency issues! This is the trade-off you have to make when dealing with such problems.
  • When you don't know what a copy of a reference type means. Would you copy a singleton? No. Would you copy a UIViewController? No. A window? Absolutely not. (You can, it's your privilege.)
  • Singletons are a typical example of when the lifecycle of an instance is tied to an external effect, or when a stable identity is simply needed.

in conclusion

As OOP programmers we are used to solving problems with classes. Over time we have developed patterns to compensate for the drawbacks of reference types. My view is that a different way of thinking in programming can effectively mitigate the use of these compromises. If we really care about scalability and reusability, we have to accept that modular programming is the way to go. Using value types combined with the new and improved protocols in Swift 2.0 will easily achieve this goal. Although the OOP way of thinking makes it difficult for us to think in terms of VOP and POP, as we write more in Swift, VOP and POP patterns will begin to become second nature. It may take a few more lines of code for our brains to adapt to this way of thinking, but I believe that the iOS community as a whole will accept these practices and greatly reduce the difficulty of our daily problem solving. At the core of Swift is an extremely powerful value type system, and frankly, we should hone our minds in VOP thinking from the beginning to promote the advantages of this value system. I hope this article can help you write more detailed, inherently safe code every day.

Happy programming, coders!

<<:  Listen to stories and learn Swift series - Xiao Ming and the red envelope (optionals - optional type)

>>:  Exclusive interview with Qu Yi, Senior Technical Director of Qilekang: Notepad, Code and Crow5

Recommend

How to use a little creativity to create a hit product?

Details determine success or failure. Do you agre...

Yamanaka City April Training Camp

Introduction to the April training camp resources...

iOS14: The spring of the beggar version of iPhone is here!

Due to the disappointment caused by iOS13 to Appl...

Yogurt really helps digestion! But it's not what you think...

Have you seen the news recently about a certain b...

5 steps to build a self-propagating brand!

Marketing often involves planning a series of act...

Changsha Tea Tasting Peripheral Reliable Recommendation

Content: Changsha New Tea website appointment arr...

How to bid on Baidu? How to do Baidu bidding promotion?

With the arrival of 2021, it has become increasin...

National Eye Care Day丨Don’t believe these 10 rumors about eyes →

As the saying goes, "eyes are windows to the...

Why would a spider magnified 100 times be crushed by itself?

Today we are going to talk about a topic that is ...

How to perform user segmentation and achieve refined operations?

The author of this article mainly shares the appl...