I didn’t always work in a larger team. At the beginning of my journey as a computer programmer, I was the only one person in a project. It meant that I had had a free-hand (or semi-free-hand) to choose how I could write a code and which solutions I could use. From day to day I could perform a little revolution in the codebase. No consequences and no problems because the only user of the code was me.

Although this situation might look as the best case for a programmer, it doesn’t. Especially for the junior programmer. You have no opportunity to learn from someone else. You can’t validate your ideas and thoughts with others. Ultimately your only friends and co-workers are Google and Mr. StackOverflow.

After some time, I got a new job and I started to work as a part of the team. As a new person in the company, I had tons of ideas and tools that we could use in a project to make our work better, more pleasant and easier. It was relatively easy to introduce new features into the codebase. On the other hand, it was almost impossible to remove the old ones. Why?

Because we didn’t have the code deprecation strategy.

If you add a revolutionary feature to the codebase and your co-workers like it, you can be 100% sure that further changes will not be that easy. That works quite good, why we want to change it? More people in the project, the more difficult is to introduce ground-breaking changes.

That’s the difference between working alone and with the team. Even if you introduce something that should replace the current feature, people should also accustom to the new one and change the old code as well. If not, your project will contain a deprecated or even obsolete code.

Why code become obsolete?

By saying obsolete, I mean that a code which is not in use anymore. Often it has left in codebase because it was used somewhere, but things changed and people forget about it. The code can become obsolete when we introduce a new way to solve a particular problem but we don’t do anything to get rid of the old feature.

So every time when we:

  • have completely new idea to solve the problem,
  • discover that that current implementation would be better,
  • change the infrastructure behind,
  • need to change something to adjust the system to business needs

There is a chance to left old code, even if it’s not needed anymore. Before code becomes obsolete, it enters the transition phase – the stage when we change the old implementation to the new one. Problems might appear when we allow the old code work or exist in parallel with the new code without control. Finally, we can forget about the old code. Sometimes we consciously leave old code in the project just in case.

So what we should do to avoid situations like above? We should write code that is easy to understand, easy to use, and easy to replace. We should encourage people to use the new one all the time.

How to change the code?

Let’s take the following statement as true:

It’s very difficult to replace every usage of function/method/class in the codebase but it’s relatively easy to implement the new function/method/class beside old one and switch a usage to the new implementation step by step.

According to above, we shouldn’t change the code directly within old methods or classes. Instead, we should create new methods and classes which will replace older counterparts in the future. If you follow SOLID principles, it’s convenient, thanks to Single Responsibility Principle and Open-Close Principle.

The cause is simple. If we don’t replace all usage of the old function or class and we change something in the old code (implementation, method’s header, etc.), then the execution may fail.

I’m going to show a simple scenario when we perceive an opportunity to improve the usability of the code. Let’s say that we have a class called ArticleRepository.

class ArticleRepository extends BaseRepository
{
   /**
    * @param User $user
    *
    * @return Article[]
    */
    public function getArticlesForUser(User $user): array
    {
        $query = $this->db->prepare("SELECT * FROM articles WHERE user_id = :id");
        $query->bindParam(':id', $user->getId());
        
        $result = $query->execute();
        
        return $this->mapToEntities(Article::class, $result);
    }
}

Method getArticlesForUser is very easy to use – its only need a User object. In our system, we use this object in many places, more often in a magic way (it make refactoring by IDE much harder).

After some time we realized that we have to create whole User object just to pass it to this method only to retrieve its id. However, chaning the implementation or function header may lead to errors if we miss some occurrence. Instead, we can implement the new method and change the old one to work as a proxy.

class ArticleRepository extends BaseRepository
{
   /**
    * @param User $user
    *
    * @return Article[]
    */
    public function getArticlesForUser(User $user): array
    {
        return $this->getArticlesForUserId($user->getId());
    }
    
    /**
     * @param int $userId
     *
     * @return Article[]
     */
    public function getArticlesForUserId(int $userId): array
    {
        $query = $this->db->prepare("SELECT * FROM articles WHERE user_id = :id");
        $query->bindParam(':id', $userId);
        
        $result = $query->execute();
        
        return $this->mapToEntities(Article::class, $result);
    }
}

Great! We can use the new method without break anything in our code! If we had cases where we would like to use the old method, we could leave it. But let’s assume, that the getArticlesForUser method isn’t vital anymore and it should be removed. Shall we do it now?

Deprecate the old method

As we previously mentioned, we couldn’t find each place where the old method is called. In spite of all, leaving the method (even commented – who is care about comments?) in the code is like the invitation to use.

Happily, thanks to the PHPDoc standards, we have a @deprecated annotation. Moreover, the majority of modern IDEs recognize this annotation and mark the corresponding calls to inform and discourage developers to use it.

