Swift: Why You Shouldn’t Use Default Implementations in Protocols
I’m currently doing a very big refactor of try! Swift Data now that the Tokyo conference is over and I have time to repay some technical debt. As part of the refactor, I’m removing a bunch of large switch statements for view-level display data and putting them into individual view models that conform to a strict protocol.
The Setup
The conference app has different sessions – talks, breakfast, lunch, announcements, etc. They are all displayed in a Table View with a title, subtitle, location, etc. Before, the data layer was messy like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
// Session.swift import RealmSwift import Foundation @objc public enum SessionType: Int { case workshop case meetup case breakfast case announcement case talk case lightningTalk case sponsoredDemo case coffeeBreak case lunch case officeHours case party } public class Session: Object { /** The type of content in this particular session */ open dynamic var type: SessionType = .talk // many other class properties here /***************************************************/ // VIEW DISPLAY LOGIC BELOW /** The main name of this session */ public var formattedTitle: String? { switch self.type { // VERY LONG SWITCH STATEMENT // LOTS OF DISPLAY LOGIC } } /** A follow-up tagline for the session */ public var formattedSubtitle: String? { switch self.type { // VERY LONG SWITCH STATEMENT // LOTS OF DISPLAY LOGIC } } /** What image, if any is available for this session */ public var logoURL: URL { switch self.type { // VERY LONG SWITCH STATEMENT // LOTS OF DISPLAY LOGIC } } /** The location for where this session will occur */ public var formattedLocation: String { switch self.type { // VERY LONG SWITCH STATEMENT // LOTS OF DISPLAY LOGIC } } /** A long-form description of the session */ public var sessionDescription: String { switch self.type { // VERY LONG SWITCH STATEMENT // LOTS OF DISPLAY LOGIC } } /** Presentation Summary */ public var presentationSummary: String { switch self.type { // VERY LONG SWITCH STATEMENT // LOTS OF DISPLAY LOGIC } } // YOU GET THE POINT // MORE METHODS HERE WITH A LOT OF SWITCH STATEMENTS } |
So I extracted the data display methods into a protocol:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
protocol SessionDisplayable { /** The main name of this session */ var title: String { get } /** A follow-up tagline for the session */ var subtitle: String { get } /** What image, if any is available for this session */ var logoURL: URL { get } /** The location for where this session will occur */ var location: String { get } /** A long-form description of the session */ var sessionDescription: String { get } /** Presentation Summary */ var presentationSummary: String { get } /** What Twitter handle, if any represents this session */ var twitter: String { get } /** Whether this type of session requires a new view controller to display more information */ var selectable: Bool { get } } |
And created individual view models for each session type. For example, here is the BreakfastSessionViewModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// BreakfastSessionViewModel.swift struct BreakfastSessionViewModel: SessionDisplayable { private let session: Session private let dataDefaults: SessionDataDefaults init?(session: Session) { if session.type == .breakfast { self.session = session self.dataDefaults = SessionDataDefaults(session: session) } else { return nil } } var title: String { return dataDefaults.title } var subtitle: String { return dataDefaults.subtitle } var logoURL: URL { return dataDefaults.imageURL ?? dataDefaults.logoImageURL } var location: String { return dataDefaults.location } var sessionDescription: String { return "❤️".localized() } var presentationSummary: String { return dataDefaults.summary } var selectable: Bool { return false } var twitter: String { return "@\(dataDefaults.twitter)" } } |
This allowed me to have only one switch statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public struct SessionViewModel: SessionDisplayable { private let displayble: SessionDisplayable public init(session: Session) { switch session.type { case .workshop: displayble = WorkshopSessionViewModel(session)! case .meetup: displayble = MeetupSessionViewModel(session)! case .breakfast: displayble = BreakfastSessionViewModel(session)! case .announcement: displayble = AnnouncementSessionViewModel(session)! case .talk: displayble = TalkSessionViewModel(session)! case .lightningTalk: displayble = LightningTalkSessionViewModel(session)! case .sponsoredDemo: displayble = SponsoredDemoSessionViewModel(session)! case .coffeeBreak: displayble = CoffeeBreakSessionViewModel(session)! case .lunch: displayble = LunchSessionViewModel(session)! case .officeHours: displayble = OfficeHoursSessionViewModel(session)! case .party: displayble = PartySessionViewModel(session)! } } public var title: String { return displayble.title } public var subtitle: String { return displayble.subtitle } public var logoURL: URL { return displayble.logoURL } public var location: String { return displayble.location } public var sessionDescription: String { return displayble.sessionDescription } public var presentationSummary: String { return displayble.presentationSummary } public var twitter: String { return displayble.twitter } public var selectable: Bool { return displayble.selectable } } |
The Problem
The big thing here is that multiple session view models have the same default data implementation. That is why I created a SessionDataDefaults object to access the default data easily (see the use-case in the BreakfastSessionViewModel implementation).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// SessionDefaults.swift struct SessionDataDefaults { let title: String let subtitle = "try! Conference" let logoImageURL = Bundle.trySwiftAssetURL(for: "Logo.png")! let imageURL: URL? let location: String let summary = Conference.current.localizedDescription let twitter = Conference.current.twitter! init(session: Session) { // properties set here } } |
So as you can imagine, some of the session view model implementations (check the BreakfastSessionViewModel for reference) use the default data values.
When I shared this refactor with a friend, he immediately saw a new refactoring opportunity – create default implementations of the relevant methods in the protocol!
Default Implementation?
At first, that sounded great, but after thinking about it I decided against the default implementation refactor. Here’s why:
- Each variable of a session needs a lot of thought put into it. Even if the implementation details end up being the same as the default, I want it to be a strong conscious choice. If I make it a default implementation, it would be too easy to forget and not think about much. Oh, and the compiler won’t complain!
- I want it to be easy to change the variable for each session. If a variable is not included in the file because the implementation is in the protocol, it’s more work to make the decision to add that into the file. The assumption is that the default should win. If it’s already there, it’s easier to just go in and make a small change.
- Similar to the above point, I want it to be super readable where each variable is coming from. If a bunch of variables are in the default implementation of the protocol, it’s not as clear just by looking at the file.
I think adding default implementations to protocols should be considered very very carefully. The default needs to be something that is consistent and is the default for most cases, so if someone forgets to implement it (because the compiler won’t complain), it’s most likely not a big deal.
The one case where I love default implementations in protocols is when the function is not included in the the protocol definition – it’s just a common method with no interface exposed. Otherwise, it’s way too easy to forget about and introduce confusing bugs later!