CHICIO CODING

Dirty clean code. Creative Stuff. Stuff.

Refactoring: a real case of a nested if structure transformed into a chain of responsibility

In this post my colleague Francesco Bonfadelli will show you how to transform a nested if structure into a chain of responsibility.


In this post I am going to describe a step by step process used to transform a nested if structure into a chain of responsibility. The purpose of this operation is to make the code easier to read, thus to change without introducing errors. We will also get for free a structure that will be able to apply a more generic set of rules than the one defined at the beginning.
The code of this post is based on a piece of code used to satisfy a real business need, we just removed the business related details. The language used is java.

The process in short

  • Flatten the if structure into a flat sequence of if clauses
  • Extract each condition and the related action into a single class
  • Create a common interface for all the extracted conditions
  • Put all the conditions into a list
  • Loop over the list and return the first action for which the condition is satisfied

The need

It seemed a normal day of work when one of our managers called a meeting to inform us of a very urgent feature that should be put in production within 2 days. So, as it usually happens in this case, between the deriving chaos and the tons of alignment meetings that continuously interrupted us, we produced a code that basically “worked”, but it was a bit chaotic. Luckily we were able at least to write the tests. So, once we put in production the feature, we decided to immediately refactor the piece of code.

The process

We are going to see a step by step refactor of a specific class that transforms the if-nested structure into a chain of responsibility.

We are not going to change the tests because they work as an acceptance test for our use case. In an ideal world, with a lot of time available, we would also write the tests of all the classes we are going to extract and simplify the current test. But, you know, we are not in an ideal world 😅.
The idea behind this refactor is to proceed with small steps, possibly using the IDE functionality (I used IDEA which is very good at it), and run the tests after every operation. Also, after each step there is a commit, not only to allow everyone to follow the evolution of the code through the commits, but also to allow us to simply use git checkout . in case of errors, in order to come back to the previous working version. All of this, allows us to keep the code strictly under control and avoid to introduce bugs during the refactoring. I will use the diff syntax to show the differences between some pieces of code. Please keep in mind that I will use it in order to highlight only the main differences between one commit and the other and it won’t be the exact diff you can get with git.

The initial code

Here you can find the code we were not very proud of. In particular, I report the nested if structure, which is the part we are going to refactor. (Source code)

public class HandBaggageInformationFactory {

    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        if (flight.isOneWay()) {
            if (isMyCompany(flight)) {
                LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
                if (flightOutboundDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                } else {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
                }
            } else {
                return noMyCompanyInformationInfo();
            }
        } else { //round trip
            if (isMyCompany(flight)) {
                LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
                LocalDate returnDepartureDate = order.getReturnDepartureDate();
                if (outboundDepartureDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))
                        || returnDepartureDate.isAfter(LocalDate.of(2018, 10, 31))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                } else {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
                }
            } else {
                return noMyCompanyInformationInfo();
            }
        }
    }
}

The execution

1. Flatten the if structure

The idea here is to transform the nested if structure into a flat sequence of if clauses in order to isolate and explicit each single condition.
To do so with very small steps, we are going to remove the else part of each if clause, by transforming such part into an if clause whose condition is the negation of the original one. In the following piece of code, you can notice how the outer if-else has become a couple of if clauses, one for the original condition flight.isOneWay() and the other one with the opposite condition !flight.isOneWay() (Source code)

public class HandBaggageInformationFactory {

    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        if (flight.isOneWay()) {
            if (isMyCompany(flight)) {
                LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
                if (flightOutboundDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                } else {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
                }
            } else {
                return noMyCompanyInformationInfo();
            }
        }
-       else {
+       if (!flight.isOneWay()) {  //round trip
            if (isMyCompany(flight)) {
                LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
                LocalDate returnDepartureDate = order.getReturnDepartureDate();
                if (outboundDepartureDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))
                        || returnDepartureDate.isAfter(LocalDate.of(2018, 10, 31))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                } else {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
                }
            } else {
                return noMyCompanyInformationInfo();
            }
        }

        return noMyCompanyInformationInfo();
    }
}

Once done this, we are going to proceed with the inner if-else conditions, which is isMyCompany(flight). (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        if (flight.isOneWay()) {
            if (isMyCompany(flight)) {
                LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
                if (flightOutboundDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                } else {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
                }
            }
-           else {
+           if (!isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
            }
        }

        if (!flight.isOneWay()) {  //round trip
            if (isMyCompany(flight)) {
                LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
                LocalDate returnDepartureDate = order.getReturnDepartureDate();
                if (outboundDepartureDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))
                        || returnDepartureDate.isAfter(LocalDate.of(2018, 10, 31))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                } else {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
                }
            }

            if (!isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
            }
        }

        return noMyCompanyInformationInfo();
    }
}

We proceed in this way until we have removed all the else conditions from the code. Here, you are not forced to start from the most external clause, but you can choose whatever position you prefer to start with. The important thing is that once finished you won’t have any else clause inside your code. (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        if (flight.isOneWay()) {
            if (isMyCompany(flight)) {
                LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
                if (flightOutboundDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                }

                if (!flightOutboundDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))) {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
                }
            }

            if (!isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
            }
        }

        if (!flight.isOneWay()) {  //round trip
            if (isMyCompany(flight)) {
                LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
                LocalDate returnDepartureDate = order.getReturnDepartureDate();
                if (outboundDepartureDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))
                        || returnDepartureDate.isAfter(LocalDate.of(2018, 10, 31))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                }

                if (!(outboundDepartureDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))
                        || returnDepartureDate.isAfter(LocalDate.of(2018, 10, 31)))) {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
                }
            }

            if (!isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
            }
        }

        return noMyCompanyInformationInfo();
    }
}

Once removed all the else, we are going to duplicate some conditions in order to have only one if clause inside another if. At a first glance, it could seem complicated to understand but it is actually pretty simple 🚀. We start by duplicating isMyCompany(flight) in the two external if clauses. (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        if (flight.isOneWay()) {
            LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
            if (isMyCompany(flight)) {
                if (flightOutboundDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                }

-               if (!flightOutboundDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))) {
-                   return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
-               }
            }
            
+           if (isMyCompany(flight)) {
+               if (!flightOutboundDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))) {
+                   return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
+               }
+           }

            if (!isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
            }
        }

        if (!flight.isOneWay()) {  //round trip
            LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
            LocalDate returnDepartureDate = order.getReturnDepartureDate();
            if (isMyCompany(flight)) {
                if (outboundDepartureDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))
                        || returnDepartureDate.isAfter(LocalDate.of(2018, 10, 31))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
                }
                
-               if (!(outboundDepartureDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))
-                       || returnDepartureDate.isAfter(LocalDate.of(2018, 10, 31)))) {
-                   return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
-               }

            }
            
+           if (isMyCompany(flight)) {
+               if (!(outboundDepartureDate.isAfter(LocalDateTime.of(2018, 11, 1, 0, 0, 0))
+                       || returnDepartureDate.isAfter(LocalDate.of(2018, 10, 31)))) {
+                   return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
+               }
+           }

            if (!isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
            }
        }

        return noMyCompanyInformationInfo();
    }
}

After having done this process for all the conditions, we will finally get the flat if structure. (Source code)

public class HandBaggageInformationFactory {
    private static final LocalDateTime FIRST_OF_NOVEMBER = LocalDateTime.of(2018, 11, 1, 0, 0, 0);
    private static final LocalDate THIRTY_FIRST_OF_OCTOBER = LocalDate.of(2018, 10, 31);

    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
        LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
        LocalDate returnDepartureDate = order.getReturnDepartureDate();

        if (flight.isOneWay()
                && isMyCompany(flight)
                && flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
            return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
        }

        if (flight.isOneWay() && isMyCompany(flight) && !flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
            return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
        }

        if (flight.isOneWay() && !isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER))) {
                    return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (!(outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER)))) {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
        }

        if (!flight.isOneWay() && !isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
        }

        return noMyCompanyInformationInfo();
    }
}

Intermediate step: extracting factories

