Some programming practices are so familiar to us that we use them automatically without much thought. Sometimes these techniques become obsolete; sometimes they are applied in the wrong context. Addressing such poorly experienced habits is often met with revolt. Especially by those who use them and perceive the topic as useful, so let's do exactly that!
Marks
Programming IDEs often recognize specific types of comments to help navigate across the codebase. Xcode’s FIXME lets other developers know that a piece of code deserves more attention. TODO is helpful when something is, well, to be done. MARK differs from the previous cases; it serves a documentation purpose. The same feature in IntelliJ IDEA/Android Studio is called region.
Marks divide the source code into multiple parts using headings. That can make the code appear broken into logical units. If you are a reader familiar with the former Objective-C era of iOS development, know that this is just an updated #pragma mark directive.
Typical usage is in files with a large number of lines. Marks create the illusion of clarity by breaking them into pieces that supposedly belong together.
The usage of marks in such cases is a bad practice. Developers often abuse them to justify a file being too big. One should not depend on Xcode to make the code comprehensive and readable. Small and well-decomposed classes are more straightforward to reason about and navigate without IDE features. Especially for pull request reviewers using the web interface where those features are absent.
Extensions
Modern programming languages such as Kotlin or Swift allow you to extend classes, interfaces/protocols, structs, or enums to provide an additional implementation. You can divide your code into multiple pieces using extensions to outline what belongs closer together. Another usage is to make a convenience extension around another type you might not even own to make its use more intuitive. The possibilities are almost limitless. This isn't always a good thing, but first, a peek into history.
Extensions existed way back in Objective-C as well. If you're not blessed with experience with programming in such a language and had to guess the name for extensions, you'd likely be surprised. It's Categories! Another surprise is that Extensions are a thing in Objective-C too, but serve different purposes. What's interesting is the difference between both languages. Categories in Objective-C forced the developer to come up with the name. That's why files named in style Class+CategoryName.swift are often used even for Swift extensions. And more importantly, to use Categories, you had to import them explicitly.
Extensions in Swift are an unnamed piece of code. Such code may be more complicated for the reader to grasp. If multiple extensions of the same type exist, adding a name to the code and wrapping it in a type might help readability immensely.
Improper extension of widely used types causes namespace pollution. It's critical, before creating extensions, to ask whether all instances of the type should have such an ability. Should all UIViews have access to a blinking method? Does one specific subclass of UIView make more sense?
Some developers use extensions to break down the implementation of multiple protocols: which might also be a warning sign. If a class implements many protocols, it may be time to consider splitting it into smaller classes.
For trolls out there: you can make your co-workers mad by extending UIView with translatesAutoresizingMasksIntoConstraints and watch them compare it with translatesAutoresizingMaskIntoConstraints.
But don't.
Comments
The ability to write comments might lead undisciplined programmers to create code of poor quality. Unfortunately, it's easier to neglect to name a variable and describe what's going on in my head with a complicated but not-so-clear comment. Easy should not be our goal. Brevity and clarity should.
Great comment for poorly written code is still a code smell. Don't just take my word for it. Robert Martin states: "A comment is a failure to express yourself in code. If you fail, then write a comment; but try not to fail."
Another reason is as the code lives in the repository and is modified and refactored, its behavior might change, and its name can express it everywhere it is called. But its comment is rarely updated and may become more confusing than helpful.
Documentation comments serve their purpose very well when you're designing an API for others to use. Remember that the API needs to stand by itself, and clarity is the priority. Don't use the documentation comments as an excuse for a lousy design.
Structure
The structure of a project is one of the first things you see when you check out a codebase, and it should outline the app's purpose at first sight. However, it is not an exception that some projects have folder structures inspired by the layers of architecture, e.g., View, ViewModel, Model.
Project structure based on architecture layers is a bad practice. It makes reusability effectively impossible. Navigating through such a structure is unnecessarily complicated and becomes harder to maintain as the scope increases. It doesn't scale. Folders inspired by the architecture might have their place, not just at the top level. It should not be the first thing you see.
See for yourself, what structure tells you more about the application?
Dependencies
Open source offers many libraries to simplify life, from UI components through networking to dependency injection solutions. It can often save a great deal of time and effort. On the other hand, this carries various dangers and limitations; using third-party libraries requires discipline, order, and balance.
Randomly scattered third-party dependencies significantly reduce robustness. Shielding the core of the application and using the libraries from the outer parts of the architecture helps mitigate the risk. Abstraction eases the process of replacing one library with another.
It's OK to utilize 3rd party dependencies, but with caution. Ask yourselves: How much time will it save me? How much effort will it take to replace? Can I install enough defense mechanisms to protect the application?
The silver bullet to protect your app, though sometimes tricky or impractical, is to have the import of the dependency in only one place.
We've had the pleasure of taking over multiple apps that were impossible to maintain anymore due to this problem. Without abstraction, no longer supported (or closed sourced) libraries disintegrated the codebase. External dependencies should never hold your product hostage.
Tests
Test-driven development is a programmer's good manners, a discipline overflowing with benefits. Technical impacts are a blog post by itself, if not a series. Non-technical impacts such as easy onboarding of new team members and executable documentation that cannot become obsolete speak for themselves.
Yet they are often neglected. A complete absence of tests is the apparent first and most common violation, followed by writing tests after the production code, which mitigates all the benefits and introduces other obstacles.
You must write unit tests first - before production code. Testing first will prevent you from creating code that's too complex. It will guide you through building components of the right size. The big classes are challenging to test, and the tests will direct you to decompose them into smaller ones.
Tests written after production code are inherently lower quality and can even be misleading. Unless you write the production code as proof of the first failing test, it is impossible to say whether the tests assert what they declare. It is then questionable how well such tests protect the system under test.
If you write tests after implementation, you may find a component challenging to test, which is impossible with a test-first approach. You can't create untestable code!
The devil's in the detail
Even the mundane can be harmful if we do something too automatically and with less attention. Challenge the ordinary and seek bad practices that you wouldn't expect.