Post on 15-Apr-2017
Why the Dark Side should use Swift and a SOLID Architecture
Jorge D. Ortiz-Fuentes @jdortiz
#SwiftDarkSide
A Canonical Examples
production
#SwiftDarkSide
#SwiftDarkSide
Agenda
The Weaknesses of the Dark Side
SOLID Architecture
Design patterns
Weaknesses of the Dark
Side
Conspiracy Theory
Agility and the MVP
Moving a Planet
SPoF
Whose blame is that?
Framework dependency
Teams can hardly work together
Do you see any
problems?
#SwiftDarkSide
Needs to Address
Fast growth
Robustness
testable
decoupled
debuggable (blame)
Team collaboration
Reusable
Defer decisions
Replaceable frameworks
There is a way: SOLID Principles
AKA Clean
Architecture
I find your lack of faith disturbing
SOLID Architecture
Clean ArchitectureAppDelegate
View (VC) Presenter Interactor Entity Gateway
Connector
#SwiftDarkSide
Single Responsibility
MVC is not enough
More classes, but more cohesive: only one reason to change
#SwiftDarkSide
Business Logicimport Foundation
class ShowAllSpeakersInteractor {
// MARK: - Properties let entityGateway: EntityGatewayFetchSpeakersProtocol weak var presenter: SpeakersListPresenterProtocol?
// MARK: - Initializers init(entityGateway: EntityGatewayFetchSpeakersProtocol) { self.entityGateway = entityGateway } }
extension ShowAllSpeakersInteractor: InteractorCommandProtocol { func execute() { let entities = entityGateway.fetchAllSpeakers() let displayData = entities.map({entity in return SpeakerDisplayData(speaker: entity)}) presenter?.presentAllSpeakers(displayData) } }
#SwiftDarkSide
Open Close
Open to Extension, Closed to Modification
#SwiftDarkSide
class SpeakersTableViewController: UITableViewController, SegueHandlerTypeProtocol { static let speakerCellIdentifier = "SpeakerCell" var eventHandler: SpeakersListEventHandlerProtocol? var numberOfRows = 0 override func viewDidLoad() { super.viewDidLoad() eventHandler?.viewDidLoad() }
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return numberOfRows }
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier(SpeakersTableViewController.speakerCellIdentifier, forIndexPath: indexPath) as! SpeakerTableViewCell eventHandler?.presentCell(cell, indexPath: indexPath) return cell } }
extension SpeakersTableViewController: SpeakersListViewProtocol { func configureListWithNumberOfRows(numberOfRows: Int) { self.numberOfRows = numberOfRows }
func addRowsAtIndexPaths(indexPaths:[NSIndexPath]) { self.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Automatic) } }
Passive View
#SwiftDarkSide
Liskov Substitution
It should be possible to use derived classes without special care
#SwiftDarkSide
extension SpeakersListsPresenter: SpeakersListEventHandlerProtocol { func viewDidLoad() { interactor.execute() }
func presentCell(cell: SpeakerCellProtocol, indexPath: NSIndexPath) { let index = indexPath.row guard index < speakers.count else { return } let speaker = speakers[index] cell.displayName(speaker.name) cell.displayTitle(speaker.title) cell.displayDateSubmitted(relativeDateStringFromDate(speaker.dateSubmitted)) } }
Presentationclass SpeakersListsPresenter { weak var view: SpeakersListViewProtocol? let interactor: ShowAllSpeakersInteractor private var speakers: [SpeakerDisplayData]=[]
// MARK: - Initializer init(interactor: ShowAllSpeakersInteractor, deleteInteractor: DeleteSpeakerInteractorProtocol) { self.interactor = interactor } }
extension SpeakersListsPresenter: SpeakersListPresenterProtocol { func presentAllSpeakers(speakers: [SpeakerDisplayData]) { view?.configureListWithNumberOfRows(speakers.count) self.speakers = speakers let addedIndexPaths = speakers.enumerate() .map({(index, _) in return NSIndexPath(forRow: index, inSection: 0)}) view?.addRowsAtIndexPaths(addedIndexPaths) } }
#SwiftDarkSide
Interface Segregation
Dont force a class to depend on methods that it wont use
One interactor will not likely use every functionality of the entity gateway
#SwiftDarkSide
class InMemorySpeakersRepo { // MARK: - Properties var speakers: [Speaker] = [] init() { } }
extension InMemorySpeakersRepo: EntityGatewayFetchSpeakersProtocol { func fetchAllSpeakers() -> [Speaker] { return speakers } }
extension InMemorySpeakersRepo: EntityGatewayCreateSpeakerProtocol { func createSpeaker(name name: String, title: String, synopsis: String, dateSubmitted: NSDate) { let id = IdentityGenerator.newUUID() speakers.append(Speaker(id: id, name: name, title: title, synopsis: synopsis, dateSubmitted: dateSubmitted)) } }
extension InMemorySpeakersRepo: EntityGatewayDeleteSpeakerProtocol { func deleteSpeaker(id: String) { speakers = speakers.filter { speaker in speaker.id != id } } }
Different Functions of the Entity Gateway
#SwiftDarkSide
Dependency Inversion
The business case SHOULDNT depend on the frameworks
#SwiftDarkSide
Deletion Use Caseclass DeleteSpeakerInteractor { let entityGateway: EntityGatewayDeleteSpeakerProtocol var id: String?
init(entityGateway: EntityGatewayDeleteSpeakerProtocol) { self.entityGateway = entityGateway } }
extension DeleteSpeakerInteractor : InteractorCommandProtocol { func execute() { guard let id = self.id else { return } entityGateway.deleteSpeaker(id) } }
Design Patterns
Take me Outta Herefunc presentCell(cell: SpeakerCellProtocol, indexPath: NSIndexPath) { let index = indexPath.row guard index < speakers.count else { return } let speaker = speakers[index] cell.displayName(speaker.name) cell.displayTitle(speaker.title) cell.displayDateSubmitted(relativeDateStringFromDate(speaker.dateSubmitted)) }
Know
That!
var control = Houston(fuel: 1.0, astronaut: nil, spaceshipOK: true)
do { try control.launchSpaceship() } catch Houston.LaunchError.NoFuel { // Add Fuel print("Adding fuel") } catch Houston.LaunchError.NoAstronaut { print("Next in line") } catch Houston.LaunchError.BrokenShip(let problem) { print(problem) } catch let unknowError { // }
Ready for Lifeclass Houston { let fuel: Double let astronaut: String let spaceshipOK: Bool init (fuel: Double, astronaut: String?, spaceshipOK: Bool) { self.fuel = fuel self.astronaut = astronaut ?? "" self.spaceshipOK = spaceshipOK } enum LaunchError: ErrorType { case NoFuel, NoAstronaut, BrokenShip(String) } func launchSpaceship() throws { guard fuel >= 1.0 else { throw LaunchError.NoFuel } guard astronaut != "" else { throw LaunchError.NoAstronaut } guard spaceshipOK else { throw LaunchError.BrokenShip("Engine") } print("Launching spaceship") } }
Know
That!
class Text { func displayContents() { print("Hola") } }
class NullText: Text { override func displayContents() { print(Not much here") } }
func fetchText() -> Text { return NullText() }
let text = fetchText() text.displayContents()
Null Objectclass Text { func displayContents() { print("Hola") } }
func fetchText() -> Text? { return nil }
if let text = fetchText() { text.displayContents() }
Poor Templateclass PoorTrooper { func getIntoSpaceShip() { print("Commencing countdown engines on") }
func conquer(target: String) { fatalError() }
func comeBack() { print("Planet earth is blue") }
func attack(target: String) { getIntoSpaceShip() conquer(target) comeBack() } }
let cloneTrooper = PoorTrooper() cloneTrooper.attack("Alderaan")
Swifty Templateprotocol Trooper { func getIntoSpaceShip() func conquer(target: String) func comeBack() func attack(target: String) }
extension Trooper { func getIntoSpaceShip() { print("Commencing countdown engines on") }
func comeBack() { print("Planet earth is blue") }
func attack(target: String) { getIntoSpaceShip() conquer(target) comeBack() } }
class StormTrooper: Trooper { func conquer(target: String) { print("Become part of the Empire, \(target)") } }
let finn = StormTrooper() finn.attack("Tatooine")
Next Steps
#SwiftDarkSide
RecommendationsPay attention to your architecture; It always pays off
Use Principles to take decisions
Take advantage of the language and adapt the patterns to it
In case of doubt, ask for an architecture loan and ship it
canonicalexamples.com coupon:
APPSTERDAMERS16
http://canonicalexamples.com
Thank you!
@jdortiz #SwiftDarkSide