Before keeping on with the extraction of the chain of responsibility from the if structure, we are going to make some intermediate steps. In order to reduce the responsibilities of the HandBaggageInformationFactory, here, we are going to extract three factories, each one responsible for creating a specific HandBaggageInformation. Without diving into the code used to create the object, we just extract the NewMyCompanyHandBaggageInformationFactory out of the method newMyCompanyHandBaggageInformation. (Step 1, Step 2 and Step 3) If you are using IDEA, an easy way is to do it is to use its Extract method object feature. I won’t explain how to do it here, because it is out of the scope of this topic, but I have just realized I have found the next topic of my blog (this is great! isn’t it? :smirk:).

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
        LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
        LocalDate returnDepartureDate = order.getReturnDepartureDate();

+        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
+                new NewMyCompanyHandBaggageInformationFactory(translationRepository);

        if (flight.isOneWay()
                && isMyCompany(flight)
                && flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
-             return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
+            return newMyCompanyHandBaggageInformationFactory.execute(renderLanguage);
        }

        if (flight.isOneWay() && isMyCompany(flight) && !flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
            return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
        }

        if (flight.isOneWay() && !isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER))) {
-           return newMyCompanyHandBaggageInformation(translationRepository, renderLanguage);
+           return newMyCompanyHandBaggageInformationFactory.execute(renderLanguage);
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (!(outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER)))) {
                    return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
        }

        if (!flight.isOneWay() && !isMyCompany(flight)) {
                return noMyCompanyInformationInfo();
        }

        return noMyCompanyInformationInfo();
    }
}

Once done this, we repeat the operation for the other two methods that create the objects, obtaining the NotMyCompanyHandBaggageInformationFactory and the OldMyCompanyHandBaggageInformationFactory. (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
        LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
        LocalDate returnDepartureDate = order.getReturnDepartureDate();

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
+       OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
+               new OldMyCompanyHandBaggageInformationFactory(translationRepository);
+       NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
+               new NotMyCompanyHandBaggageInformationFactory();

        if (flight.isOneWay()
                && isMyCompany(flight)
                && flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
              return newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (flight.isOneWay() && isMyCompany(flight) && !flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
-            return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);        
+            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (flight.isOneWay() && !isMyCompany(flight)) {
-            return noMyCompanyInformationInfo();        
+            return notMyCompanyHandBaggageInformationFactory.make();
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER))) {
             return newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (!(outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER)))) {
-            return oldMyCompanyHandBaggageInformationInfo(translationRepository, renderLanguage);
+            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (!flight.isOneWay() && !isMyCompany(flight)) {
-            return noMyCompanyInformationInfo();
+            return notMyCompanyHandBaggageInformationFactory.make();
        }

-       return noMyCompanyInformationInfo();
+       return notMyCompanyHandBaggageInformationFactory.make();
    }
}

2. Creating the components of the chain

By using again the Extract method object feature of Idea, you can easily extract the first condition into a class. In this way we get new MyCompanyOneWayAfterTheFirstOfNovember().canHandle(flight, flightOutboundDate) in the first if condition. (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
        LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
        LocalDate returnDepartureDate = order.getReturnDepartureDate();

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
        OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
                new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();

-       if (flight.isOneWay()
-               && isMyCompany(flight)
-               && flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
-             return newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
-       }
        
+       if (new MyCompanyOneWayAfterTheFirstOfNovember().canHandle(flight, flightOutboundDate)) { 
+           return newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
+       }

        if (flight.isOneWay() && isMyCompany(flight) && !flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (flight.isOneWay() && !isMyCompany(flight)) {
            return notMyCompanyHandBaggageInformationFactory.make();
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER))) {
            return newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (!(outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER)))) {
            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (!flight.isOneWay() && !isMyCompany(flight)) {
            return notMyCompanyHandBaggageInformationFactory.make();
        }

        return notMyCompanyHandBaggageInformationFactory.make();
    }

+   private class MyCompanyOneWayAfterTheFirstOfNovember {
+       public boolean canHandle(Flight flight, LocalDateTime flightOutboundDate) {
+           return flight.isOneWay()
+                   && isMyCompany(flight)
+                   && flightOutboundDate.isAfter(FIRST_OF_NOVEMBER);
+       }
+   }
}

And, after that, we can move newMyCompanyHandBaggageInformationFactory.from(renderLanguage) inside MyCompanyOneWayAfterTheFirstOfNovember.

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
        LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
        LocalDate returnDepartureDate = order.getReturnDepartureDate();

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
        OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
                new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();

-       if (new MyCompanyOneWayAfterTheFirstOfNovember().canHandle(flight, flightOutboundDate)) { 
-           return newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
-       }
+        MyCompanyOneWayAfterTheFirstOfNovember myCompanyOneWayAfterTheFirstOfNovember =
+                        new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
+                if (myCompanyOneWayAfterTheFirstOfNovember.canHandle(flight, flightOutboundDate)) {
+                    return myCompanyOneWayAfterTheFirstOfNovember.getFrom(renderLanguage);
+                }

        if (flight.isOneWay() && isMyCompany(flight) && !flightOutboundDate.isAfter(FIRST_OF_NOVEMBER)) {
            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (flight.isOneWay() && !isMyCompany(flight)) {
            return notMyCompanyHandBaggageInformationFactory.make();
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER))) {
            return newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (!flight.isOneWay() && isMyCompany(flight) && (!(outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                        || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER)))) {
            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }

        if (!flight.isOneWay() && !isMyCompany(flight)) {
            return notMyCompanyHandBaggageInformationFactory.make();
        }

        return notMyCompanyHandBaggageInformationFactory.make();
    }

    private class MyCompanyOneWayAfterTheFirstOfNovember {
    
        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;
    
        private MyCompanyOneWayAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }
        
        public boolean canHandle(Flight flight, LocalDateTime flightOutboundDate) {
            return flight.isOneWay()
                        && isMyCompany(flight)
                        && flightOutboundDate.isAfter(FIRST_OF_NOVEMBER);
        }
    
+       public HandBaggageInformation getFrom(String renderLanguage) {
+               return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
+       }
    }
}

Then, we repeat the operation for all the conditions except for the ones that use our default value, notMyCompanyHandBaggageInformationFactory.make(). First of all, we remove the condition that once true uses the default behaviour (!flight.isOneWay() && !isMyCompany(flight)), because it is redundant. Then, for each remaining condition we create a class containing the evaluation of the condition and the related action.
I skip this step by step diff because it is a repetition of the previous one, but in the repo there are all the commits that show the process. The resulting code, after having extracted all the conditions, is the following. (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        LocalDateTime flightOutboundDate = flight.getFirstLeg().getFirstHop().getDeparture().getDate();
        LocalDateTime outboundDepartureDate = order.getOutboundDepartureDate();
        LocalDate returnDepartureDate = order.getReturnDepartureDate();

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
        OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
                new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();

        MyCompanyOneWayAfterTheFirstOfNovember myCompanyOneWayAfterTheFirstOfNovember =
                new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        if (myCompanyOneWayAfterTheFirstOfNovember.canHandle(flight, flightOutboundDate)) {
            return myCompanyOneWayAfterTheFirstOfNovember.getFrom(renderLanguage);
        }

        MyCompanyOneWayBeforeTheFirstOfNovember myCompanyOneWayBeforeTheFirstOfNovember =
                new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        if (myCompanyOneWayBeforeTheFirstOfNovember.canHandle(flight, flightOutboundDate)) {
            return myCompanyOneWayBeforeTheFirstOfNovember.getFrom(renderLanguage);
        }

        MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
                new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        if (myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember.canHandle(flight, outboundDepartureDate, returnDepartureDate)) {
            return myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember
                    .getFrom(renderLanguage);
        }

        MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
                MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        if (myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.canHandle(flight, outboundDepartureDate, returnDepartureDate)) {
            return myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.getFrom(renderLanguage);
        }

        return notMyCompanyHandBaggageInformationFactory.make();
    }

    private class MyCompanyOneWayAfterTheFirstOfNovember {

        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;

        private MyCompanyOneWayAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }

        public boolean canHandle(Flight flight, LocalDateTime flightOutboundDate) {
            return flight.isOneWay()
                    && isMyCompany(flight)
                    && flightOutboundDate.isAfter(FIRST_OF_NOVEMBER);
        }

        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyOneWayBeforeTheFirstOfNovember {
        private final OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory;

        private MyCompanyOneWayBeforeTheFirstOfNovember(OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory) {
            this.oldMyCompanyHandBaggageInformationFactory = oldMyCompanyHandBaggageInformationFactory;
        }

        public boolean canHandle(Flight flight, LocalDateTime flightOutboundDate) {
            return flight.isOneWay() && isMyCompany(flight) && !flightOutboundDate.isAfter(FIRST_OF_NOVEMBER);
        }

        private HandBaggageInformation getFrom(String renderLanguage) {
            return this.oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember {
        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;

        public MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }

        public boolean canHandle(Flight flight, LocalDateTime outboundDepartureDate, LocalDate returnDepartureDate) {
            return !flight.isOneWay() && isMyCompany(flight) && (outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                    || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER));
        }

        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember {
        private final OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory;

        private MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory) {
            this.oldMyCompanyHandBaggageInformationFactory = oldMyCompanyHandBaggageInformationFactory;
        }

        private boolean canHandle(Flight flight, LocalDateTime outboundDepartureDate, LocalDate returnDepartureDate) {
            return !flight.isOneWay() && isMyCompany(flight) && (!(outboundDepartureDate.isAfter(FIRST_OF_NOVEMBER)
                    || returnDepartureDate.isAfter(THIRTY_FIRST_OF_OCTOBER)));
        }

        public HandBaggageInformation getFrom(String renderLanguage) {
            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }
}

