Heart containing Coding Chica Java 101

On the Way Out! Deprecation

TIP: References Quick List

Table of Contents

  1. Table of Contents
  2. Alternate Timeline – Adding the “Old” Code
    1. Unit Tests – AnimalTest
    2. Runtime Code – Animal
  3. Deprecating the “Old” Methods
    1. Unit Tests
    2. Runtime Changes
  4. Maven Build – Javadoc Generation
  5. Commit

Imagine an alternative timeline where we didn’t have the AgeUnits enum and instead exposed 3 separate methods for retrieving the age in hours, days and years. Then, we got the idea to add the combined getAge(AgeUnits) method, so we could reduce the amount of code we must maintain.

Removing the no-longer-desired approach would be a breaking-change for any existing users of those methods – meaning it may prevent their existing code from compiling and/or running correctly. If the Animal class (or an interface it represents) is consumed by external clients, we may need to give them notice that we intend to remove code from the existing structures. This is called deprecation – it is no longer the desired approach, but it has not yet been entirely removed from the code base. It is basically a warning mechanism that, if they do not migrate off of the current approach, their consuming code may break in the future. I have also heard this called the sunset-path, where sunset indicates the time at which the logic will be removed. Some organizations will

  • Require a notice period of a given length, or
  • Focus on how many versions are released before the code gets retired.
  • Wait until they have enough large / breaking features that they want to do a major version upgrade (we’ll talk more about semantic versioning in the future).

Therefore, it is important to read the Javadoc comments and release notes (if available) to see how quickly the migration to the new approach needs to occur and what, if anything, the owning team recommends as an alternative.

I see this as a frequent point of confusion – deprecated methods still need to be maintained by the owning team until such time as we can fully remove them. We are not rid of them until they are actually removed from the code base. Granted, if you reach out to support asking for basic troubleshooting, the recommendations you may receive are to use the new approach instead.

How do we document a deprecated method? Let’s pretend we are adding these “old” methods before the “new” getAge(AgeUnits) was available. Then, let’s mark them deprecated.

Alternate Timeline – Adding the “Old” Code


The interesting part of this discussion isn’t adding the code we want to pretend was already there, so let’s skim past this part. The tests and code themselves are very similar to what we added for getAge(AgeUnits), we just have separate methods for years, days and hours calculations. Here is the example code for this change.

Unit Tests – AnimalTest

    /**
     * Unit test for the getAgeInYears method.
     * @see Animal#getAgeInYears()
     */
    @Nested
    class GetAgeInYearsTest {

        @Test
        void getAgeInYears_whenNull_thenReturnsExpectedValue(){
            // Setup
            Animal animal = new Animal();
            long expectedResult = -1;

            // Execution
            long result = animal.getAgeInYears();

            // Validation
            assertEquals(expectedResult, result);
        }

        @ParameterizedTest
        @EnumSource(value = RelativeTime.class, mode = EnumSource.Mode.EXCLUDE, names = {})
        void getAgeInYears_whenInvokedWithTimeOfBirthSet_thenReturnsExpectedValue(RelativeTime value){
            // Setup
            Animal animal = new Animal();
            animal.setTimeOfBirth(value.getInstant());
            long expectedResult = switch (value){
                case TEN_YEARS_AGO -> -10;
                case YEAR_AGO -> -1;
                case DAY_AGO,HOUR_AGO, HOUR_AHEAD, DAY_AHEAD -> 0;
                case YEAR_AHEAD -> 1;
                case TEN_YEARS_AHEAD -> 10;
            };

            // Execution
            long result = animal.getAgeInYears();

            // Validation
            assertEquals(expectedResult, result, () -> String.format("%s: %s", value, value.getDurationString()));
        }
    }


    /**
     * Unit test for the getAgeInDays method.
     * @see Animal#getAgeInDays()
     */
    @Nested
    class GetAgeInDaysTest {

        @Test
        void getAgeInDays_whenNull_thenReturnsExpectedValue(){
            // Setup
            Animal animal = new Animal();
            long expectedResult = -1;

            // Execution
            long result = animal.getAgeInDays();

            // Validation
            assertEquals(expectedResult, result);
        }

        @ParameterizedTest
        @EnumSource(value = RelativeTime.class, mode = EnumSource.Mode.EXCLUDE, names = {})
        void getAgeInDays_whenTimeOfBirthPopulated_thenReturnsExpectedValue(RelativeTime value){
            // Setup
            Animal animal = new Animal();
            animal.setTimeOfBirth(value.getInstant());
            long expectedResult = switch (value){
                case TEN_YEARS_AGO -> -3750;
                case YEAR_AGO -> -366;
                case DAY_AGO -> -1;
                case HOUR_AGO, HOUR_AHEAD -> 0;
                case DAY_AHEAD -> 1;
                case YEAR_AHEAD -> 365;
                case TEN_YEARS_AHEAD -> 3750;
            };

            // Execution
            long result = animal.getAgeInDays();

            // Validation
            assertEquals(expectedResult, result, () -> String.format("%s: %s", value, value.getDurationString()));
        }
    }


    /**
     * Unit test for the getAgeInHours method.
     * @see Animal#getAgeInHours()
     */
    @Nested
    class GetAgeInHoursTest {

        @Test
        void getAgeInHours_whenNull_thenReturnsExpectedValue(){
            // Setup
            Animal animal = new Animal();
            long expectedResult = -1;

            // Execution
            long result = animal.getAgeInHours();

            // Validation
            assertEquals(expectedResult, result);
        }

        @ParameterizedTest
        @EnumSource(value = RelativeTime.class, mode = EnumSource.Mode.EXCLUDE, names = {})
        void getAgeInHours_whenTimeOfBirthPopulated_thenReturnsExpectedValue(RelativeTime value){
            // Setup
            Animal animal = new Animal();
            animal.setTimeOfBirth(value.getInstant());
            long expectedResult = switch (value){
                case TEN_YEARS_AGO -> -90000;
                case YEAR_AGO -> -8784;
                case DAY_AGO -> -24;
                case HOUR_AGO -> -1;
                case HOUR_AHEAD -> 1;
                case DAY_AHEAD -> 28;
                case YEAR_AHEAD -> 8783;
                case TEN_YEARS_AHEAD -> 90000;
            };

            // Execution
            long result = animal.getAgeInHours();

            // Validation
            assertEquals(expectedResult, result, () -> String.format("%s: %s", value, value.getDurationString()));
        }
    }

