Потеря полиморфизма при использовании Java лямбда-выражений

В рамках серии мастер-классов IT-гуру, которые организовывает Luxoft Training, предлагаем познакомиться с переводом статьи Якова Файна «Losing Polymorphism with Java Lambda Expressions».В своей статье Яков показывает, как решить одну и ту же задачу с помощью применения объектно-ориентированного подхода и использования лямбда-выражений. И доказывает, что потеря полиморфизма не всегда плохо сказывается на коде.

Об автореЯков Файн — один из основателей двух стартапов: IT-консалтинговой компании Farata Systems и компании по разработке ПО SuranceBay; Java Champion, организатор Princeton Java Users Group. Автор и соавтор большого числа технических книг по программированию (например, Enterprise Web Development, O«Reilly, 2014, Java 24-Hour Trainer, Wrox, 2011).

Вчера я решил переписать один из примеров, который я привожу при объяснении полиморфизма во время моих Java-тренингов, используя лямбда-выражения. Я понимаю, что лямбда-выражения типичны для функционального стиля программирования, в то время как полиморфизм является одним из важнейших объектно-ориентированных принципов. Тем не менее я решил попробовать смешать ООП и функциональный стили, чтобы посмотреть, что получится.

Задача

Класс Person имеет 2 подкласса: Employee и Contractor. Также есть метод интерфейсов Payable, который используется для увеличения зарплаты рабочим.

public interface Payable {int INCREASE_CAP = 20; boolean increasePay (int percent);}

И Employee, и Contractor выполняют метод Payable, но исполнение функции increasePay () должно отличаться. Вы можете увеличивать зарплату Employee на любой процент, но увеличение оплаты Contractor должно быть ограничено значением переменной INCREASE_CAP.

Объектно-ориентированная версия.

Класс Person:

public class Person {private String name; public Person (String name){this.name=name;}

public String getName (){return «Person’s name is » + name;}}

Класс Employee:

public class Employee extends Person implements Payable{

public Employee (String name){super (name);}public boolean increasePay (int percent) {System.out.println («Increasingry by » + percent + »%.»+ getName ()); return true;}}

Класс Contractor:

