Favor composition over inheritance — in Swift

Application design still has importance, no matter which “fun” framework you want to learn this week

Image for post
Image for post

Object-oriented langages have been accused of a lack of reusability. The accusation is that there is an implicit exnvironment that they carry around them. A possible solution to this is protocol conformance, and this Medium post will cover this particular topic.

Prerequisites:

  • Some understanding of OOP is useful

Terminology

Interface: A programming structure that allows properties to be exposed as a public API.

Inheritance: A fundamental OO concept that enables new objects to take on the properties of existing objects. A class that inherits from a superclass is called a subclass or derived class.

Mixin: A special kind of multiple inheritance. Is a trait with its own properties, and since property values are stored in the concrete class rather than the protocol we can say that Swift does not fully support mixins.

Trait: An interface containing method bodies (in Swift protocol extensions can provide the trait of interface methods).

The problem

Saving and loading files

To do so we are going to use inheritance, in this case the implementation of a base class.

This means that each class inheriting from it needs to implement a read and a write function

class FileHandlerBaseClass {func read(_ filename: String) -> String {return ""}func write(_ filename: String, _ contents: String) {}func allFiles() -> [String] {return []}
}

Which makes perfect sense for reading and writing files from the file system. We would want to return all of the files as an array of String (as shown), which we can then implement as follows:

class LocalFileHandler: FileHandlerBaseClass {override func read(_ filename: String) -> String {let ddpath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)let filename = ddpath.first!.appendingPathComponent(filename)var localFile = String()if let fnString = try? String(contentsOf: filename, encoding: String.Encoding.utf8){localFile = fnString}return localFile}override func write(_ filename: String, _ contents: String) {let ddpath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)let filename = ddpath.first!.appendingPathComponent(filename)do {try contents.write(to: filename, atomically: true, encoding: String.Encoding.utf8)} catch {print ("\(error) Error")}}}

However we become rather unstuck when we want a UserDefaults implementation to use the same base class.

class LocalUserDefaultsHandler: FileHandlerBaseClass {override func read(_ filename: String) -> String {return UserDefaults.standard.string(forKey: filename)!}override func write(_ filename: String, _ contents: String) {UserDefaults.standard.set(contents, forKey: filename)}}

The problem isn’t obvious — but the information in UserDefaults is stored as a dictionary so we want to implement write all files as something that returns a dictionary. No problem: we can update the base class.

class FileHandlerBaseClass {func read(_ filename: String) -> String {return ""}func write(_ filename: String, _ contents: String) {}func allFiles() -> [String] {return []}func allKeysValues() -> [String: String] {return [:]}}

Putting this all into the top of the hierarchy doesn’t seem right. But if we want to reuse these function (for example with a Core Data, SQLite, API call…) it needs to be in the base class. There is now a danger that we can use a method in a subclass that does not support it!

Possible disaster

Image for post
Image for post
A future earth due to your fragile and inflexible Base Class. Future possible earth declares you “loser”

The solution

Composition

A solution? Multiple protocols:

protocol FileHandlerReadable {func read(_ filename: String) -> String}protocol FileHandlerWritable {func write(_ filename: String, _ contents: String)}protocol FileHandlerReadDict {func allKeysValues() -> [String: String]}protocol FileHandlerReadStrings {func allFiles() -> [String]}

When you want to conform to multiple protocols a good strategy is to use a typealias:

typealias UDFileHandler = FileHandlerReadable & FileHandlerWritable

which we can then conform to:

class LocalUserDefaultsHandler: UDFileHandler {func allKeysValues() -> [String : String] {// return dictionary of valuesreturn [:]}func read(_ filename: String) -> String {return UserDefaults.standard.string(forKey: filename)!}func write(_ filename: String, _ contents: String) {UserDefaults.standard.set(contents, forKey: filename)}}

and likewise our local files can conform to a typealias:

typealias LFFileHandler = FileHandlerReadable & FileHandlerWritable & FileHandlerReadStrings

by doing the following

class LocalFileHandler: LFFileHandler {func allFiles() -> [String] {// return list of the filesreturn []}func read(_ filename: String) -> String {let ddpath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)let filename = ddpath.first!.appendingPathComponent(filename)var localFile = String()if let fnString = try? String(contentsOf: filename, encoding: String.Encoding.utf8){localFile = fnString}return localFile}func write(_ filename: String, _ contents: String) {let ddpath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)let filename = ddpath.first!.appendingPathComponent(filename)do {try contents.write(to: filename, atomically: true, encoding: String.Encoding.utf8)} catch {print ("\(error) Error")}}}

Now what about mixins?

As you will well know having a default implementation in a protocol results in a nasty error:

Image for post
Image for post

To overcome this we can simply use a protocol extension. Meaning if we wanted out Core Data implementation to return have a default read, we would not need to provide an implementation for read

protocol FileHandlerReadable {func read(_ filename: String) -> String}extension FileHandlerReadable {func read(_ filename: String) -> String {return ""}}

WOOT!

Conclusions

We’ve seen the disadvantage of not favoring compostion.

So what’s stopping you?

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store