Refactor Your
Way Forward
Jorge D. Ortiz-Fuentes
@jdortiz
A Canonical

Examples

Production
#AdvArchMobile
Agenda
★ The challenge
★ Strategy
★ Tactics
★ Recap
The
Challenge
#AdvArchMobile
Sounds familiar?
★ Legacy application
• No tests
• Outdated
★ Written in Objective-C
★ Not (m)any tests
★ Multiple styles and ways to do things
★ Not much info from the previous developer
#AdvArchMobile
Need a Better
Architecture
★ Difficult to add new features without
breaking existing ones
★ Difficult to find and solve bugs
★ Expensive to maintain
★ Difficult to add tests
#AdvArchMobile
My Example
★ App: OpenIt
★ Credit: Patrick Balestra
★ Thanks!
★ Great code for an example
★ All criticism IS constructive
Strategy
Ideas to Enhance
Persistance FW
View
Network
LocationFW
Presenter
Entity Gateway
Clean Architecture
Interactor
Entity
Clean Architecture: iOS
App
Delegate
View (VC) Presenter Interactor
Entity
Gateway
Connector
#AdvArchMobile
Goals
★ New feature: Apple rating API
★ Don’t break anything
★ Enhance when possible
No Big Bang Rewrite
Still love the TV series
Information
Gathering
– Sun Tzu
“Know your enemy and know yourself and
you can fight a hundred battles without
disaster.”
Pragmatic Information
Gathering
Make it Work
#AdvArchMobile
Make it Work
★ Install dependencies
• Pods/Carthage, if any
• API keys
★ Build
★ Fix big problems until it works
#AdvArchMobile
Make it Work
★ DON’T update the project settings yet
★ DON’T add any functionality yet
★ DON’T fix any other bugs yet
Commit!
#AdvArchMobile
Explore the Battlefield
★ Take a glimpse a the code
★ Tests? Runnable? Pass?
★ Documentation?
★ Oral transmission?
★ Business goals
#AdvArchMobile
Design your strategy
★ Planed features
★ Pain points
#AdvArchMobile
Main Strategic
Approaches
★ From the model upwards
• Less intuitive
• Still requires injections from top to bottom
★ From the views inwards
• More work initially
#AdvArchMobile
Shape the Strategy
★ App delegate: Size? Relevant tasks? Easy to
replace (And remove main.m)?
★ Storyboards?
★ Tests coverage? Only for the model?
★ Frameworks used (Persistence & others)?
★ DI? Abstractions for DIP?
★ VCs screaming for changes?
But remember
Only small non breaking changes allowed
Tactics
Add Tests
#AdvArchMobile
Add Tests
★ Set up the testing target
★ Language is Swift
★ Start with main target
★ Don't add others (frameworks) until required
★ Cmd+U To test that it works.
★ Delete the default tests
Commit!
Zero Visibility
Here Be Dragons
Use the Tools
#AdvArchMobile
Proper Git
★ Branch often
★ Better, git flow: Feature branch for each
part of migration
★ Avoid long lived branches
★ Use branch by abstraction instead
#AdvArchMobile
Branch by Abstraction
2 1
1
1 2
#AdvArchMobile
Use Xcode Groups
★ Put all legacy code in a group
★ Support files and assets in another one
★ Create new Groups (or folders) to organize
new code
Use Xcode Refactor
Feature
😂
Replace App
Delegate (& main.m)
#AdvArchMobile
Very Simple (when it is)
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
}
Commit!
Enable Dependency
Injection
#AdvArchMobile
Introduce DI
from Root VC
★ Change behavior in Info.plist
★ App delegate creates initial view controller
★ Pass into a dumb (yet) connector
★ Add Bridging header
#AdvArchMobile
Info.plist
Commit!
View Controller Talks
to Dumb Presenter
Mark Connection Points
#AdvArchMobile
Introduce Presenter
★ @objc
★ Pass events
★ Test VC
★ Generate a skeleton for the presenter
#AdvArchMobile
Cheat to keep it working
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
// DONE: Invoke presenter numberOfActions
NSInteger rows = self.presenter.numberOfActions;
// TODO: Remove when using real presenter
if (rows < 0) {
rows = self.actions.count;
}
return rows;
}
#AdvArchMobile
Cheat to provide
dependencies
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// DONE: Invoke presenter configure(cell:forRow:)
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
// TODO: In the new view replace with the actual cell
[self.presenter configureWithCell:[[ActionTableViewCell alloc]
init] atRow:indexPath.row];
cell.textLabel.text = self.actions[indexPath.row][0][@"Title"];
cell.detailTextLabel.text = self.actions[indexPath.row][1]
[@"Type"];
cell.imageView.image = [UIImage
imageNamed:self.actions[indexPath.row][1][@"Type"]];
return cell;
}
Commit!
Refactor Persistence
#AdvArchMobile
- (void)viewDidLoad {
[super viewDidLoad];
[self.presenter viewReady];
// …
self.actions = [self
fetchActions];
// …
}
- (NSMutableArray *) fetchActions
{
return [[NSMutableArray alloc]
initWithArray:[[NSUserDefaults
standardUserDefaults]
objectForKey:@"actions"]];
}
Extract Methods with
Persistence
- (void)viewDidLoad {
[super viewDidLoad];
[self.presenter
viewReady];
// …
self.actions =
[[NSMutableArray alloc]
initWithArray:
[[NSUserDefaults
standardUserDefaults]
objectForKey:@"actions"]];
// …
}
#AdvArchMobile
And Test It
func testFetchActionsObtainsDataFromUserDefaults() {
let userDefaultsMock = UserDefaultsMock()
sut.setValue(userDefaultsMock, forKey: "userDefaults")
_ = sut.fetchActions()
XCTAssertTrue(userDefaultsMock.objectForKeyInvoked)
}
Commit!
Get the new VC in
#AdvArchMobile
New Swift VC
★ Replaces the old one
★ Refactor Storyboard
★ New Swift class with the desired name
★ Reuse the tests to create an instance of this
one
Refactor the Storyboard
Deal with
Limitations
#AdvArchMobile
Rough Edges
★ Structs & Enums
★ Tuples
★ Generics
★ Curried & global functions
★ Typealiases
#AdvArchMobile
@objcMembers class ActionWrapper: NSObject
{
private var action: Action
var title: String {
get {
return action.title
}
set(newTitle) {
action.title = newTitle
}
}
//…
init(action: Action) {
self.action = action
}
init(title: String, type: String, url:
String) {
action = Action(title: title,
type: type, url: url)
}
}
Use Wrappers
struct Action {
var title: String
var type: String
var url: String
}
Commit!
#AdvArchMobile
But…
★ Entity Gateway should implement both
★ Value semantics are lost
★ Use scarcely
★ Remove when possible
And Finally…
New Use Case
#AdvArchMobile
Use Case
typealias AskForRatingCompletion = (Bool) -> ()
class AskForRatingUseCase: UseCase {
let entityGateway: EntityGateway
let preferencesGateway: PreferencesGateway
let completion: AskForRatingCompletion
init(entityGateway: EntityGateway, preferencesGateway:
PreferencesGateway,
completion: @escaping AskForRatingCompletion) {
self.entityGateway = entityGateway
self.preferencesGateway = preferencesGateway
self.completion = completion
}
func execute() {
let ask = entityGateway.numberOfActions > 10
&& preferencesGateway.daysSinceLastRating > 180
completion(ask)
}
}
#AdvArchMobile
// View (extension)
func askForRating() {
SKStoreReviewController.requestRe
view()
}
Presenter & View
// Presenter
func viewReady() {
actions = fetchActions()
mayAskUserForRating()
}
private func
mayAskUserForRating() {
let useCase =
useCaseFactory.askForRatingUseCas
e(completion: { (shouldAsk: Bool)
in
view.askForRating()
})
useCase.execute()
}
Commit!
Recap
#AdvArchMobile
Recap
★ Incremental refactoring is feasible
★ Design your strategy
★ Use the tactics
★ Small non breaking changes are best
★ Tests are key
★ Don’t follow sequential order
Tusen
Takk!
Thank
You!
@jdortiz
#AdvArchMobile

Refactor your way forward