3. Create a common signature to extract the chain item

The purpose here is to obtain a common signature for all the conditions we have just extracted.
As this part is more domain oriented, I am not diving into the details.
Just notice the three main modifications:

  • the Order has the same information as the Flight for our case, so we remove the Order class in favour of the Flight class.
  • Keep only one date, which is our threshold date, by playing a bit with isAfter() and isBefore().
  • Move some operations from the HandBaggageInformationFactory class to the Flight one, which is more domain oriented. (Source code)

So, after these operations the code looks like

public class HandBaggageInformationFactory {
    private static final LocalDateTime FIRST_OF_NOVEMBER = LocalDateTime.of(2018, 11, 1, 0, 0, 0);

    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
        OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
                new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();

        MyCompanyOneWayAfterTheFirstOfNovember myCompanyOneWayAfterTheFirstOfNovember =
                new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        if (myCompanyOneWayAfterTheFirstOfNovember.canHandle(flight)) {
            return myCompanyOneWayAfterTheFirstOfNovember.getFrom(renderLanguage);
        }

        MyCompanyOneWayBeforeTheFirstOfNovember myCompanyOneWayBeforeTheFirstOfNovember =
                new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        if (myCompanyOneWayBeforeTheFirstOfNovember.canHandle(flight)) {
            return myCompanyOneWayBeforeTheFirstOfNovember.getFrom(renderLanguage);
        }

        MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
                new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        if (myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember.canHandle(flight)) {
            return myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember
                    .getFrom(renderLanguage);
        }

        MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
                MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        if (myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.canHandle(flight)) {
            return myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.getFrom(renderLanguage);
        }

        return notMyCompanyHandBaggageInformationFactory.make();
    }


    private class MyCompanyOneWayAfterTheFirstOfNovember {

        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;

        private MyCompanyOneWayAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }

        public boolean canHandle(Flight flight) {
            return flight.isOneWay()
                    && flight.isMyCompany()
                    && flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER);
        }

        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyOneWayBeforeTheFirstOfNovember {
        private final OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory;

        private MyCompanyOneWayBeforeTheFirstOfNovember(OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory) {
            this.oldMyCompanyHandBaggageInformationFactory = oldMyCompanyHandBaggageInformationFactory;
        }

        public boolean canHandle(Flight flight) {
            return flight.isOneWay()
                    && flight.isMyCompany()
                    && !flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER);
        }

        private HandBaggageInformation getFrom(String renderLanguage) {
            return this.oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember {
        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;

        public MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }

        public boolean canHandle(Flight flight) {
            return !flight.isOneWay()
                    && flight.isMyCompany()
                    && (flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                        || flight.getReturnDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                    );
        }

        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember {
        private final OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory;

        private MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory) {
            this.oldMyCompanyHandBaggageInformationFactory = oldMyCompanyHandBaggageInformationFactory;
        }

        private boolean canHandle(Flight flight) {
            return !flight.isOneWay()
                    && flight.isMyCompany()
                    && (!(flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                        || flight.getReturnDepartureDate().isAfter(FIRST_OF_NOVEMBER))
                    );
        }

        public HandBaggageInformation getFrom(String renderLanguage) {
            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }
}

4. Extracting the interface of the chain item

By watching closely all the extracted conditions after the simplifications made, you can notice that now all the items has a common method signature. And if you think that is time of an interface, you are totally right 🏆. So, we can easily extract an interface from one conditions chosen randomly, for example MyCompanyOneWayAfterTheFirstOfNovember. (Source code)

If you use Idea, its Extract interface feature can be helpful.

+   public interface HandBaggageInformationPolicy {
+       boolean canHandle(Flight flight);
+       HandBaggageInformation getFrom(String renderLanguage);
+   }


public class HandBaggageInformationFactory {
    private static final LocalDateTime FIRST_OF_NOVEMBER = LocalDateTime.of(2018, 11, 1, 0, 0, 0);

    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
        OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
                new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();

        MyCompanyOneWayAfterTheFirstOfNovember myCompanyOneWayAfterTheFirstOfNovember =
                new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        if (myCompanyOneWayAfterTheFirstOfNovember.canHandle(flight)) {
            return myCompanyOneWayAfterTheFirstOfNovember.getFrom(renderLanguage);
        }

        MyCompanyOneWayBeforeTheFirstOfNovember myCompanyOneWayBeforeTheFirstOfNovember =
                new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        if (myCompanyOneWayBeforeTheFirstOfNovember.canHandle(flight)) {
            return myCompanyOneWayBeforeTheFirstOfNovember.getFrom(renderLanguage);
        }

        MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
                new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        if (myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember.canHandle(flight)) {
            return myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember
                    .getFrom(renderLanguage);
        }

        MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
                MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        if (myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.canHandle(flight)) {
            return myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.getFrom(renderLanguage);
        }

        return notMyCompanyHandBaggageInformationFactory.make();
    }

-   private class MyCompanyOneWayAfterTheFirstOfNovember {
+   private class MyCompanyOneWayAfterTheFirstOfNovember implements HandBaggageInformationPolicy {

        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;

        private MyCompanyOneWayAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }

+       @Override
        public boolean canHandle(Flight flight) {
            return flight.isOneWay()
                    && flight.isMyCompany()
                    && flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER);
        }

+       @Override
        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyOneWayBeforeTheFirstOfNovember {
        private final OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory;

        private MyCompanyOneWayBeforeTheFirstOfNovember(OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory) {
            this.oldMyCompanyHandBaggageInformationFactory = oldMyCompanyHandBaggageInformationFactory;
        }

        public boolean canHandle(Flight flight) {
            return flight.isOneWay()
                    && flight.isMyCompany()
                    && !flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER);
        }

        private HandBaggageInformation getFrom(String renderLanguage) {
            return this.oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember {
        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;

        public MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }

        public boolean canHandle(Flight flight) {
            return !flight.isOneWay()
                    && flight.isMyCompany()
                    && (flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                        || flight.getReturnDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                    );
        }

        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

    private class MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember {
        private final OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory;

        private MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory) {
            this.oldMyCompanyHandBaggageInformationFactory = oldMyCompanyHandBaggageInformationFactory;
        }

        private boolean canHandle(Flight flight) {
            return !flight.isOneWay()
                    && flight.isMyCompany()
                    && (!(flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                        || flight.getReturnDepartureDate().isAfter(FIRST_OF_NOVEMBER))
                    );
        }

        public HandBaggageInformation getFrom(String renderLanguage) {
            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }
}

And then, we are going to make all the conditions implement the interface HandBaggageInformationPolicy. (Source code)

Unfortunately Idea won’t help us in this.

