В рамках серии мастер-классов 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 John
Sorry, can’t increase hourly rate by more than 20%. Person’s name is Mary
Increasing 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»);
//Лямбда-выражение для увеличения зарплаты Employee
Payable 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 (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');
//Лямбда-выражение для увеличения зарплаты Employee
Payable 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. Полученные навыки могут применяться для разработки клиентских частей веб-приложений вне зависимости от используемой технологии серверной части.
Узнать больше о тренинге и зарегистрироваться можно здесь.
Автор: Evgenia_s5