PHPStorm notification about use of deprecated method.
PHPStorm notifies when you use deprecated methods or class.

The only thing we need is to add a @deprecated annotation to the DocBlock comment above the method. It’s also recommended to add extra information about the future of this method and potential replacements.

class ArticleRepository extends BaseRepository
{
   /**
    * @param User $user
    *
    * @return Article[]
    *
    * @deprecated in favour of {@see getArticlesForUserId}. It'll be remove in the next major release.
    */
    public function getArticlesForUser(User $user): array
    {
        @trigger_error("Method getArticlesForUser is deprecated and it'll be removed in the next major release. Use getArticlesForUserId instead.", E_USER_DEPRECATED);
        
        return $this->getArticlesForUserId($user->getId());
    }
    
    // ...
}

Bracket syntax within the PHPDoc means that the tag within is embedded as inline to refer to other parts of the documentation.

We also put the trigger_error function with preceding silence operator (@) to simply notify users whenever a method is executed. Thanks to this, we can set up a custom error handler dedicated to collect deprecation errors. It’ll give us a lot of useful information.

A custom handler may look like below.

$deprecationHandler = function(int $no, string $message, string $file, int $line, array $context) {
    $trace = debug_backtrace();

    $message = date("Y-m-d H:i:s") . ": {$message} in {$file}:{$line}." . PHP_EOL;
    $message .= "Context: " . json_encode($context) . PHP_EOL;
    $message .= "Trace: " . json_encode($trace) . PHP_EOL . PHP_EOL;

    $file = fopen("deprecation.log", "a");
    fputs($file, $message);
    fclose($file);
};

set_error_handler($deprecationHandler, E_USER_DEPRECATED);

Of course, it’s only an example to show which data we can retrieve. You can use whatever you want – whatever you choose, Monolog should cover all your needs. The most interesting part is a debug_backtrace()function which lets us see where the method call comes from – it’s usually done by third-party handlers like Sentry or Bugsnag.

When removing deprecated code?

By deprecating code we set a contract:

/* @deprecated in favour of {@see getArticlesForUserId}. It'll be remove in the next major release. */

So, please keep your word. If you don’t make it because of problems, deadlines, whatever, please update the obsolete information! There’s nothing more misleading than deprecated comments and deprecated promises about deprecated features.

Does my team need a deprecation strategy?

You may ask: Does my team (or even I) need a code deprecation strategy? “My team is small enough and our project is small yet and evolve dynamically”. It doesn’t depend on the size of a team or even project. It’s about the maturity of every member and the whole team. Moreover, it’s related to how we solve problems.

Our solutions may look like hotfix every time and there’s not a field to use a deprecation strategy. It’s not a way to work at all.

By applying well–tested and checked principles to the work we can produce good quality code and solutions that are open to following improvements. Also, the whole process evolves so we can push next improvements like deprecation strategy to it.

If the code changes dynamically and we have to be sure that everything works as expected then we need an easy and understandable system that helps us remove old features. That’s what a deprecation strategy is about – removing the code in a save way.

Maybe you develop an open–source library whose use thousands of developers. You may have tons of ideas about further development, new features and improvements to existing ones. But if you want to change something you have to give people easy to use replacement and encourage them to change. You can remove the functionality in the next major release causing astonishment in many people. You can also depreciate the functionality and announce the replacement to slowly accustom people to the new solution.

The same situation happens when you work in a team. Developers also used to some solutions. We should encourage them to change but it needs time. That’s why it’s worth to keep the old implementation working. It’s not the only reason. It’s simply hard to perform a global change across the whole system.

I loved the way how Symfony team delivers features that don’t have a backward compatibility. They don’t change interfaces, that could break everything. They manage by using a simple deprecation strategy. We can learn from them.

Summary

All processes and principles should make our work easier. The deprecation strategy is like another contract between developers in the team – I’m going to remove it, but I have a better alternative for you. We can also deliver new functionalities and improvements without worrying about the previous features, however, we will have to maintain one thing more.

Needless to say, that keeping the codebase at the minimum is very important. Less code to read and understand make our work easier. An obsolete code is worse than legacy because the legacy code at least works and we often know when we break something.

Don’t hesitate to remove old code, if it’s really old and needless. But don’t forget about the Principle of least astonishment. If something is in use but it needs to be replaced by the other, give people the alternative and depreciate the former feature. Try to encourage and accustom them to the new implementation. If it’s really better, people will catch up quickly.

And how often do you remove old code? If you had joined to new team and project, where everything is completely new to you, would you like to explore the obsolete code? Probably it’ll be confusing. Therefore, maybe is worth to remove code that we don’t have in the codebase?

Resources