public class HandBaggageInformationFactory {
    private static final LocalDateTime FIRST_OF_NOVEMBER = LocalDateTime.of(2018, 11, 1, 0, 0, 0);

    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        //same as before
    }


    private class MyCompanyOneWayAfterTheFirstOfNovember implements HandBaggageInformationPolicy {

        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;

        private MyCompanyOneWayAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }

        @Override
        public boolean canHandle(Flight flight) {
            return flight.isOneWay()
                    && flight.isMyCompany()
                    && flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER);
        }

        @Override
        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

-    private class MyCompanyOneWayBeforeTheFirstOfNovember {
+    private class MyCompanyOneWayBeforeTheFirstOfNovember implements HandBaggageInformationPolicy {
        private final OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory;

        private MyCompanyOneWayBeforeTheFirstOfNovember(OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory) {
            this.oldMyCompanyHandBaggageInformationFactory = oldMyCompanyHandBaggageInformationFactory;
        }

+       @Override
        public boolean canHandle(Flight flight) {
            return flight.isOneWay()
                    && flight.isMyCompany()
                    && !flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER);
        }

+       @Override
        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

-    private class MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember {
+    private class MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember implements HandBaggageInformationPolicy {
        private final NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory;

        public MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory) {
            this.newMyCompanyHandBaggageInformationFactory = newMyCompanyHandBaggageInformationFactory;
        }

+       @Override
        public boolean canHandle(Flight flight) {
            return !flight.isOneWay()
                    && flight.isMyCompany()
                    && (flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                        || flight.getReturnDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                    );
        }

+       @Override
        public HandBaggageInformation getFrom(String renderLanguage) {
            return this.newMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }

-    private class MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember {
+    private class MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember implements HandBaggageInformationPolicy {
        private final OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory;

        private MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory) {
            this.oldMyCompanyHandBaggageInformationFactory = oldMyCompanyHandBaggageInformationFactory;
        }

+        @Override
        public boolean canHandle(Flight flight) {
            return !flight.isOneWay()
                    && flight.isMyCompany()
                    && (!(flight.getOutboundDepartureDate().isAfter(FIRST_OF_NOVEMBER)
                        || flight.getReturnDepartureDate().isAfter(FIRST_OF_NOVEMBER))
                    );
        }

+        @Override
        public HandBaggageInformation getFrom(String renderLanguage) {
            return oldMyCompanyHandBaggageInformationFactory.from(renderLanguage);
        }
    }
}

Then it’s time to extract each policy into its own file and move all the policies into a package, that, for the sake of giving meaningful names, we call policy. To do this, we need to duplicate our threshold constant into more than one policy implementation. This could be arguable as it is a duplication. Of course, there are alternatives to this, like making the constant public, but then we have the problem to decide where to put it.
So, given the nature of our real case problem, in which the date will pass soon, and its very urgent time to market, we decide to apply the ostrich algorithm (i.e. we are ignore this discussion) and prefer duplication over other solutions 🙈 🙉 🙊.

Also we move the creation of all the policies before the evaluation, for a reason you will understand in the next step. (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
        OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
                new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();

        MyCompanyOneWayAfterTheFirstOfNovember myCompanyOneWayAfterTheFirstOfNovember =
                new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
+       MyCompanyOneWayBeforeTheFirstOfNovember myCompanyOneWayBeforeTheFirstOfNovember =
+               new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
+       MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
+               new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
+       MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
+               MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);

        if (myCompanyOneWayAfterTheFirstOfNovember.canHandle(flight)) {
            return myCompanyOneWayAfterTheFirstOfNovember.getFrom(renderLanguage);
        }

-       MyCompanyOneWayBeforeTheFirstOfNovember myCompanyOneWayBeforeTheFirstOfNovember =
-               new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        if (myCompanyOneWayBeforeTheFirstOfNovember.canHandle(flight)) {
            return myCompanyOneWayBeforeTheFirstOfNovember.getFrom(renderLanguage);
        }

-       MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
-               new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        if (myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember.canHandle(flight)) {
            return myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember
                    .getFrom(renderLanguage);
        }

-       MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
-               MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        if (myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.canHandle(flight)) {
            return myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.getFrom(renderLanguage);
        }

        return notMyCompanyHandBaggageInformationFactory.make();
    }
}

5. Inject the chain at construction time

Now that we have all the policies created at the same point of our code, we can put all of them inside a list, loop over the list and just apply the first policy satisfying the condition. In this way, we remove the chain of if in favour of a chain of responsibility. (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
        OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
                new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();

        HandBaggageInformationPolicy myCompanyOneWayAfterTheFirstOfNovember =
                new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        HandBaggageInformationPolicy myCompanyOneWayBeforeTheFirstOfNovember =
                new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        HandBaggageInformationPolicy myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
                new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        HandBaggageInformationPolicy myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
                MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);

+       List<HandBaggageInformationPolicy> policies = Arrays.asList(
+               myCompanyOneWayAfterTheFirstOfNovember,
+               myCompanyOneWayBeforeTheFirstOfNovember,
+               myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember,
+               myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember
+       );
+
+       for (HandBaggageInformationPolicy policy : policies) {
+           if (policy.canHandle(flight)) {
+               return policy.getFrom(renderLanguage);
+           }
+       }

-       if (myCompanyOneWayAfterTheFirstOfNovember.canHandle(flight)) {
-           return myCompanyOneWayAfterTheFirstOfNovember.getFrom(renderLanguage);
-       }
-       
-       if (myCompanyOneWayBeforeTheFirstOfNovember.canHandle(flight)) {
-           return myCompanyOneWayBeforeTheFirstOfNovember.getFrom(renderLanguage);
-       }
-       
-       if (myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember.canHandle(flight)) {
-           return myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember
-                           .getFrom(renderLanguage);
-       }
-       if (myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.canHandle(flight)) {
-           return myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember.getFrom(renderLanguage);
-       }

        return notMyCompanyHandBaggageInformationFactory.make();
    }
}

If you prefer, you can use a stream instead of a classical loop.

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);

        NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
                new NewMyCompanyHandBaggageInformationFactory(translationRepository);
        OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
                new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();

        HandBaggageInformationPolicy myCompanyOneWayAfterTheFirstOfNovember =
                new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        HandBaggageInformationPolicy myCompanyOneWayBeforeTheFirstOfNovember =
                new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
        HandBaggageInformationPolicy myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
                new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
        HandBaggageInformationPolicy myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
                MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);

        List<HandBaggageInformationPolicy> policies = Arrays.asList(
                myCompanyOneWayAfterTheFirstOfNovember,
                myCompanyOneWayBeforeTheFirstOfNovember,
                myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember,
                myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember
        );
        
+       return policies.stream()
+               .filter(policy -> policy.canHandle(flight))
+               .findFirst()
+               .map(policy -> policy.getFrom(renderLanguage))
+               .orElse(notMyCompanyHandBaggageInformationFactory.make());

-       for (HandBaggageInformationPolicy policy : policies) {
-           if (policy.canHandle(flight)) {
-               return policy.getFrom(renderLanguage);
-           }
-       }
-        
-       return notMyCompanyHandBaggageInformationFactory.make();
    }
}

Now we are going to move the responsibility of creating the policies in a new class, named HandBaggagePoliciesFactory. The purpose here, is to have each class which performs a single operation. Does it sound familiar 🤔? No? Yes? Well, this is the Single Responsibility Principle 🤩.
Again, if you use Idea, you can use its Extract method object feature. I know, it’s getting kind of repetitive 😅 but this is one of the refactoring commands we use most frequently when we refactor code, so using it makes you save a lot of time. (Source code)

