Refactor your way forward
-
Upload
jorge-ortiz -
Category
Software
-
view
147 -
download
0
Transcript of Refactor your way forward
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
Netw
ork
Location FW
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 RewriteStill 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 rememberOnly 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 VisibilityHere 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 Casetypealias 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.requestReview() }
Presenter & View// Presenter func viewReady() { actions = fetchActions() mayAskUserForRating() } private func mayAskUserForRating() { let useCase = useCaseFactory.askForRatingUseCase(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