public class Contractor extends Person implements Payable {

public Contractor (String name){super (name);}public boolean increasePay (int percent) {if (percent < Payable.INCREASE_CAP){System.out.println(«Increasingly rate by » + percent +"%. "+ getName());return true;} else {System.out.println(«Sorry't increase hourly rate by more than » +Payable.INCREASE_CAP + "%. "+ getName());return false;}}}

Код, описывающий увеличение зарплаты с использованием полиморфизма:

public class TestPayInceasePoly {

public static void main (String[] args) {

Payable workers[] = new Payable[3]; workers[0] = new Employee («John»); workers[1] = new Contractor («Mary»); workers[2] = new Employee («Steve»);

for (Payable p: workers){p.increasePay (30);}}}

Вывод программы выглядит так:

Increasing salary by 30%. Person«s name is JohnSorry, can«t increase hourly rate by more than 20%. Person«s name is MaryIncreasing salary by 30%. Person«s name is Steve

Введение лямбдТеперь я решил поэкспериментировать с лямбдами. А именно: я хотел передать функцию в качестве аргумента метода. Я хотел извлечь логику увеличения зарплаты из классов Employee и Contractor и с помощью лямбда-выражения передать ее в массив workers в качестве аргумента его методов.

Я оставил код описания метода интерфейса Payable без изменений.Ниже новое описание класса Person, которое включает в себя метод validatePayIncrease, который будет содержать лямбда-выражения в качестве первого аргумента (передача значения функции методу):

public class Person {

private String name;

public Person (String name){this.name = name;}

public String getName (){return name;}

public boolean validatePayIncrease (Payable increaseFunction, int percent) {

boolean isIncreaseValid= increaseFunction.increasePay (percent);

System.out.println (» Increasing pay for » + name + » is » +(isIncreaseValid? «valid.»: «not valid.»)); return isIncreaseValid;}}

Новая версия описания класса Employee не выполняет Payable:

public class Employee extends Person{

//некоторый другой код, специфичный для Employee, представлен здесьpublic Employee (String name){super (name);}}

Новая версия описания класса Contractor также не выполняет Payable:

public class Contractor extends Person{

//некоторый другой код, специфичный для Contractor, представлен здесьpublic Contractor (String name){super (name);}}

Наконец, ниже представлена программа, которая позволяет увеличить зарплату всем работникам (workers), передавая различные лямбда-выражения в Employee и Contractor.

public class TestPayIncreaseLambda {

public static void main (String[] args) {

Person workers[] = new Person[3]; workers[0] = new Employee («John»); workers[1] = new Contractor («Mary»); workers[2] = new Employee («Steve»);

//Лямбда-выражение для увеличения зарплаты EmployeePayable increaseRulesEmployee = (int percent) → {return true;};

//Лямбда-выражение для увеличения зарплаты ContractorPayable increaseRulesContractor = (int percent) → {if (percent > Payable.INCREASE_CAP){System.out.print (» Sorry, can’t increase hourly rate by more than » +Payable.INCREASE_CAP + »%.»); return false;} else {return true;}};

for (Person p: workers){if (p instanceof Employee){//Проверка 30% увеличения зарплаты для каждого сотрудникаp.validatePayIncrease (increaseRulesEmployee, 30);} else if (p instanceof Contractor){p.validatePayIncrease (increaseRulesContractor, 30);}}}

}

Как видите, я передаю одно или другое лямбда-выражение в метод validatePayIncrease. Запуск программы выдает следующий вывод:

Increasing pay for John is valid.Sorry, can«t increase hourly rate by more than 20%. Increasing pay for Mary is not valid.Increasing pay for Steve is valid.

Это работает, но все-таки мне больше нравится моя объектно-ориентированная версия, чем версия с использованием лямбда.1. В ООП-версии я исполняю принцип: как Employee, так и Contractor должны реализовать метод Payable. В версии лямбда — это ушло на уровень класса. Строгая типизация все еще существует в версии лямбда тип первого аргумента выражения validatePayIncrease соответствует методу Payable.2. В объектно-ориентированной версии я не использовал проверку типов, но в лямбда-версию этот ужасный instanceof все-таки пробрался.

Здорово, что я могу передать функцию объекту и выполнить ее там, но цена кажется высокой. Давайте посмотрим, можно ли улучшить этот код.

Избавляемся от иерархии классов

В настоящее время коды описания класса Employee и Contractor одинаковые. В таком случае, если их единственное различие — это реализация метода validatePayIncrease, мы можем удалить иерархию наследования и просто добавить свойство workerStatus логического типа (boolean) классу Person, чтобы отличить Employee от Contractor.Давайте избавимся от классов Employee и Contractor и изменим класс Person. Я добавлю второй аргумент в конструктор workerStatus.

public class Person {

private String name; private char workerStatus; // 'E' or 'C'

public Person (String name, char workerStatus){this.name = name; this.workerStatus=workerStatus;}

public String getName (){return name;}

public char getWorkerStatus (){return workerStatus;}

public boolean validatePayIncrease (Payable increaseFunction, int percent) {

boolean isIncreaseValid= increaseFunction.increasePay (percent);

System.out.println (» Increasing pay for » + name + » is » +(isIncreaseValid? «valid.»: «not valid.»)); return isIncreaseValid;}}

Код класса TestPayIncreaseLambda сейчас станет проще. Нам не нужно хранить объекты разного типа в массиве workers и мы можем избавиться от instanceof:

public class TestPayIncreaseLambda {

public static void main (String[] args) {

Person workers[] = new Person[3]; workers[0] = new Person («John», 'E'); workers[1] = new Person («Mary», 'C'); workers[2] = new Person («Steve», 'E');

//Лямбда-выражение для увеличения зарплаты EmployeePayable increaseRulesEmployee = (int percent) → {return true;};

//Лямбда-выражение для увеличения зарплаты Contractor

Payable increaseRulesContractor = (int percent) → {if (percent > Payable.INCREASE_CAP){System.out.print (» Sorry, can’t increase hourly rate by more than » +Payable.INCREASE_CAP + »%.»); return false;} else {return true;}};

for (Person p: workers){if ('E'==p.getWorkerStatus ()){// Validate 30% increase for every worker//Проверка 30% повышения для любого сотрудникаp.validatePayIncrease (increaseRulesEmployee, 30);} else if ('C'==p.getWorkerStatus ()){p.validatePayIncrease (increaseRulesContractor, 30);}}}}

Если будет введен новый тип работника (например, иностранных рабочих), нам просто необходимо добавить еще одно лямбда-выражение к классу TestPayIncreaseLambda, который реализует бизнес-правила для иностранных работников.Любителям идеального кода может не понравиться тот факт, что я использую неизменяемые 'E' и 'C'. Вы можете добавить пару результирующих переменных EMPLOYEE и CONTRACTOR в верхнем уровне этого класса.

Так каков вердикт? Потерять полиморфизм может быть не так плохо. Код стал проще, мы удалили два класса, но не потеряли соответствие типов (Payable-интерфейс). Разработчики программного обеспечения, которые используют функциональные языки программирования, живут без полиморфизма и не скучают по нему.

P.S. На самом деле вы можете избавиться также от метода Payable. Это потребует другой реализации наших лямбда-выражений с помощью интерфейсов от нового пакета java.util.function. Но это должно быть темой отдельного блога. Ну, хорошо, вот подсказка: я сделал это с помощью интерфейса BiFunction.

Ближайший тренинг Якова Файна «Практическая разработка веб-приложений на Javascript и AngularJS» пройдет 8–11 декабря 2014 года в online-формате.Тренинг посвещен практической разработке клиентской части веб-приложений. Обучение будет проходить в среде, максимально приближенной к реальной. 70% времени отводится на лекции и 30% на отработку практических навыков. К концу тренинга слушатели создадут пробное одностраничное веб-приложение онлайн-магазина, которое будет использовать макетные данные в формате JSON. Полученные навыки могут применяться для разработки клиентских частей веб-приложений вне зависимости от используемой технологии серверной части.

Узнать больше о тренинге и зарегистрироваться можно здесь.

© Habrahabr.ru