public class HandBaggageInformationFactory {
    public HandBaggageInformation from(Order order, TranslationRepository translationRepository, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);

-       NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
-               new NewMyCompanyHandBaggageInformationFactory(translationRepository);
-       OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
-               new OldMyCompanyHandBaggageInformationFactory(translationRepository);
        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
               new NotMyCompanyHandBaggageInformationFactory();

-       HandBaggageInformationPolicy myCompanyOneWayAfterTheFirstOfNovember =
-               new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
-       HandBaggageInformationPolicy myCompanyOneWayBeforeTheFirstOfNovember =
-               new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
-       HandBaggageInformationPolicy myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
-               new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
-       HandBaggageInformationPolicy myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
-               MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
-
-       List<HandBaggageInformationPolicy> policies = Arrays.asList(
-               myCompanyOneWayAfterTheFirstOfNovember,
-               myCompanyOneWayBeforeTheFirstOfNovember,
-               myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember,
-               myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember
-       );

+       List<HandBaggageInformationPolicy> policies = new HandBaggagePoliciesFactory().make(translationRepository);

        return policies.stream()
                .filter(policy -> policy.canHandle(flight))
                .findFirst()
                .map(policy -> policy.getFrom(renderLanguage))
                .orElse(notMyCompanyHandBaggageInformationFactory.make());
    }

+   private static class HandBaggagePoliciesFactory {
+       public List<HandBaggageInformationPolicy> make(TranslationRepository translationRepository) {
+           NewMyCompanyHandBaggageInformationFactory newMyCompanyHandBaggageInformationFactory =
+                   new NewMyCompanyHandBaggageInformationFactory(translationRepository);
+           OldMyCompanyHandBaggageInformationFactory oldMyCompanyHandBaggageInformationFactory =
+                   new OldMyCompanyHandBaggageInformationFactory(translationRepository);
+
+           HandBaggageInformationPolicy myCompanyOneWayAfterTheFirstOfNovember =
+                   new MyCompanyOneWayAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
+           HandBaggageInformationPolicy myCompanyOneWayBeforeTheFirstOfNovember =
+                   new MyCompanyOneWayBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
+           HandBaggageInformationPolicy myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember =
+                   new MyCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember(newMyCompanyHandBaggageInformationFactory);
+           HandBaggageInformationPolicy myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember = new
+                   MyCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember(oldMyCompanyHandBaggageInformationFactory);
+
+           return Arrays.asList(
+                   myCompanyOneWayAfterTheFirstOfNovember,
+                   myCompanyOneWayBeforeTheFirstOfNovember,
+                   myCompanyRoundTripAtLeastOneDepartureAfterTheFirstOfNovember,
+                   myCompanyRoundTripAllDeparturesBeforeTheFirstOfNovember
+           );
+       }
+   }
}

And after extracting the class HandBaggagePoliciesFactory in its own file, we are going to inject the policies as parameter at construction time of HandBaggageInformationFactory. In case of Idea, you can make the IDE work for you. You can just make the policies variable of from method become a field, with the command Extract field, decide to define it in the constructor and, then, simply use the Extract parameter feature in order to update all the constructors of your object.

public class HandBaggageInformationFactory {

+   private final List<HandBaggageInformationPolicy> handBaggageInformationPolicies;

+   public HandBaggageInformationFactory(List<HandBaggageInformationPolicy> handBaggageInformationPolicies) {
+       this.handBaggageInformationPolicies = handBaggageInformationPolicies;
+   }

    public HandBaggageInformation from(Order order, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);

        NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
                new NotMyCompanyHandBaggageInformationFactory();
                
-       List<HandBaggageInformationPolicy> policies = new HandBaggagePoliciesFactory().make(translationRepository);                

        return handBaggageInformationPolicies.stream()
                .filter(policy -> policy.canHandle(flight))
                .findFirst()
                .map(policy -> policy.getFrom(renderLanguage))
                .orElse(notMyCompanyHandBaggageInformationFactory.make());
    }

}

In our example, only the HandBaggageInformationFactoryTest has been updated. Notice, though, that we don’t need to perform changes on the tests at all. They still pass. (Source code)

public class HandBaggageInformationFactoryTest {

    private HandBaggageInformationFactory handBaggageInformationFactory;

    @Before
    public void setUp() {
        TranslationRepository translationRepository = Mockito.mock(TranslationRepository.class);
-        handBaggageInformationFactory = new HandBaggageInformationFactory();
+        handBaggageInformationFactory =
+                new HandBaggageInformationFactory(HandBaggagePoliciesFactory.make(translationRepository));
+        }
        
    //The rest is the same    
}

In order to maintain the same level of abstraction, we are going to inject also the NotMyCompanyHandBaggageInformationFactory into the HandBaggageInformationFactory.

public class HandBaggageInformationFactory {

    private final List<HandBaggageInformationPolicy> handBaggageInformationPolicies;
+   private final NotMyCompanyHandBaggageInformationFactory fallbackHandBaggageFactory;

-   public HandBaggageInformationFactory(List<HandBaggageInformationPolicy> handBaggageInformationPolicies) {
+   public HandBaggageInformationFactory(List<HandBaggageInformationPolicy> handBaggageInformationPolicies,
+                                        NotMyCompanyHandBaggageInformationFactory fallbackHandBaggageFactory) {
        this.handBaggageInformationPolicies = handBaggageInformationPolicies;
+       this.fallbackHandBaggageFactory = fallbackHandBaggageFactory;
+   }

    public HandBaggageInformation from(Order order, String renderLanguage, Integer flightId) {
        Flight flight = order.findFlight(flightId);
        
-       NotMyCompanyHandBaggageInformationFactory notMyCompanyHandBaggageInformationFactory =
-                       new NotMyCompanyHandBaggageInformationFactory();

        return handBaggageInformationPolicies.stream()
                .filter(policy -> policy.canHandle(flight))
                .findFirst()
                .map(policy -> policy.getFrom(renderLanguage))
                .orElse(fallbackHandBaggageFactory.make());
    }
}

And, then, again, the HandBaggageInformationFactoryTest gets updated. (Source code)

public class HandBaggageInformationFactoryTest {

 private HandBaggageInformationFactory handBaggageInformationFactory;

     @Before
     public void setUp() {
         TranslationRepository translationRepository = Mockito.mock(TranslationRepository.class);
         handBaggageInformationFactory =
            new HandBaggageInformationFactory(
                HandBaggagePoliciesFactory.make(translationRepository),
+                    new NotMyCompanyHandBaggageInformationFactory()
            );
         
     //The rest is the same    
    }
}

And, that’s it. You could keep on working on this piece of code to improve it more and more. But for post, we reached our goal, so no more refactoring 👏.

Conclusion

In this article we saw how to transform, step by step, a nested if structure into a chain of responsibility. The purpose of it is to make the code more readable and though easier to extend without introducing bugs. The main steps are flatten the if structure into a sequence of plain if clauses, extract each if clause and its correspondent effect into a separate class, extract an interface which is common to all the extracted classes, create an array containing all the classes, loop over it by using the first item of the list that is able to handle the case you are dealing with, inject the list of classes in order to make the class work with any combination of rules that you want.
Additionally, we saw some features of Idea IDE that allow us to perform most of the refactoring operations automatically, with only a few shortcuts.

Rich iOS notifications with Notification Content App Extension

In this post I will talk about iOS notification customization with Notification Content App Extension


During a workshop I recently attended I had the opportunity to explore a new interesting app extension type available in the iOS SDK: Notification Content App Extension. This extension has been added by Apple in iOS 10. By using this extension it is possible to customize the UI of your push and local notification. In this way you can show more engaging notification to your users :grin:.
Let’s see an example where we create a notification with custom UI using this extension type. To do that I will use some assets taken from the nasa photo journal. You can find the complete example in this github repository. Let’s start from the beginning: add a Notification Content Extension to your app. To do that just select the + button in the target section that appears after selecting the xcodeproj file.

add target

The extension that is created contains a NotificationViewController that adheres to the UNNotificationContentExtension. The documentation for this protocol says:

An object that presents a custom interface for a delivered local or remote notification. The UNNotificationContentExtension protocol provides the entry point for a notification content app extension, which displays a custom interface for your app’s notifications. You adopt this protocol in the custom UIViewController subclass that you use to present your interface. You create this type of extension to improve the way your notifications are presented, possibly by adding custom colors and branding, or by incorporating media and other dynamic content into your notification interface.

So if a UIViewController inside a Notification Content Extension adhere to the UNNotificationContentExtension we are able to access to the notification content and we can customize its UI. In the extension there’s also a MainInterface.storyboard that contains a single controller associated with the
NotificationViewController previously mentioned. We can use this storyboard to customize the notification UI using interface builder. So let’s start by defining the interface in the storyboard. Below in the image there’s the final result.

notification storyboard

Then we can fill the UI with the notification content we receive in the func didReceive(_ notification: UNNotification) of the NotificationViewController controller that implements the protocol shown above. Below you can find its source code.

import UserNotifications
import UserNotificationsUI

class NotificationViewController: UIViewController, UNNotificationContentExtension {
    @IBOutlet weak var nasaImage: UIImageView!
    @IBOutlet weak var nasaTitle: UILabel!
    @IBOutlet weak var nasaDescription: UILabel!
    
