How I take control of legacy code
I'm leaving Trifork to start working on Snapsale at Skylib. I will be taking over their iOS code base. I've taken over maintenance of many codebases before, and I thought it was time to describe my process.
I have two goals for this process:
- learn the code that is there
- make sure I end up with a maintainable project
This piece is a bit lengthy, and I'm not saying I'll need to do it all on Skylibs code, nor should this be seen as a fixed guide to any project. I do recommend following the same steps for anyone taking over code that I have maintained at Trifork, so it is also not meant to be any judgement. It is simply a description of my current process for accomplishing these two goals. With that said, let's go through the steps:
Step #1, the most important step, is to gather a list of features and use-cases, so I understand what the app does. So far I have never been able to complete this step, I always come back to add to this list later, and I really wish I could become better at this step. It really helps every step going forward.
Step #2, does it compile? Very often it does not. It depends on something installed on the developers computer he or she took for granted (hello protobuf), not everything was committed because of a too loose .gitignore file, or the project simply hasn't been maintained for long because the customer was happy with it being in the store and did not want any expenses maintaining it without there being a "fire" or a new business-critical feature
Step #3, run and read all the tests. Do the tests cover the list from step #1? If so, I am forever in the debt of the author I have taken the project over from and he or she is now my personal hero. And I'm happy to say I have a few of these heros.
Step #4, get the certificates and accompanying private- and public keys and put them in a separate keychain file (without a password) that I'll commit to the repo. If you have been granted access to the repo, I'll want you to have this information.
Step #5, are there any build scripts? If so, do they run? Do they work? I'm no wizard at reading shell scripts, but I need to understand what's going on during my build.
Step #6, is there a build server? If so, make sure I have access to it and that I can trigger a build and understand the process of what is happening to it.
At this point, I have gotten to know the project a bit, and I know what makes it tick. Now I will begin pruning away what I don't think belongs in the project and improve on the code, in order to improve maintainability. It being more maintainable will help me understand the code more in-depth later. So let's get at it!
Step #7, take control of dependencies. All too often I get a .xcodeproj with a lot of external code and libraries thrown in and hacked together into the build. This is no way to live! If I'm lucky enough to be able to talk to the original author, more often than not I get the line "I used to be a Java developer, and I got stuck in Maven hell". I feel for you, bro, but this is not Java, and managing dependencies does not mean downloading the internet Maven style. And doing it by hand is probably not managing the at all.
CococaPods is great for managing dependencies. So I will gather a list of the dependencies, and write the list of them in a Podfile. Then I will use CocoaPods to manage these dependencies and remove them from the .xcodeproj. Usually the dependencies that were there are embarrassingly out of date, often exposing know security holes or just not supported any more by their service providers. Yes, I'm looking at you, AFNetworking, Facebook, Flurry and Fabric. So I will try updating them all to the latest version and see if it compiles with only minor modifications. If it does, great. If not, I'll evaluate whether I should back down a couple of versions, or whether I should accept that the app is now broken and work to fix that. And even if they work, I'll need to compare it to what was in the release we had at step #6 to see if this has given unintended consequences. If it has, back down to the original versions (although still in CocoaPods) and make a ticket for upgrading these in the task management system I use.
Yes, I will check in the Pods directory into the repo. I want to be able to check out the repo and have it compiled on any Mac with Xcode installed - I don't want to depend on neither CocoaPods nor the internet.
CocoaPods is great at compiling a list of acknowledgements in either plist or markdown format. Too often these are not reflected in the app, so I'll make a ticket to include them, either in an About page or in the Settings bundle.
Finally, if Core Data is a dependency, I will usually add mogenerator and Magical Record - my go-to tools for making Core Data easy to handle and maintainable. And yes, I'll include the Mogenerator binaries in the repo.
Step #8 is cleaning up the Xcode project and git repo further. I'll remove any accidentally committed userdata, .DS_Store files, .bak files and other temporary files. I'll add entries in the .gitignore file so they don't reappear.
I'll remove code that has been commented out. If I'll ever need it (probably not), it is in the repo and I can look it up there.
I'll turn TODOs, FIXMEs and the like into warnings. If it's an all Objective-C codebase, I'll turn them into #warnings, if there is Swift in the project I have a little script that will turn these into warnings at build time.
Lately I've explored having a scheme that will build with the latest SDK as a project that will only run on the latest iOS version. I'll run this on a build server, and this will make any deprecated methods or constants light up, so that I can be sure not to use them anymore.
Step #9, readable code. I'm sure the code you write is consistent and easily readable. While that is my goal too, it is hard to be consistent. But code is written to be read, and having a high degree of consistency and low variability accross projects makes for an easier read. That's why I love Uncrustify. Uncrustify will take a config file of how the code should be formatted and apply that. This means that all code will read the same, making me only having to read what is actually going on in the code, instead of parsing different syntax from code to code.
But Uncrustify can't do it all. So after that I'll go through the ivars and make sure they have an underscore as a prefix, just like we've been taught. It makes it really easy to see what are ivars, without having to resolve to workarounds such as prefixing them with self->. At the moment this is the only task I'll take out AppCode for. I probably should spend more time with AppCode and find other areas for it, but for now, this is where it shines in my toolbelt.
Step #10 is reducing the number of targets. Targets are high-maintenance. They drift apart and have buckets and buckets of options. Chances are you only want every few of those options to diverge. This is why I prefer configurations and schemes instead. If there is some variability I cannot fit into this, I'll use my PreBuild tool, and with this in hand I've been able to deliver many apps that share a code-base but diverge in both features, looks, app store details and languages. The benefit is that this is just configuration, expressed as JSON, and thus easily followed over time in the git repo. In contrast to your project.pbxproj.
Step #11 is grouping functions belonging to a protocol together. For each Objective C class, I'll run use #pragma mark for each protocol, and for Swift I'll use class extensions.
Step #12, while grouping functions, this is a great time to remove dead code, meaning code that has been commented out, that can never be reached or which isn't included in the compile.
Step #13, another thing that I'll do at this time is looking at the class interfaces and see if only what should be public is public, and possibly refactor parts into a protocol. Then I can begin writing missing tests to ensure that I've understood the code correctly.
This step is one of those usually-never-complete steps, and I'm not going to be too rigorous about it. If I can devote a week or two in a medium sized project, this is usually well worth the effort. It'll increase my knowledge of the code base, and reduce technical debt at the same time
Step #14, understanding how State is managed in the app. Usually this is too intertwined in the code to reasonably be done anything about, but at least I should understand it. Then as I write new code, I'll probably transform it slowly and try to bring those changes back to the old code. If there is one thing I'm really holding my fingers crossed for when entering a new project, it is good state management. And to be perfectly honest, I haven't quite figured out what that is myself yet. But I'm confident that I'm on a good path.
Step #15, warnings. Another thing that is good doing at this stage is fixing all those warnings that either were in the project already, or that cropped up because of TODOs and FIXMEs. Also, rigorously running build & analyze probably yields interesting code paths. And finally, run instruments to check for leaks and other memory buildup, high CPU or GPU usage, and framerates dropping below 60 FPS on the target devices.
Step #16, dependency injection. Scary word? Not really, it's just creating properties that can be populated by whoever creates an object, and that will be used instead of the singletons that too often litter an iOS project. This is usually counted as minutes per class, and makes the classes so much more testable.
Step #17, when refactoring, if there is no logging framework beyond print() in Swift and NSLog() in Objective-C, I'll probably include CocoaLumberjack. Also, if there is an analytics library, I'll probably add the ARAnalytics wrapper so that it is easy to add another one if it provides interesting metrics and services. My current gang is Flurry and Fabric.
Step #18, move graphics into asset catalogs, and make sure all the graphics are there. Way to often there is just the @2x.png file, which of course is no good as it'll slow down slow non-retina devices, and look blurry on @3x devices. More often than not I will ask the designers to re-create all the files as PDFs, and use them in the asset catalogs and have Xcode create PNGs for the different resolutions.
Finally, step #19, is to move code into components. There are three parts to this: if there are any parts (usually custom controls) that can be moved into a CocoaPod, this should be done at once. I'm not saying it has to be open sourced, having an in-house repo is just fine.
Then the code that can be shared between extensions and apps for other platforms such as Apple Watch or OS X should be moved into a framework.
At last I'll move the rest of my app code into a framework. I do this so that I can import it into a Playground and use the Playground to work with views and animations instead of having to do compile-and-run to show a proof-of-concept or navigate to the place in the code where it is being used. This should make me more efficient when working together with the designer.
Wow, that was a lot of steps, and a lot of important work to do. Some may be redundant because it has already been done, others may be deprioritized because of project constraints. Sometimes it is fine incurring more technical debt in order to get to market. But if I get to do it my way, this is what I will do - and I'm sure I'll have a better understanding of the project, and a more flexible project for it. Which means being able to more reliably deliver those new features and versions month after month.