Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Thibault Klein - Architecting for Reusability

Thibault Klein - Architecting for Reusability

At Prolific, we often work with partners who own more than one brand, and want mobile apps for all of them. As engineers, we naturally want to share as much code between each project as possible. Building these projects has given us the chance to tackle some unique architectural challenges.

In this talk, I will go over two different approaches we've taken and give you some insights on the technical decisions we made. Whether you're building for a massive, multi-brand commerce empire or a solo-developer side project, these tips can help you make your codebase more modular and reusable.

Thibault is a Senior iOS Engineer at Prolific Interactive. He's led several large commerce projects, including partnerships with David's Bridal, Lilly Pulitzer and Saks Fifth Avenue.

Recording from the March 2017 Brooklyn Swift Developers Meetup: meetup.com/Brooklyn-Swift-Developers/events/238066139
Video: vimeo.com/211194632
Code: github.com/prolificinteractive/ReusabilityMeetup

More Decks by Brooklyn Swift Developers Meetup

Other Decks in Programming

Transcript

  1. Building separate apps for 2 or more of a company’s

    consumer brands. What is a multi-brand project?
  2. Build and release the first app as soon as possible

    in a classic architecture. Figure out the plan for the other brands later. Scenario 1 Single Codebase
  3. Brand Flag enum Brand { case cocaCola, sprite var current:

    Brand { #if COCA_BRAND return .cocaCola #elseif SPRITE_BRAND return .sprite #endif } }
  4. Factory Pattern Replace class constructors and abstract the process of

    object generation so that the type of the object instantiated can be determined at run-time.
  5. protocol Constants { var contactUsPhoneNumber: String { get } }

    struct CocaColaConstants: Constants { let contactUsPhoneNumber: String = "123-123-1234" } struct ConstantsFactory { static func makeConstants(forBrand brand: Brand) -> Constants { switch brand { case .cocaCola: return CocaColaConstants() case .sprite: return SpriteConstants() } } } let constants = ConstantsFactory.makeConstants(forBrand: .cocaCola) print(constants.contactUsPhoneNumber) // 123-123-1234
  6. protocol Constants { var contactUsPhoneNumber: String { get } }

    struct CocaColaConstants: Constants { let contactUsPhoneNumber: String = "123-123-1234" } struct ConstantsFactory { static func makeConstants(forBrand brand: Brand) -> Constants { switch brand { case .cocaCola: return CocaColaConstants() case .sprite: return SpriteConstants() } } } let constants = ConstantsFactory.makeConstants(forBrand: .cocaCola) print(constants.contactUsPhoneNumber) // 123-123-1234
  7. protocol Constants { var contactUsPhoneNumber: String { get } }

    struct CocaColaConstants: Constants { let contactUsPhoneNumber: String = "123-123-1234" } struct ConstantsFactory { static func makeConstants(forBrand brand: Brand) -> Constants { switch brand { case .cocaCola: return CocaColaConstants() case .sprite: return SpriteConstants() } } } let constants = ConstantsFactory.makeConstants(forBrand: .cocaCola) print(constants.contactUsPhoneNumber) // 123-123-1234
  8. protocol Constants { var contactUsPhoneNumber: String { get } }

    struct CocaColaConstants: Constants { let contactUsPhoneNumber: String = "123-123-1234" } struct ConstantsFactory { static func makeConstants(forBrand brand: Brand) -> Constants { switch brand { case .cocaCola: return CocaColaConstants() case .sprite: return SpriteConstants() } } } let constants = ConstantsFactory.makeConstants(forBrand: .cocaCola) print(constants.contactUsPhoneNumber) // 123-123-1234
  9. protocol Constants { var contactUsPhoneNumber: String { get } }

    struct CocaColaConstants: Constants { let contactUsPhoneNumber: String = "123-123-1234" } struct ConstantsFactory { static func makeConstants(forBrand brand: Brand) -> Constants { switch brand { case .cocaCola: return CocaColaConstants() case .sprite: return SpriteConstants() } } } let constants = ConstantsFactory.makeConstants(forBrand: .cocaCola) print(constants.contactUsPhoneNumber) // 123-123-1234
  10. Theme Share a common theme structure with font and color

    combinations represented in a grid format of rows and columns.
  11. // Font protocol FontSize { var xl: CGFloat { get

    } } protocol FontName { var medium: String { get } } protocol FontTheme { var fontSize: FontSize { get } var fontName: FontName { get } func mediumXL() -> FontAttributes } typealias FontAttributes = [String: Any] extension FontTheme { func mediumXL() -> FontAttributes { return [ NSFontAttributeName: UIFont(name: fontName.medium, size: fontSize.xl)!, NSKernAttributeName: 0.0 as NSObject ] } } // Color protocol ColorTheme { var primaryColor: UIColor { get } }
  12. // Font protocol FontSize { var xl: CGFloat { get

    } } protocol FontName { var medium: String { get } } protocol FontTheme { var fontSize: FontSize { get } var fontName: FontName { get } func mediumXL() -> FontAttributes } typealias FontAttributes = [String: Any] extension FontTheme { func mediumXL() -> FontAttributes { return [ NSFontAttributeName: UIFont(name: fontName.medium, size: fontSize.xl)!, NSKernAttributeName: 0.0 as NSObject ] } } // Color protocol ColorTheme { var primaryColor: UIColor { get } }
  13. // Font protocol FontSize { var xl: CGFloat { get

    } } protocol FontName { var medium: String { get } } protocol FontTheme { var fontSize: FontSize { get } var fontName: FontName { get } func mediumXL() -> FontAttributes } typealias FontAttributes = [String: Any] extension FontTheme { func mediumXL() -> FontAttributes { return [ NSFontAttributeName: UIFont(name: fontName.medium, size: fontSize.xl)!, NSKernAttributeName: 0.0 as NSObject ] } } // Color protocol ColorTheme { var primaryColor: UIColor { get } }
  14. // Font protocol FontSize { var xl: CGFloat { get

    } } protocol FontName { var medium: String { get } } protocol FontTheme { var fontSize: FontSize { get } var fontName: FontName { get } func mediumXL() -> FontAttributes } typealias FontAttributes = [String: Any] extension FontTheme { func mediumXL() -> FontAttributes { return [ NSFontAttributeName: UIFont(name: fontName.medium, size: fontSize.xl)!, NSKernAttributeName: 0.0 as NSObject ] } } // Color protocol ColorTheme { var primaryColor: UIColor { get } }
  15. // Font protocol FontSize { var xl: CGFloat { get

    } } protocol FontName { var medium: String { get } } protocol FontTheme { var fontSize: FontSize { get } var fontName: FontName { get } func mediumXL() -> FontAttributes } typealias FontAttributes = [String: Any] extension FontTheme { func mediumXL() -> FontAttributes { return [ NSFontAttributeName: UIFont(name: fontName.medium, size: fontSize.xl)!, NSKernAttributeName: 0.0 as NSObject ] } } // Color protocol ColorTheme { var primaryColor: UIColor { get } }
  16. // Font protocol FontSize { var xl: CGFloat { get

    } } protocol FontName { var medium: String { get } } protocol FontTheme { var fontSize: FontSize { get } var fontName: FontName { get } func mediumXL() -> FontAttributes } typealias FontAttributes = [String: Any] extension FontTheme { func mediumXL() -> FontAttributes { return [ NSFontAttributeName: UIFont(name: fontName.medium, size: fontSize.xl)!, NSKernAttributeName: 0.0 as NSObject ] } } // Color protocol ColorTheme { var primaryColor: UIColor { get } }
  17. // Grid enum Row { case r1 func font(theme: Theme)

    -> FontAttributes { switch self { case .r1: return theme.fontTheme.mediumXL() } } } enum Column { case c1 func color(theme: Theme) -> UIColor { switch self { case .c1: return theme.colorTheme.primaryColor } } }
  18. // Grid enum Row { case r1 func font(theme: Theme)

    -> FontAttributes { switch self { case .r1: return theme.fontTheme.mediumXL() } } } enum Column { case c1 func color(theme: Theme) -> UIColor { switch self { case .c1: return theme.colorTheme.primaryColor } } }
  19. // Grid enum Row { case r1 func font(theme: Theme)

    -> FontAttributes { switch self { case .r1: return theme.fontTheme.mediumXL() } } } enum Column { case c1 func color(theme: Theme) -> UIColor { switch self { case .c1: return theme.colorTheme.primaryColor } } }
  20. typealias FontAttributes = [String: Any] // Theme protocol Theme {

    var fontTheme: FontTheme { get } var colorTheme: ColorTheme { get } func style(row: Row, column: Column) -> FontAttributes } extension Theme { func style(row: Row, column: Column) -> FontAttributes { var font = row.font(theme: self) let color = column.color(theme: self) font[NSForegroundColorAttributeName] = color return font } }
  21. typealias FontAttributes = [String: Any] // Theme protocol Theme {

    var fontTheme: FontTheme { get } var colorTheme: ColorTheme { get } func style(row: Row, column: Column) -> FontAttributes } extension Theme { func style(row: Row, column: Column) -> FontAttributes { var font = row.font(theme: self) let color = column.color(theme: self) font[NSForegroundColorAttributeName] = color return font } }
  22. typealias FontAttributes = [String: Any] // Theme protocol Theme {

    var fontTheme: FontTheme { get } var colorTheme: ColorTheme { get } func style(row: Row, column: Column) -> FontAttributes } extension Theme { func style(row: Row, column: Column) -> FontAttributes { var font = row.font(theme: self) let color = column.color(theme: self) font[NSForegroundColorAttributeName] = color return font } }
  23. struct CocaColaFontSize: FontSize { let XL: CGFloat = 18 }

    struct CocaColaFontName: FontName { let medium: String = "Arial-BoldMT" } struct CocaColaFontTheme: FontTheme { let fontSize: FontSize = CocaColaFontSize() let fontName: FontName = CocaColaFontName() } struct CocaColaColorTheme: ColorTheme { var primaryColor: UIColor = UIColor.red } struct CocaColaTheme: Theme { var fontTheme: FontTheme = CocaColaFontTheme() var colorTheme: ColorTheme = CocaColaColorTheme() } struct TextRendered { static func render(text: String, attributes: FontAttributes) -> NSAttributedString { return NSAttributedString(string: text, attributes: attributes) } } let theme = ThemeFactory.makeTheme(forBrand: .cocaCola) let style = theme.style(row: .r1, column: .c1) let text = "Text to show" label.attributedText = TextRenderer.render(text: text, attributes: style)
  24. struct CocaColaFontSize: FontSize { let XL: CGFloat = 18 }

    struct CocaColaFontName: FontName { let medium: String = "Arial-BoldMT" } struct CocaColaFontTheme: FontTheme { let fontSize: FontSize = CocaColaFontSize() let fontName: FontName = CocaColaFontName() } struct CocaColaColorTheme: ColorTheme { var primaryColor: UIColor = UIColor.red } struct CocaColaTheme: Theme { var fontTheme: FontTheme = CocaColaFontTheme() var colorTheme: ColorTheme = CocaColaColorTheme() } struct TextRendered { static func render(text: String, attributes: FontAttributes) -> NSAttributedString { return NSAttributedString(string: text, attributes: attributes) } } let theme = ThemeFactory.makeTheme(forBrand: .cocaCola) let style = theme.style(row: .r1, column: .c1) let text = "Text to show" label.attributedText = TextRenderer.render(text: text, attributes: style)
  25. struct CocaColaFontSize: FontSize { let XL: CGFloat = 18 }

    struct CocaColaFontName: FontName { let medium: String = "Arial-BoldMT" } struct CocaColaFontTheme: FontTheme { let fontSize: FontSize = CocaColaFontSize() let fontName: FontName = CocaColaFontName() } struct CocaColaColorTheme: ColorTheme { var primaryColor: UIColor = UIColor.red } struct CocaColaTheme: Theme { var fontTheme: FontTheme = CocaColaFontTheme() var colorTheme: ColorTheme = CocaColaColorTheme() } struct TextRendered { static func render(text: String, attributes: FontAttributes) -> NSAttributedString { return NSAttributedString(string: text, attributes: attributes) } } let theme = ThemeFactory.makeTheme(forBrand: .cocaCola) let style = theme.style(row: .r1, column: .c1) let text = "Text to show" label.attributedText = TextRenderer.render(text: text, attributes: style)
  26. struct CocaColaFontSize: FontSize { let XL: CGFloat = 18 }

    struct CocaColaFontName: FontName { let medium: String = "Arial-BoldMT" } struct CocaColaFontTheme: FontTheme { let fontSize: FontSize = CocaColaFontSize() let fontName: FontName = CocaColaFontName() } struct CocaColaColorTheme: ColorTheme { var primaryColor: UIColor = UIColor.red } struct CocaColaTheme: Theme { var fontTheme: FontTheme = CocaColaFontTheme() var colorTheme: ColorTheme = CocaColaColorTheme() } struct TextRendered { static func render(text: String, attributes: FontAttributes) -> NSAttributedString { return NSAttributedString(string: text, attributes: attributes) } } let theme = ThemeFactory.makeTheme(forBrand: .cocaCola) let style = theme.style(row: .r1, column: .c1) let text = "Text to show" label.attributedText = TextRenderer.render(text: text, attributes: style)
  27. struct CocaColaFontSize: FontSize { let XL: CGFloat = 18 }

    struct CocaColaFontName: FontName { let medium: String = "Arial-BoldMT" } struct CocaColaFontTheme: FontTheme { let fontSize: FontSize = CocaColaFontSize() let fontName: FontName = CocaColaFontName() } struct CocaColaColorTheme: ColorTheme { var primaryColor: UIColor = UIColor.red } struct CocaColaTheme: Theme { var fontTheme: FontTheme = CocaColaFontTheme() var colorTheme: ColorTheme = CocaColaColorTheme() } struct TextRendered { static func render(text: String, attributes: FontAttributes) -> NSAttributedString { return NSAttributedString(string: text, attributes: attributes) } } let theme = ThemeFactory.makeTheme(forBrand: .cocaCola) let style = theme.style(row: .r1, column: .c1) let text = "Text to show" label.attributedText = TextRenderer.render(text: text, attributes: style)
  28. struct CocaColaFontSize: FontSize { let XL: CGFloat = 18 }

    struct CocaColaFontName: FontName { let medium: String = "Arial-BoldMT" } struct CocaColaFontTheme: FontTheme { let fontSize: FontSize = CocaColaFontSize() let fontName: FontName = CocaColaFontName() } struct CocaColaColorTheme: ColorTheme { var primaryColor: UIColor = UIColor.red } struct CocaColaTheme: Theme { var fontTheme: FontTheme = CocaColaFontTheme() var colorTheme: ColorTheme = CocaColaColorTheme() } struct TextRendered { static func render(text: String, attributes: FontAttributes) -> NSAttributedString { return NSAttributedString(string: text, attributes: attributes) } } let theme = ThemeFactory.makeTheme(forBrand: .cocaCola) let style = theme.style(row: .r1, column: .c1) let text = "Text to show" label.attributedText = TextRenderer.render(text: text, attributes: style)
  29. struct CocaColaFontSize: FontSize { let XL: CGFloat = 18 }

    struct CocaColaFontName: FontName { let medium: String = "Arial-BoldMT" } struct CocaColaFontTheme: FontTheme { let fontSize: FontSize = CocaColaFontSize() let fontName: FontName = CocaColaFontName() } struct CocaColaColorTheme: ColorTheme { var primaryColor: UIColor = UIColor.red } struct CocaColaTheme: Theme { var fontTheme: FontTheme = CocaColaFontTheme() var colorTheme: ColorTheme = CocaColaColorTheme() } struct TextRendered { static func render(text: String, attributes: FontAttributes) -> NSAttributedString { return NSAttributedString(string: text, attributes: attributes) } } let theme = ThemeFactory.makeTheme(forBrand: .cocaCola) let style = theme.style(row: .r1, column: .c1) let text = "Text to show" label.attributedText = TextRenderer.render(text: text, attributes: style)
  30. struct CocaColaFontSize: FontSize { let XL: CGFloat = 18 }

    struct CocaColaFontName: FontName { let medium: String = "Arial-BoldMT" } struct CocaColaFontTheme: FontTheme { let fontSize: FontSize = CocaColaFontSize() let fontName: FontName = CocaColaFontName() } struct CocaColaColorTheme: ColorTheme { var primaryColor: UIColor = UIColor.red } struct CocaColaTheme: Theme { var fontTheme: FontTheme = CocaColaFontTheme() var colorTheme: ColorTheme = CocaColaColorTheme() } struct TextRendered { static func render(text: String, attributes: FontAttributes) -> NSAttributedString { return NSAttributedString(string: text, attributes: attributes) } } let theme = ThemeFactory.makeTheme(forBrand: .cocaCola) let style = theme.style(row: .r1, column: .c1) let text = "Text to show" label.attributedText = TextRenderer.render(text: text, attributes: style)
  31. Reusability • Create protocols to define reusable code • Use

    factories to abstract object creation • Use a shared language between designers and engineers
  32. Extra Tips • Create one localizable strings file per target

    • Create one assets folder per target • Write tests before refactoring
  33. What We Learned • It’s never too late to refactor

    • The simpler, the better • Perceive the need
  34. Build and release the first app with the right architecture

    to support multiple brands from the get-go. Scenario 2 Multiple Codebases
  35. B-VIPER (Builder, View, Interactor, Presenter, Entity, Router) is a popular

    iOS architecture that divides an app’s logical structure into distinct layers of responsibility. B-VIPER
  36. Builders Builders have an essential role in the architecture as

    they provide a reusable interface to access to a VIPER stack. /// Bag VIPER builder func build() -> UINavigationController? /// Category VIPER builder func build(categoryId categoryId: String) -> UIViewController?
  37. B-VIPER VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER PRODUCT DETAIL

    VIEW PRODUCT ARRAY HOME SCREEN CHECKOUT ACCOUNT VIEW … VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER
  38. B-VIPER CHECKOUT SHIPPING BILLING PAYMENT Sub Modules VIEW PRESENTER INTERACTOR

    ENTITY ENTITY ROUTER BUILDER VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER VIEW PRESENTER INTERACTOR ENTITY ENTITY ROUTER BUILDER
  39. SDK Build an SDK to be the brain of all

    your business logic and define the default VIPER stacks. COCA COLA SDK … SPRITE
  40. public protocol ProductInteractor { func findProduct(productId: String) func addToBag(selectedSKU: SKU,

    productCode: String) func availableSizes(product: Product) -> [Size] } SDK Protocol Oriented Programming
  41. public protocol ProductDataManagerInterface: DataManagerInterface { func product<T: Product>(productId: String, completion:

    Result<T, Error> -> Void) } public extension ProductDataManagerInterface { func product<T: Product>(productId: String, completion: Result<T, Error> -> Void) { let productURL = productEndpoint.url(productId) NetworkingManager.request(.GET, urlString: productURL, parameters: nil) { [weak self] (result) -> () in self?.handleProductCompletion(result, completion: completion) } } }
  42. public protocol ProductDataManagerInterface: DataManagerInterface { func product<T: Product>(productId: String, completion:

    Result<T, Error> -> Void) } public extension ProductDataManagerInterface { func product<T: Product>(productId: String, completion: Result<T, Error> -> Void) { let productURL = productEndpoint.url(productId) NetworkingManager.request(.GET, urlString: productURL, parameters: nil) { [weak self] (result) -> () in self?.handleProductCompletion(result, completion: completion) } } }
  43. public protocol ProductDataManagerInterface: DataManagerInterface { func product<T: Product>(productId: String, completion:

    Result<T, Error> -> Void) } public extension ProductDataManagerInterface { func product<T: Product>(productId: String, completion: Result<T, Error> -> Void) { let productURL = productEndpoint.url(productId) NetworkingManager.request(.GET, urlString: productURL, parameters: nil) { [weak self] (result) -> () in self?.handleProductCompletion(result, completion: completion) } } }
  44. SDK public protocol DependencyManagerInterface { func accountBuilder() -> AccountBuilderInterface func

    productBuilder() -> ProductBuilderInterface func bagBuilder() -> BagBuilderInterface func categoryBuilder() -> CategoryBuilderInterface . . . } internal final class DependencyManager: DependencyManagerInterface { func accountBuilder() -> AccountBuilderInterface { return AccountBuilder(dependencyManager: self) } . . . } Dependency Manager
  45. SDK public protocol DependencyManagerInterface { func accountBuilder() -> AccountBuilderInterface func

    productBuilder() -> ProductBuilderInterface func bagBuilder() -> BagBuilderInterface func categoryBuilder() -> CategoryBuilderInterface . . . } internal final class DependencyManager: DependencyManagerInterface { func accountBuilder() -> AccountBuilderInterface { return AccountBuilder(dependencyManager: self) } . . . } Dependency Manager
  46. SDK public protocol DependencyManagerInterface { func accountBuilder() -> AccountBuilderInterface func

    productBuilder() -> ProductBuilderInterface func bagBuilder() -> BagBuilderInterface func categoryBuilder() -> CategoryBuilderInterface . . . } internal final class DependencyManager: DependencyManagerInterface { func accountBuilder() -> AccountBuilderInterface { return AccountBuilder(dependencyManager: self) } . . . } Dependency Manager
  47. Reusability • Create builders to have modular code • Code

    everything under dependency injection • Implement VIPER to reuse code in a flexible way at a layer or stack level
  48. What we learned • B-VIPER is a very powerful architecture

    • Onboarding was not easy • Hard to anticipate the right amount of flexibility required
  49. Scenario 1 vs scenario 2 • Pros and cons of

    each scenarios Scenario 1 Single codebase • Simple code • Easy onboarding • Less flexibility for custom features • Every custom code will end up being messy Scenario 2 Multiple codebases • A lot of flexibility and modularity • Granular scalability • Complexity in the architecture • A lot of code updates required at every change
  50. What you need to remember • Abstraction through Protocol Oriented

    Programming • Modularity through Builders • Flexibility with Dependency Injection
  51. Thank you. Brooklyn 77 Sands St, 10th Floor Brooklyn, NY

    11201 347.462.0990 San Francisco 995 Market St, 14th Floor San Francisco, CA 94103 415.813.4199