    func didReceive(_ notification: UNNotification) {
        if let validTitle = notification.request.content.userInfo["title"] as? String,
            let validDescription = notification.request.content.userInfo["description"] as? String,
            let validImageName = notification.request.content.userInfo["imageName"] as? String {
            nasaTitle.text = validTitle
            nasaDescription.text = validDescription
            nasaImage.image = UIImage(named: validImageName)
        }
    }
}

Now to test our controller and our new notification UI, we can create a local notification. We will use some data taken from the nasa photo journal. We will create a NasaLocalNotificationBuilder that contains all the notification creation logic. This is the final result.

import UserNotifications

class NasaLocalNotificationBuilder {
    private let notificationCenter: UNUserNotificationCenter = UNUserNotificationCenter.current()
    private var notificationActions: [UNNotificationAction] = []
    private var notificationContent = UNMutableNotificationContent()
    
    func setActions() -> NasaLocalNotificationBuilder {
        notificationActions.append(
            UNNotificationAction(identifier: "view",
                                 title: "View Photo in app",
                                 options: [.foreground, .authenticationRequired])
        )
        notificationActions.append(
            UNNotificationAction(identifier: "skip",
                                 title: "Skip",
                                 options: [])
        )
        return self
    }
    
    func setCategory() -> NasaLocalNotificationBuilder {
        let notificationCategory = UNNotificationCategory(identifier: "NasaDailyPhoto",
                                                          actions: notificationActions,
                                                          intentIdentifiers: [],
                                                          hiddenPreviewsBodyPlaceholder: "",
                                                          options: .customDismissAction)
        notificationCenter.setNotificationCategories([notificationCategory])
        return self
    }
    
    func setContent() -> NasaLocalNotificationBuilder {
        notificationContent.title = "Your Nasa Daily Photo"
        notificationContent.body = "Long press to see you daily nasa photo"
        notificationContent.sound = UNNotificationSound.default()
        notificationContent.categoryIdentifier = "NasaDailyPhoto"
        notificationContent.userInfo = [
            "title" : "At the Heart of Orion",
            "description" : """
            Near the center of this sharp cosmic portrait, at the heart of the Orion Nebula, are four hot, massive
            stars known as the Trapezium. Gathered within a region about 1.5 light-years in radius, they dominate the
            core of the dense Orion Nebula Star Cluster. Ultraviolet ionizing radiation from the Trapezium stars,
            mostly from the brightest star Theta-1 Orionis C powers the complex star forming region's entire visible
            glow. About three million years old, the Orion Nebula Cluster was even more compact in its younger years
            and a recent dynamical study indicates that runaway stellar collisions at an earlier age may have formed
            a black hole with more than 100 times the mass of the Sun. The presence of a black hole within the cluster
            could explain the observed high velocities of the Trapezium stars. The Orion Nebula's distance of some
            1,500 light-years would make it the closest known black hole to planet Earth.
            """,
            "imageName" : "orion"
        ]
        return self
    }
    
    func build() {
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
        let request = UNNotificationRequest(identifier: "NasaDailyPhoto",
                                            content: notificationContent,
                                            trigger: trigger)
        notificationCenter.add(request, withCompletionHandler: nil)
    }
}

Then we can call the notification builder in the main controller of the app ViewController to generate the notification.

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        NasaLocalNotificationBuilder()
            .setActions()
            .setCategory()
            .setContent()
            .build()
    }
}

Now we have to go back in our notification content extension and set some attribute in its info.plist. In particular we have to set:

  • UNNotificationExtensionCategory to the notification category that we want to customize. In our case we use NasaDailyPhoto, the value we previously set in the builder
  • UNNotificationExtensionDefaultContentHidden to hide the standard notification content when the user long press the notification.
  • if needed it is possible also to set the UNNotificationExtensionInitialContentSizeRatio to a value different than 1 to customize the aspect ration of our notification.

We’re ready to test our implementation. Below you can find a video with the final result of our implementation.

That’s all for Notification Content App Extension on iOS. Let’s start to test them in your app :relaxed:.

Blender tutorial: advanced modeling

In this new post of the series Blender tutorial I will talk about advanced modeling in Blender.


In the previous post of the series “Blender tutorial” we talked about the basics of modeling in Blender. In this post we will study Blender advanced modeling tools.
Let’s start from modifiers. All the operation we saw in the previous modeling post were all “one time operation”: after we apply them we can’t change their properties to modify the result we obtain. Modifiers let us apply modification to a mesh that is possible to revert, modify and preview. When we are sure about the result, we can apply the modifier to get the mesh modification we want.
We can apply a modifier by selecting an object (right click on it) and go under the properties panel and click on the wrench icon. Here we can select a modifier from the list Add modifier.

blender add modifier

For example let’s try to add a simple deform to a cube. After adding it we see our cube deformed. We can than tweak the parameters of the deformation as we prefer. Each modifier as its own properties, based of which type of modification it applies to the mesh. Anyway there are some common button to:

  • show/hide the modifier result during rendering
  • show/hide the modifier result in the 3D window
  • show/hide the modifier result in the 3D window in Edit mode
  • show/hide the cage of the modifier in the 3D window

blender add modifier icons

After that we have the subdivision surface. As stated on Wikipedia, subdivision surface is a technique discovered simultaneously by Edwin Catmull and Jim Clark:

A subdivision surface, in the field of 3D computer graphics, is a method of representing a smooth surface via the specification of a coarser piecewise linear polygon mesh. The smooth surface can be calculated from the coarse mesh as the limit of recursive subdivision of each polygonal face into smaller faces that better approximate the smooth surface.

This basically means that with subdivision surface we can approximate smoother surfaces starting from another one.
In Blender we can apply subdivision surface as a modifier.

blender add subdivision surface

After the subdivision is applied it is possible to modify the mesh only on the original edges and faces of the mesh. In fact this elements will be the only one that we can select from our mesh.

blender added subdivision surface

Another interesting modifier is the mirror modifier. It is especially useful when you need to do Symmetric modeling. After applying this modifier every operation you made on one side of your mesh will be mirrored on the other side. For this reason, especially when you’re modeling some character/organic mesh, it is useful to delete one side of the mesh and mirror the other one (with all the modification you do). In the option of the modifier you can choose on which axis the mirroring must be applied.

blender mirror modifier

The last interesting modifier (for now) we will see it is the boolean modifier. This modifier let us modify meshes by calculate difference, union and intersection between them.

blender boolean modifier blender boolean modifier operation option

Another interesting tool is the “Loop Cut and Slide”. With this option we can add edge loops to our mesh. This tool is useful especially when you need to have more mesh polygon to model the mesh details you want.

blender loop edge and slide

Then we have the joining mesh tool and the stitching vertices tool. For the first one, as the name suggest, you can combine multiple mesh into a single one in blender. To join two or more meshes, we have to select them while we are object mode. Remember: the last one selected will be the mesh to which the other ones will be joined. After that we can apply the join mesh using:

  • the menu option Object -> Join
  • the shortcut “ctrl + j keys”

blender join meshes

After joining two or more meshes, we will need to fill the gap between then. We can do that by using the merge tool, the remove double tool and the snap element tool to stitch the vertices that are on the border of the meshes. The vertices must be open: they need to be without a connection on one side. So after selecting one vertex we can snap it using the snap element tool. We can activate it using the button in the bottom bar of the 3D window. After activating it we can grab the selected vertex by pressing the g key, and snap it to our target vertex.

blender snap element

