Vicnent @damaofficial @vincz a · Creating new feature - Demo application What language do you want...

Preview:

Citation preview

Dama@damaofficial

Vicnent@vincz_a

Architecture & Automation:

How development processeswork at N26

Contents

1. Isolation on the code level2. Isolation on the project level3. Delivery

Features

Features

● Real bank account

● Real bank account● Opened in under 8 minutes

Features

● Real bank account● Opened in under 8 minutes● Video identification

Features

● Real bank account● Opened in under 8 minutes● Video identification● Mobile first

Features

Features

● Real bank account● Opened in under 8 minutes● Video identification● Mobile first● Realtime (Money transfers, push notifications, card block/unblock)

Features

● Real bank account● Opened in under 8 minutes● Video identification● Mobile first● Realtime (Money transfers, push notifications, card block/unblock)● Financial products (Overdraft, N26 Invest, N26 Credit)

Features

● Real bank account● Opened in under 8 minutes● Video identification● Mobile first● Realtime (Money transfers, push notifications, card block/unblock)● Financial products (Overdraft, N26 Invest, N26 Credit)● ...

N26 - Banking by design

● >100 MB

N26 - Banking by design

● >100 MB● > 250k users across platforms

N26 - Banking by design

● >100 MB● > 250k users across platforms● Tons of features

2014

History of the projectIsolation on the code level

History of the projectIsolation on the code level

History of the projectIsolation on the code level

History of the projectIsolation on the code level

Legacy

Legacy

1. Boostraping and iterating quickly over the product2. Understaffed team3. Developing feature like a hackaton

Isolation on the code level

Dealing with legacyIsolation on the code level

Dealing with legacy

● Isolate as much as possible the legacy and continue with product development

Isolation on the code level

Dealing with legacy

● Isolate as much as possible the legacy and continue with product development

● Refactor as much as you can

Isolation on the code level

● Isolate as much as possible the legacy and continue with product development

● Refactor as much as you can ● Carry on and try to make the best out of it

Dealing with legacyIsolation on the code level

Dealing with legacy

● Isolate as much as possible the legacy and continue with product development

● Refactor as much as you can ● Carry on and try to make the best out of it

Isolation on the code level

Isolate legacy code and continue development

1. Refactor only when needed2. If you don’t touch it you don’t break it!3. Pick up an efficient design pattern to deal with legacy

Isolation on the code level

VIPER

https://www.objc.io/issues/13-architecture/viper/

Wireframe

View Presenter Interactor

Data Store

Entity

Entity

Isolation on the code level

Wireframe

https://www.objc.io/issues/13-architecture/viper/

Wireframe

View Presenter Interactor

Data Store

Entity

Entity

Isolation on the code level

Presenter

https://www.objc.io/issues/13-architecture/viper/

Wireframe

View Presenter Interactor

Data Store

Entity

Entity

Isolation on the code level

View

https://www.objc.io/issues/13-architecture/viper/

Wireframe

View Presenter Interactor

Data Store

Entity

Entity

Isolation on the code level

Interactor

https://www.objc.io/issues/13-architecture/viper/

Wireframe

View Presenter Interactor

Data Store

Entity

Entity

Isolation on the code level

Isolating your views

Wireframe

View Presenter Interactor

Isolation on the code level

Isolating your views

Wireframe

View Presenter Interactor

Black Box

Isolation on the code level

Isolating your views

Wireframe

View

Black Box

Isolation on the code level

Isolating your views

Input something

Output somethingelse

Wireframe

Isolation on the code level

Presenting a view using Wireframe

/**

Present Delete Contact formular

*/

func presentDeleteContact(_ contact: TransferContact, completion: @escaping (_ deleted: Bool) -> Void) { DeleteContactWireframe.present(from: self.navigationController, with: contact, completion: completion) }

DeleteContactWireframe - Dummy implementation