Runtime Code – Animal

/**
     * Retrieve the age for the animal in years.  An animal that has not
     * yet been born will have a negative age.
     *
     * @return The age of the animal in years, or a negative
     * number if the animal has not yet been born or hatched.
     */
    public long getAgeInYears() {
        long age = -1;
        if (timeOfBirth != null) {
            Instant now = Instant.now();
            Duration difference = Duration.between(now, timeOfBirth);
            age = difference.toDays() / AVERAGE_DAYS_IN_YEAR;
        }
        return age;
    }

    /**
     * Retrieve the age for the animal in days.  An animal that has not
     * yet been born will have a negative age.
     *
     * @return The age of the animal in days, or a negative
     * number if the animal has not yet been born or hatched.
     */
    public long getAgeInDays() {
        long age = -1;
        if (timeOfBirth != null) {
            Instant now = Instant.now();
            Duration difference = Duration.between(now, timeOfBirth);
            age = difference.toDays();
        }
        return age;
    }

    /**
     * Retrieve the age for the animal in hours.  An animal that has not
     * yet been born will have a negative age.
     *
     * @return The age of the animal in days, or a negative
     * number if the animal has not yet been born or hatched.
     */
    public long getAgeInHours() {
        long age = -1;
        if (timeOfBirth != null) {
            Instant now = Instant.now();
            Duration difference = Duration.between(now, timeOfBirth);
            age = difference.toHours();
        }
        return age;
    }

Deprecating the “Old” Methods


Now, let’s deprecate the “old” methods. We need to add the Deprecated annotation, so the calling code in most IDEs, such as IntelliJ, will display the method call with a strike-through, as a visual queue to the developers of the calling code that they should consider migrating.

Unit Tests

N/A – For a documentation change like deprecating methods, we need not change any unit tests.

Runtime Changes

Let’s update the src/main/java/codingchica.java101.model.Animal.java class to notate

    /**
     * Retrieve the age for the animal in years.  An animal that has not
     * yet been born will have a negative age.
     *
     * @deprecated As of 0.1, replaced by {@link #getAge(AgeUnits)}
     *
     * @return The age of the animal in years, or a negative
     * number if the animal has not yet been born or hatched.
     */
    @Deprecated(since = "0.1", forRemoval = false)
    public long getAgeInYears() {
...

    /**
     * Retrieve the age for the animal in days.  An animal that has not
     * yet been born will have a negative age.
     *
     * @deprecated As of 0.1, replaced by {@link #getAge(AgeUnits)}
     *
     * @return The age of the animal in days, or a negative
     * number if the animal has not yet been born or hatched.
     */
    @Deprecated(since = "0.1", forRemoval = false)
    public long getAgeInDays() {
...

    /**
     * Retrieve the age for the animal in hours.  An animal that has not
     * yet been born will have a negative age.
     *
     * @deprecated As of 0.1, replaced by {@link #getAge(AgeUnits)}
     *
     * @return The age of the animal in days, or a negative
     * number if the animal has not yet been born or hatched.
     */
    @Deprecated(since = "0.1", forRemoval = false)
    public long getAgeInHours() {
...

In both the @Deprecated annotation and the Javadoc comment, we can indicate the version of the code in which the logic was marked as deprecated. The Javadoc comment’s best practice is to either indicate the recommended replacement, or that there is no replacement. In newer Java versions, the @Deprecated annotation also supports a forRemoval property, which defaults to false. When this value is true, it means that the next version of the code intends to retire (AKA delete / sunset) that logic. I have specified it above, just so you can see that it exists and how it looks in the code.

Maven Build – Javadoc Generation

If we run the Maven build at this point, it should be successful and show the updates in the Javadoc comments. In IntelliJ, right click on java101/target/apidocs/codingchica/java101/model/Animal.html -> Open in -> Browser -> Your browser of choice. You should see content like the following in the All Methods section:

Modifier and TypeMethodDescription
longgetAge(AgeUnits unit)Retrieve the age for the animal.
longgetAgeInDays()Deprecated.
As of 0.1, replaced by getAge(AgeUnits)
longgetAgeInHours()Deprecated.
As of 0.1, replaced by getAge(AgeUnits)
longgetAgeInYears()Deprecated.
As of 0.1, replaced by getAge(AgeUnits)

Commit

Remember to commit in small, incremental batches. Now is a good time.

On the Way Out! Deprecation

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.