After snapping vertices we have to remove duplicates. We can do it using the merge tool or using the remove doubles tool. We can find these tools:

  • *under Mesh -> *
  • from the contextual menu that you can show by using the shortcut ctrl + v key for vertices (NB.: there are also contextual menus for faces and edges that we can show using ctrl + f key and ctrl + e key)

If we decide to go with the remove doubles tools, after applying the operation a popup at the top of the 3D window will appear that will tell us the number of duplicated vertices removed.

blender merge remove tools

In blender it is possible to model also text. We can add text by selecting Add -> Text. We can modify the text by selecting the edit mode: a text cursor will appear so that we can modify the text. In the property panel a special tab will let as customize the text property.

blender add text blender text properties

Last but not least is the vertex groups tool. This tool allow us to save selection of vertices so that we are able to select them later. We can create a vertex group in the property panel in the corresponding tab. In this tab we have buttons to:

  • add/remove vertices to a group
  • select/deselect the vertices of a group

blender vertex group

In the next post we will discuss about outliner, layers, groups, hierarchies and scenes.

Design thinking and Design sprint: embrace you creativity

In this new post I will talk about my experience with design sprint and design thinking during a workshop I attended.


As I already told you in a previous post, in the last few months I worked in the team Lynch at lastminute.com group on a big “app relaunch” project where we basically rewrote some sections of our main mobile apps using React Native and we did a general restyle of them with a more modern and engaging design. As a reward for the hard work the team did, our customer area manager Elena Bianchini, a manager with many years of experiences in big companies (e.g. Vodafone), decided to give us a two days retreat with a one day workshop in the location of Castelnuovo Calcea (Asti) at the bed & breakfast “Cascina Il Quercin”. Below you can find some photos of this beautiful location (yes, I want you to be envy :grin:)

Cascina Il Quercin Castelnuovo Calcea

What was the main topic of the workshop? Well, in this one day workshop our UX team shown us what is the Design Thinking and the Design Sprint framework and we tried to apply it to came up with a solution for one of the next big projects we have on the mobile app. Our UX team is composed by:

  • Diletta Assandri is a senior UX designer with many years of experiences in project all over the world with Accenture Italia. She is truly passionate for her work. She is one of the first designer I worked with that listen to what the developers have to say (“NON SI PUÒ FARE” (that we could translate as “this can’t be done”) is one of her biggest nightmares :laughing:).
  • Federica Pisuttu is the last new entry in the team Lynch. She is a Senior UX designer with many years of experience in big companies like Assist Digital and she also collaborate with various digital startup.

Let’s start from the definition of design thinking and design sprint:

Design thinking is a method for the practical, creative resolution of problems using the strategies designers use during the process of designing. Design thinking has also been developed as an approach to resolve issues outside of professional design practice, such as in business and social contexts. The design thinking process has various stages including: defining the problem, researching, forming ideas, prototyping and testing. The steps can occur simultaneously and be repeated.

A design sprint is a five-phase framework that helps answer critical business questions through rapid prototyping and user testing. Sprints let your team reach clearly defined goals and deliverables and gain key learnings, quickly. The process helps spark innovation, encourage user-centered thinking, align your team under a shared vision, and get you to product launch faster.

So design thinking is an approach to design product based on iterations of a series of step that includes: acquire a deep knowledge of the problem, prototyping and user test your new product ideas. The design sprint is the formalisation of design thinking: it is a time constrained process (usually 5 days) where all the design thinking steps are applied in sequence. During the workshop we tried to simulate a design sprint. We had just one day, so we compressed all the phases of a sprint in just a few hours :grin:.

agenda

We created two separated teams and then we started our design sprint. Let’s see the various phases we went through and the final result.

Understand

In the first phase of our design sprint we tried to understand the problem we were approaching. To do this we used an exercise called “A day in a life”. It helped us to think like our users. To apply it:

  • Start with the user’s first step or entrance into your product experience.
  • Add each step in the journey until the user’s goal has been reached.
  • Include descriptions for each step and highlight pain points along the journey.

By the end of this activity we had been able to highlight all the weakest/negative points of the section of the app we analyzed.

Crazy 8

The second phase is the crazy 8. Let’s see how our UX designers explained us this activity:

Fold your paper into 8 rectangles. Sketch 1 idea in each rectangle rather than a storyboard. Go for quantity, don’t worry about making these beautiful, these are just for yourself. Try to get beyond your initial idea.

So basically we tried to sketch out some ideas of how we could improve the section we analyzed in previous step by creating some sketch. As the definition says, in this phase it is better to favor quantity over quality of the sketch. The more ideas you get, the better it is. So everyone in the two teams started to draw at the speed of light like a 10 years old children :laughing:.

crazy 8

One big idea

The next phase is called “one big idea”. In this phase we had to choose some of the ideas we created during the crazy 8 and try to go deeper in the details. It is basically a selection of ideas: you choose what you think are the best ideas you created in the crazy 8 phase and you create a new prototype with a little bit more of details. As our UX designers explained us we had to follow these key points:

  • Start outside of the software.
  • Focus on emotions.
  • Enough fidelity to start building a prototype.

one big idea

Decide

In this phase we decided which one were the best ideas created in the previous phases. We decided by doing another exercise, the “Share & Vote”. Basically we had 3 minutes to present our idea. After the presentations each team member got “budget” of 3 dot stickers: we had to use them to vote what we thought was the best ideas. After voting the best 3 ideas were chosen as the base to create a detail prototype. So the final prototype is a mix of the best ideas created during the sprint.

decide

Prototyping

In this phase you need to create some prototype. Which kind of prototype? Let’s see again how our UX designer explained us the features of good prototype:

Your Sprint is an attempt to identify the solution to a problem. Your prototype is a chance to see how good the ideas from your Decide phase are. To do this you have to make it real enough that you can get a realistic response from a potential user when you’re in the Validate phase.

So in this phase we had to go much deeper in the details of our ideas to get a working prototype that could be used as a test with some real user. We used some of the famous tools available on the market, like balsamiq for example, and for some parts of our prototype we created a real demo.

Conclusion

That was the end of our workshop (we didn’t have enough real user for the validation test). It was very interesting to be a UX designer just for one day. Honestly, a lot of time I found myself thinking: how can I do a job where I can use and improve my creativity? Software development requires a lot of creativity, but it is not the same kind of creativity that is required to be a UX designer :relieved:. I don’t think I will ever be a designer but it was a good starting point to let me try to enter in the mindset of a UX designer (and also life is unpredictable so who knows? :blush:). Anyway…after the hard work…we had some relaxing moments :relieved:.

relax

To be noted, in the right lower corner you can see Alessandro Romano during some swim stunts with the lastminute.com mascot :laughing:. He is “the clean” a senior frontend developer with many years of experience that just got graduated from University of Insubria in Varese (student/worker at the same time like I was some years ago :blush:). So, what do you think about design thinking and design sprint? Let me know in the comments section below :blush:.

Eat Your Own Dog Food: distribute a beta version of your iOS app using Apple Enterprise Developer Program

In this post I will show you how to distribute a beta version of your iOS app using Apple Enterprise Developer Program.


In a previous post I talked about the “app relaunch” project I was involved in the last few months at lastminute.com group. During one of the last sprint before the release we started to think about putting in place an internal beta testing program to have more testing data and eventually discover more bugs before the release on the App Store (we really wanted to eat our food :smile:). We thought about using the TestFlight environment with external testers. But this solution didn’t work for us because:

  • we wanted to let everyone in the company be able to install the beta version without the need for any kind of registration. The external testers in the TestFlight environment must be registered in the “External Tester” section of you app in the App Store Connect (someone said iTunes Connect?!?!?! :stuck_out_tongue_closed_eyes:).
  • we wanted to associate the beta with a new bundle id so that we could have a custom monitoring section in the Fabric dashboard.
  • we didn’t want to go through the “light review” that Apple does each time you want to distribute a new build to your external tester. We also wanted to avoid the slow processing timing of TestFlight.