class DeleteContactWireframe { /// Present the delete contact formular /// /// - Parameters: /// - viewController: ViewController from where the view should be displayed /// - contact: The contact to be deleted /// - completion: Completion block, returns true if contact has been deleted static func present(from viewController: UIViewController, with contact: Contact, completion: (_ deleted: Bool) -> Void) { let view = DeleteContactViewController() // Create the view let interactor = DeleteContactInteractor() // Create the Interactor let presenter = DeleteContactPresenter(view: view, interactor: interactor) // Create the presenter view.delegate = presenter // Set view delegate viewController.present(view, animated: true, completion: nil) }}

Isolating features

Wireframe

Black box

Wireframe

Black box

Wireframe

Black box

...

Isolation on the project level

Short-term

ObjC

Isolation on the project level

Long-term

ObjC

Isolation on the project level

Build timeIsolation on the project level

Xcode bugIsolation on the project level

Xcode bugsIsolation on the project level

Problem

https://bugs.swift.org/browse/SR-2461

Isolation on the project level

Problem

https://bugs.swift.org/browse/SR-2461

Xcode 8.3

Isolation on the project level

ProblemIsolation on the project level

Solution

Framework

Isolation on the project level

Module Module

Modules Module

Isolation on the project level

Core

Core

@interface NSString (IBANFormat)

/// Returns a formatted string in groups of 4 characters

separated by a space

- (NSString * _Nonnull)IBANFormattedString;

@end

GraphicsReusable UI components

Networking

public protocol CardService {

/// Fetches cards for the current user

func cards(_ success: @escaping ([Card]) -> Void,

failure: @escaping (Error) -> Void)

}

Data VisualizationCustom drawing

# Specify the private specs repo

source 'https://github.com/owner/Specs.git'

...

# Add private dependencies

pod 'N26Core'

pod 'N26Graphics'

pod 'N26Networking'

pod 'N26DataVisualization'

...

CocoaPods

N26 App

Setup

Data visualization

Core Graphics

Networking

Isolation on the project level

N26 App

Setup

Data visualization

Core_ObjC

Graphics

Networking

Core

Isolation on the project level

Core

Networking

Data visualization

Graphics

Remote hosted modules drawbacksIsolation on the project level

Core

Networking

Data visualization

Graphics

Remote hosted modules drawbacksIsolation on the project level

Core

Networking

Data visualization

Graphics

Remote hosted modules drawbacksIsolation on the project level

Core

Networking

Data visualization

Graphics

Remote hosted modules drawbacksIsolation on the project level

Self hosted modules

Core

Networking

Data visualization

Graphics

Core

Networking

Data visualization

Graphics feature/credit

feature/invest

develop

Isolation on the project level

Current state

● Splitted the app into modules● Modules are now locally hosted● We’re still missing something …

Isolation on the project level

Missing moduleIsolation on the project level

Missing module

We’re missing a module that would

● Handle user sessions● Handle Login and access token● Cache current user data

Isolation on the project level

N26Session

import N26Session

Session.current.start

N26Session, booting up

import N26Session

Session.current.start(with: login, password: password, success: { data in

N26Session, booting up

N26Session, booting up

import N26Session

Session.current.start(with: login, password: password, success: { data in /// User is logged in

}) { error in /// Whatever error happend (Bad credentials, 500 ... )}

N26Session, booting up

import N26Session

Session.current.start(with: login, password: password, success: { data in /// User is logged in

print(Session.current.firstName) // Print the current user name print(Session.current.availableBalance) // Print the current account Balance

}) { error in /// Whatever error happend (Bad credentials, 500 ... )}

Feature-developed modulesIsolation on the project level

Generic Module architecture

Core

Network layer

User session

Graphic library

Tracking tool ...

Feature 1 Feature 2 ... ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Generic Module architecture

Core

Network layer

User session

Graphic library

Tracking tool ...

Feature 1 Feature 2 ... ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Generic Module architecture

Core

Network layer

User session

Graphic library

Tracking tool ...

Feature 1 Feature 2 ... ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Generic Module architecture

Core

Network layer

User session

Graphic library

Tracking tool ...

Feature 1 Feature 2 ... ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Generic Module architecture

Core

Network layer

User session

Graphic library

Tracking tool ...

Feature 1 Feature 2 ... ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Generic Module architecture

Core

Network layer

User session

Graphic library

Tracking tool ...

Feature 1 Feature 2 ... ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Generic Module architecture

Core

Network layer

Graphic library

Tracking tool ...

Feature 1 Feature 2 ... ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Generic Module architecture

Core

Network layer

User session

Graphic library

Tracking tool ...

Feature 1 Feature 2 ... ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Our Module architecture

Nucleus

Networking Session Dali Tracker Polyglot

Credit Invest Transactor ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

Our Module architecture

Nucleus

Networking Session Dali Tracker ...

Credit Invest Transactor ...

Tier 0

Tier 1

Tier 2

Isolation on the project level

App Extensions done with modulesIsolation on the project level

Apple Watch

Today Widget

Siri

iMessage

N26 - Podfile

# ...

target 'SiriKit' do

pod 'N26Nucleus', :path => 'N26Modules/N26Nucleus'

pod 'N26Networking', :path => 'N26Modules/N26Networking'

pod 'N26Session', :path => 'N26Modules/N26Session'

end

# ..

func widgetPerformUpdate(completionHandler: @escaping (NCUpdateResult) -> Void) { if Session.current.loggedIn {

// user is already logged in Session.current.syncUserData({ (data, error) in

// Refresh the user data if let _ = error {

// display error completionHandler(.failed) } else {

// Refresh the widget self.refreshDisplayData(completionHandler) } }) } else {

// User is not logged in, use the refresh token Session.current.startUsingStoredData({ (data) in self.refreshDisplayData(completionHandler) }, failure: { (refreshTokenExpired, error) in self.displayNeedToAuthenticate() completionHandler(.failed) }) }

}

Creating new feature

$ pod lib create N26NewSecretFeature

Creating new feature - Demo application

What language do you want to use?? [ Swift / ObjC ]> Swift

Would you like to include a demo application with your library? [ Yes / No ] > Yes

Which testing frameworks will you use? [ Quick / None ] > None

Creating new feature - Demo application

What language do you want to use?? [ Swift / ObjC ]> Swift

Would you like to include a demo application with your library? [ Yes / No ] > Yes

Which testing frameworks will you use? [ Quick / None ] > None

Pod::Spec.new do |s| s.name = 'N26NewSecretFeature' s.version = '0.1.0' s.summary = 'A short description of N26NewSecretFeature.'

s.ios.deployment_target = '9.0'

s.source_files = 'N26NewSecretFeature/Classes/**/*'

N26NewSecretFeature.podspec

Pod::Spec.new do |s| s.name = 'N26NewSecretFeature' s.version = '0.1.0' s.summary = 'A short description of N26NewSecretFeature.'

s.ios.deployment_target = '9.0'

s.source_files = 'N26NewSecretFeature/Classes/**/*'

s.dependency 'N26Nucleus'

N26NewSecretFeature.podspec

Pod::Spec.new do |s| s.name = 'N26NewSecretFeature' s.version = '0.1.0' s.summary = 'A short description of N26NewSecretFeature.'

s.ios.deployment_target = '9.0'

s.source_files = 'N26NewSecretFeature/Classes/**/*'

s.dependency 'N26Nucleus' s.dependency 'N26Session'

N26NewSecretFeature.podspec

Pod::Spec.new do |s| s.name = 'N26NewSecretFeature' s.version = '0.1.0' s.summary = 'A short description of N26NewSecretFeature.'

s.ios.deployment_target = '9.0'

s.source_files = 'N26NewSecretFeature/Classes/**/*'

s.dependency 'N26Nucleus' s.dependency 'N26Session' s.dependency 'N26Dali'

N26NewSecretFeature.podspec

N26NewSecretFeature_Example Podfile

use_frameworks!

target 'N26NewSecretFeature_Example' do pod 'N26NewSecretFeature', :path => '../'

target 'N26NewSecretFeature_Tests' do inherit! :search_paths endend

N26NewSecretFeature_Example Podfile

use_frameworks!

target 'N26NewSecretFeature_Example' do pod 'N26Dali', :path => '../../N26Dali' pod 'N26NewSecretFeature', :path => '../'

target 'N26NewSecretFeature_Tests' do inherit! :search_paths endend

N26NewSecretFeature_Example Podfile

use_frameworks!

target 'N26NewSecretFeature_Example' do pod 'N26Dali', :path => '../../N26Dali' pod 'N26Session', :path => '../../N26Session' pod 'N26NewSecretFeature', :path => '../'

target 'N26NewSecretFeature_Tests' do inherit! :search_paths endend

N26NewSecretFeature_Example Podfile

use_frameworks!

target 'N26NewSecretFeature_Example' do pod 'N26Dali', :path => '../../N26Dali' pod 'N26Session', :path => '../../N26Session' pod 'N26Nucleus', :path => '../../N26Nucleus' pod 'N26N26NewSecretFeature', :path => '../'

target 'N26NewSecretFeature_Tests' do inherit! :search_paths endend

Pod install on example app

~/$ cd Example

~/Example$ pod install

NewFeature - Entry point

// NewFeature entry pointpublic class NewFeature {

// Initialize the NewFeature environment and display it public static func start(on viewController: UIViewController) { //TODO: implement this feature }

}

NewFeature - Entry point

import N26Nucleusimport N26Session

// NewFeature entry pointpublic class NewFeature { // Initialize the NewFeature environment and display it public static func start(on viewController: UIViewController) { // Print user firstName print(Session.current.firstName) // Print users formatted IBAN print(Session.current.iban.IBANFormattedString()) }}

Example Project - ViewController.swift

import N26NewSecretFeature

class ViewController: UIViewController {

override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated)

// Kickoff the new feature NewSecretFeature.start(on: self) }

}

DownsidesIsolation on the project level

Downsides

● Running pod update

Isolation on the project level

Downsides

● Running pod update● Conflicts and clogged pull requests

Isolation on the project level

Downsides

● Running pod update● Conflicts and clogged pull requests● Big repo

Isolation on the project level

Downsides

● Running pod update● Conflicts and clogged pull requests● Big repo

Nucleus

formating library

Isolation on the project level

Downsides

● Running pod update● Conflicts and clogged pull requests● Big repo

Nucleus

formating library

formating library

Nucleus Example

Isolation on the project level

Downsides

● Running pod update● Conflicts and clogged pull requests● Big repo

Nucleus

Credit

formating library

formating library

formating library

Nucleus Example

Credit Example

Isolation on the project level

● Running pod update● Conflicts and clogged pull requests● Big repo

Downsides

Nucleus

Credit

formating library

formating library

formating library

Nucleus Example

Credit Example

Isolation on the project level

● Running pod update● Conflicts and clogged pull requests● Big repo● Multiple compilations of dependencies

when testing

Downsides

Nucleus

Credit

formating library

formating library

formating library

Nucleus Example

Credit Example

Isolation on the project level

● Running pod update● Conflicts and clogged pull requests● Big repo● Multiple compilations of dependencies

when testing

Downsides

Nucleus

Credit

formating library

formating library

formating library

Nucleus Example

Credit Example

Isolation on the project level

● Running pod update● Conflicts and clogged pull requests● Big repo● Multiple compilations of dependencies

when testing

Downsides

Nucleus

Credit

formating library

formating library

formating library

Nucleus Example

Credit Example

Isolation on the project level

Downsides

● Running pod update● Conflicts and clogged pull requests● Big repo● Multiple compilations of dependencies

when testing● Changes break other modules

Isolation on the project level

Internal CI

✚Mac Mini Jenkins

Delivery

Internal CI Downsides

● New version of iOS (Updating Jenkins)

Delivery

Internal CI Downsides

● New version of iOS (Updating Jenkins)● No public IP

Delivery

Internal CI Downsides

● New version of iOS (Updating Jenkins)● No public IP● Not scalable

Delivery

Internal CI Downsides

● New version of iOS● No public IP● Not scalable● …

Delivery

Paid solutions to the rescue

Travis CI

Delivery

Paid solutions to the rescue

✚Travis CI Fastlane

Delivery

Paid solutions to the rescue

✚ ✚Travis CI Fastlane Bob

Delivery

Bob The Builder to the rescue

● Travis CI● Fastlane● Slack + Bob The Builder

Delivery

Bob The Builder - Stack

● Slack● Swift● Vapor (Server swift) ● Communicate to Slack via Sockets

Delivery

Slack Bob Travis

Build staging

Slack Bob Travis

Processing

Slack Bob Travis

Build staging

Slack Bob Travis

Done!

Slack Bob Travis

Bob in action

Bob

Open sourcehttps://github.com/N26-OpenSource/bob

Delivery

build staging

“If your build takes more than pressing a button, you’re doing it wrong”

-Someone

align 3.3 1

sync strings | align 3.3 1 | build staging | build appstore

Making an RCDelivery

https://github.com/N26-OpenSource/bob

BobDelivery

$ curl -sL toolbox.qutheory.io | bash

Getting the vapor toolbox

$ vapor new BobTheBuilder

$ cd BobTheBuilder

Creating a new vapor project

Package.swift

Package.swift

import PackageDescription

let package = Package(

name: "BobTheBuilder",

dependencies: [

.Package(url:

"https://github.com/N26-OpenSource/bob.git", majorVersion: 0)

]

)

$ rm -rf Sources/App/Controllers

$ rm -rf Sources/App/Models

Tidying up

$ vapor xcode

Creating an Xcode project

main.swift

main.swift

import Bob

/// Create the config using a slack token

let config = Bob.Configuration(slackToken: "your-slack-token")

main.swift

import Bob

/// Create the config using a slack token

let config = Bob.Configuration(slackToken: "your-slack-token")

/// Create bob instance

let bob = Bob(config: config)

main.swift

import Bob

/// Create the config using a slack token

let config = Bob.Configuration(slackToken: "your-slack-token")

/// Create bob instance

let bob = Bob(config: config)

/// Start bob up

try bob.start()

Using the TravisScriptCommand/// Create TravisCI config

let travisConfig = TravisCI.Configuration(repoUrl: “repo url”, token: “token”)

Using the TravisScriptCommand/// Create TravisCI config

let travisConfig = TravisCI.Configuration(repoUrl: “repo url”, token: “token”)

/// Specify targets

let buildTargets = [

TravisTarget(name: "staging", script: Script("fastlane ios distribute_staging")),

TravisTarget(name: "appstore", script: Script("fastlane ios distribute_appstore")),

]

Using the TravisScriptCommand/// Create TravisCI config

let travisConfig = TravisCI.Configuration(repoUrl: “repo url”, token: “token”)

/// Specify targets

let buildTargets = [

TravisTarget(name: "staging", script: Script("fastlane ios distribute_staging")),

TravisTarget(name: "appstore", script: Script("fastlane ios distribute_appstore")),

]

/// Create the build command

let buildCommand = TravisScriptCommand(name: "build", config: travisConfig, targets: buildTargets,

defaultBranch: "Develop")

Using the TravisScriptCommand/// Create TravisCI config

let travisConfig = TravisCI.Configuration(repoUrl: “repo url”, token: “token”)

/// Specify targets

let buildTargets = [

TravisTarget(name: "staging", script: Script("fastlane ios distribute_staging")),

TravisTarget(name: "appstore", script: Script("fastlane ios distribute_appstore")),

]

/// Create the build command

let buildCommand = TravisScriptCommand(name: "build", config: travisConfig, targets: buildTargets,

defaultBranch: "Develop")

/// Register the command with bob

try bob.register(buildCommand)

Using the AlignVersionCommand

/// Create GitHub config

let gitHubConfig = GitHub.Configuration(username: "username",

personalAccessToken: "token", repoUrl: "url")

Using the AlignVersionCommand

/// Create GitHub config

let gitHubConfig = GitHub.Configuration(username: "username",

personalAccessToken: "token", repoUrl: "url")

/// Specify .plist file to be changed

let plistPaths: [String] = ["App/Info.plist", "siriKit/Info.plist"]

Using the AlignVersionCommand

/// Create GitHub config

let gitHubConfig = GitHub.Configuration(username: "username",

personalAccessToken: "token", repoUrl: "url")

/// Specify .plist file to be changed

let plistPaths: [String] = ["App/Info.plist", "siriKit/Info.plist"]

/// Create the command

let alignCommand = AlignVersionCommand(config: gitHubConfig,

defaultBranch: "Develop", plistPaths: plistPaths, author: author)

Using the AlignVersionCommand

/// Create GitHub config

let gitHubConfig = GitHub.Configuration(username: "username",

personalAccessToken: "token", repoUrl: "url")

/// Specify .plist file to be changed

let plistPaths: [String] = ["App/Info.plist", "siriKit/Info.plist"]

/// Create the command

let alignCommand = AlignVersionCommand(config: gitHubConfig,

defaultBranch: "Develop", plistPaths: plistPaths, author: author)

/// Register the command with bob

try bob.register(alignCommand)

Creating your commandspublic protocol Command {

}

Creating your commandspublic protocol Command { /// The name used to identify a command (`hello`, `version` etc.). Case insensitive var name: String { get }

}

Creating your commandspublic protocol Command { /// The name used to identify a command (`hello`, `version` etc.). Case insensitive var name: String { get } /// String describing how to use the command. var usage: String { get }

}

Creating your commandspublic protocol Command { /// The name used to identify a command (`hello`, `version` etc.). Case insensitive var name: String { get } /// String describing how to use the command. var usage: String { get } /// Executes the command /// /// - Parameters: /// - parameters: parameters passed to the command /// - sender: object used to send feedback to the user /// - completion: block to be called when the command finishes. In case of an error, pass it in /// - Throws: Throws if something goes wrong while executing the command, usually while parsing the parameters func execute(with parameters: [String], replyingTo sender: MessageSender, completion: @escaping (_ error: Error?) -> Void) throws }

BobDelivery

Lessons learned on scaling

● Split your code into smaller pieces● Continuous integration is essential● Module will save you time● …● Try out Bob!

Appendix

● Architecting iOS Apps with VIPERhttps://www.objc.io/issues/13-architecture/viper/

● Bobhttps://github.com/N26-OpenSource/bob

● Vapor (Web Framework For Swift)https://vapor.codes/

● CocoaPodshttps://cocoapods.org/

● Travis CIhttps://travis-ci.com/

Thanks

Dama@damaofficial

Vicnent@vincz_a

Bobgithub.com/N26-OpenSource/bob

Recommended