So what do we did? We put in place our custom Beta distribution program using Apple Enterprise Developer Program. By enrolling in this program you have the ability to distribute you app inside your company without the need of the App Store. You can basically create your own Company App Store!! :open_mouth: I worked on the creation of this custom internal Beta distribution program with my colleague Giordano Tamburrelli. Giordano is the Team Leader of the Lynch Team, the agile team where I work at lastminute.com group. He has a Phd in Computer Science at Politecnico di Milano university and a master degree in Computer Science at the University of Illinois. He is one of the most skilled person I ever met, both from a technical and managerial point of view (yes you know, I’m just kissing his ass :kissing_heart::stuck_out_tongue_closed_eyes:). Obviously, he is also one of the biggest nerd you could ever met :cupid::sparkling_heart:. So how did we decide to proceed? We basically did the following operation to put in place our beta program:

  • Create a new bundle identifier and a new provisioning profile in our enterprise account.
  • Create inside our app a new configuration with some customization for the Beta build.
  • Add some new scripts to automatize the creation and the upload of new beta ipa using Fastlane, Jenkins (CI), maven (for the upload) and Nexus as our repository manger (we already use all these platforms for the distribution of our release version and to store development snapshots of the ipa).
  • Put in place a mini website, configured specifically for the distribution of the beta app.

To show you the details and some screenshots of the operation we made I will use a sample project SampleBetaApp with bundle identifier it.chicio.SampleBetaApp.
Let’s start from the first step: configuration on the Apple developer account for our new enterprise program. As already explained before, we decided to create a new bundle identifier for the beta app and obviously a new provisioning profile. The procedure is basically the same of the standard program for both the bundle identifier and the provisioning profile. We just need to follow the instruction in the Apple Developer Account site in the section “Certificates, Identities & Profiles” (and maybe there’s a high chance that, if you’re reading this post you already release at least one app on the Apple app store so you already know the stuff to do :smirk:).

enterprise profiles

After that we created in our iOS project a new Beta configuration by duplicating the release one. In this way we were able to generate an ipa similar to the release one and were also able to customize some settings of our app.

beta configuration

The Build Setting that we customized are:

  • Obviously the bundle identifier. Here we used the one created in the first step (Apple Enterprise developer Account configuration). The entry that we modified in the Build settings is named “Product Bundle Identifier”.

beta bundle identifier

  • The app icon. You can add a new asset in your main assets catalog. The entry that we modified in the Build settings is named “Asset Catalog App Icon Set Name”.

beta icon

  • We added a new custom preprocessor macro in the “Preprocessor Macros” build setting to be able to detect in our source code that we are running a beta version and eventually do some custom stuff. For example we added the build version number in the about section only for the beta version. In this way our tester were able to tell us exactly which version they were using. This macro is visible only to Objective-C. If you need to do the same thing in Swift you need to customize the “Other Swift Flags” field.

beta preprocessor macro

Then we created the scripts needed to automatize the build on Jenkins and the upload of our artifacts repository to Nexus. As I said before we were already using Fastlane to automatize the releases of our app to the store. For the beta publishing process we decided to create a new lane in our Fastfile where we launch a custom script to do all the operation needed to publish our beta.

lane :create_beta_ipa do |options|
  sh './scripts/build_beta.sh'
end

As you can see below, in the script build_beta.sh we do three things:

  • We generate an archive of our app using the xcodebuild archive command using the Beta configuration we created before. We customized some other options. For example the archive path. At the end of this command we obtain a file named SampleBetaApp.xcarchive in the folder <project root>/DerivedData/Beta.
  • Then we exporte the ipa by using the xcodebuild -exportArchive command. This command needs and exportOptionsPlist file that contains some exporting options, including the provisioning profile name that must corresponds to the name of the provisioning profile we created at the beginning in the Apple Enterprise Developer account. Below the script you can find the exportOptions-SampleBetaApp.plist sample file for the SampleBetaApp project.
  • Last but not least, when the ipa is ready we upload it on Nexus using Maven (in the script below you can find some placeholder for some of the parameter used :wink:). The command is simple and clear: we are uploading our artifact, the ipa, without generating any pom version. One important thing to note: we use always 1 as version, because we decided that we don’t want to store the beta versions history. We just want to keep the last one we publish.
#!/usr/bin/env bash

TARGET="SampleBetaApp"
DERIVED_DATA_BETA_PATH="../DerivedData/Beta/"
BETA_TARGET_PATH=$DERIVED_DATA_BETA_PATH$TARGET
XARCHIVE="${BETA_TARGET_PATH}/${TARGET}.xcarchive"
IPA="${BETA_TARGET_PATH}/${TARGET}.ipa"

xcodebuild -workspace ../Container.xcworkspace
            -scheme "$TARGET"
            -sdk iphoneos
            -configuration Beta
            archive -archivePath "$XARCHIVE"

xcodebuild -exportArchive
            -archivePath "$XARCHIVE"
            -exportOptionsPlist "exportOptions-${TARGET}.plist"
            -exportPath "$BETA_TARGET_PATH"

mvn deploy:deploy-file -DgroupId="<group id project identifier>"
                        -DartifactId="$TARGET"
                        -Dversion=1
                        -DgeneratePom=false
                        -DrepositoryId=nexus
                        -Durl="<nexus repository url>"
                        -Dfile="$IPA"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>method</key>
        <string>enterprise</string>
        <key>signingCertificate</key>
        <string>iOS Distribution</string>
        <key>provisioningProfiles</key>
        <dict>
            <key>it.chicio.BETA.SampleBetaApp</key>
            <string>SampleApp Beta Distribution</string>
        </dict>
    </dict>
</plist>

At this moment we were ready to create the new Jenkins job to build our beta. We decided to trigger it using Jenkins webhook triggers. In this way we were able to trigger the build and release of a new beta by just calling an url. This job basically clone our app repository and then execute the lane create_beta_ipa that we defined before in the Fastlane Fastfile.
So in the end with all this steps above we obtained an ipa reachable at a public url (on Nexus). We decided to publish our beta using Github Pages (the same service that runs this website :hearts:). Why? Because we needed a server with HTTPS already configured. Github pages let us create the beta website really fast. So we created a new account and published a new html that contains the following code:

<html>
    <head>
    </head>
    <body>
        <p>
            <br />
            <br />
            <a href="itms-services://?action=download-manifest&url=<a base url>manifest.plist">
                <p>Install the SampleAppBeta app</p>
            </a>
        </p>
    </body>
</html>

As you can see above, in this html there’s a special link with the protocol itms-services. If a user clicks on this link from Safari browser on a iOS device, the download and the installation of the app will starts. If you look carefully at the content of the link above, you will notice there’s a reference to a url of a manifest.plist file. This is a file generated by Xcode if you export an archive with an enteprise certificate and contains some metadata for the ipa, including the location/url of the ipa to be downloaded. In our case this url was the Nexus link to the ipa. This file can be generated the first time and eventually edited if there’s any change. This is a sample manifest file that could be used for our SampleBetaApp.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>items</key>
        <array>
            <dict>
                <key>assets</key>
                <array>
                    <dict>
                        <key>kind</key>
                        <string>software-package</string>
                        <key>url</key>
                        <string>http://www.example-url-to-where-the-ipa-is.com/SampleBetaApp.ipa</string>
                    </dict>
                    <dict>
                        <key>kind</key>
                        <string>display-image</string>
                        <key>url</key>
                        <string>https://www.example.com/image.57x57.png</string>
                    </dict>
                    <dict>
                        <key>kind</key>
                        <string>full-size-image</string>
                        <key>url</key>
                        <string>https://www.example.com/image.512x512.png</string>
                    </dict>
                </array>
                <key>metadata</key>
                <dict>
                    <key>bundle-identifier</key>
                    <string>it.chicio.SampleBetaApp</string>
                    <key>bundle-version</key>
                    <string>1.0.0</string>
                    <key>kind</key>
                    <string>software</string>
                    <key>title</key>
                    <string>beta-sample.com</string>
                </dict>
            </dict>
        </array>
    </dict>
</plist>

Now we were ready to distribute our beta app, and you know everything is needed to create your own beta program. One last thing: you will need to explain to the less experienced user that they need to accept the enterprise provisioning profile from Settings -> Profiles & Device Management. If they don’t do it they will see the alert contained in the screenshot below.

enterprise untrusted developer

That’s it!!! Go to your boss and tell her/him you’re ready to create you custom iOS beta internal program!!! :sunglasses::apple: