Статьи

Абстракция в Java – ULTIMATE Tutorial (PDF Download)

ПРИМЕЧАНИЕ РЕДАКЦИИ : В этом посте мы представим всеобъемлющее руководство по абстракции в Java. Абстракция происходит во время проектирования на уровне класса с целью скрыть сложность реализации того, как были реализованы функции, предлагаемые API / design / system, в некотором смысле упрощая «интерфейс» для доступа к базовой реализации. Этот процесс может повторяться на все более «высоких» уровнях (уровнях) абстракции, что позволяет создавать большие системы без увеличения сложности кода и понимания на более высоких уровнях.

1. Введение

В этом уроке мы познакомимся с абстракцией в Java и определим простую систему начисления заработной платы с использованием интерфейсов, абстрактных классов и конкретных классов.

В Java существует два уровня абстракции — интерфейсы , используемые для определения ожидаемого поведения, и абстрактные классы , используемые для определения неполной функциональности.

Теперь мы рассмотрим эти два различных типа абстракции подробно.

2. Интерфейсы

Интерфейс похож на контракт. Это обещание обеспечить определенные поведения, и все классы, которые реализуют интерфейс, гарантируют также реализацию этих поведений. Чтобы определить ожидаемое поведение, интерфейс перечисляет несколько сигнатур методов. Любой класс, который использует интерфейс, может полагаться на те методы, которые реализуются в классе времени выполнения, который реализует интерфейс. Это позволяет любому пользователю, использующему интерфейс, знать, какая функциональность будет предоставлена, не беспокоясь о том, как эта функциональность будет фактически достигнута. Детали реализации скрыты от клиента, это важнейшее преимущество абстракции.

2.1. Определение интерфейсов

Вы можете использовать ключевое слово interface для определения интерфейса:

1
2
3
4
5
6
7
8
9
public interface MyInterface {
 
void methodA();
 
int methodB();
 
String methodC(double x, double y);
 
}

Здесь мы видим определенный интерфейс с именем MyInterface, обратите внимание, что вы должны использовать те же соглашения о регистре для интерфейсов, что и для классов. MyInterface определяет 3 метода, каждый с различными типами и параметрами возврата. Вы можете видеть, что ни один из этих методов не имеет тела; при работе с интерфейсами нас интересует только определение ожидаемого поведения, а не его реализация. Примечание. В Java 8 появилась возможность создавать реализацию по умолчанию для методов интерфейса, однако мы не будем рассматривать эту функциональность в этом руководстве.

Интерфейсы также могут содержать данные о состоянии с помощью переменных-членов:

1
2
3
4
5
6
7
public interface MyInterfaceWithState {
 
    int someNumber;
 
    void methodA();
 
}

Все методы в интерфейсе по умолчанию являются общедоступными, и фактически вы не можете создать метод в интерфейсе с уровнем доступа, отличным от public.

2.2. Реализация интерфейсов

Теперь мы определили интерфейс, который мы хотим создать класс, который будет предоставлять детали реализации поведения, которое мы определили. Мы делаем это, написав новый класс и используя ключевое слово Implements, чтобы сообщить компилятору, какой интерфейс должен реализовывать этот класс.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class MyClass implements MyInterface {
 
    public void methodA() {
        System.out.println("Method A called!");
    }
 
    public int methodB() {
        return 42;
    }
 
    public String methodC(double x, double y) {
        return "x = " + x + ", y = " y;
    }
 
}

Мы взяли сигнатуры методов, которые мы определили в MyInterface и дали им тела для их реализации. Мы только что произвели некоторую глупость в реализациях, но важно отметить, что мы могли бы сделать что-нибудь в этих телах, если бы они удовлетворяли сигнатурам метода. Мы также можем создать столько классов реализации, сколько захотим, каждый с разными телами реализации методов из MyInterface .

Мы реализовали все методы из MyInterface в MyClass и если бы нам не удалось реализовать ни один из них, компилятор выдал бы ошибку. Это потому, что тот факт, что MyClass реализует MyInterface, означает, что MyClass гарантирует предоставление реализации для каждого из методов из MyInterface. Это позволяет любому клиенту, использующему интерфейс, полагаться на тот факт, что во время выполнения будет реализована реализация метода, который он хочет вызвать, гарантировано.

2,3. Использование интерфейсов

Для вызова методов интерфейса из клиента нам просто нужно использовать оператор точки (.), Как и в случае методов классов:

1
2
MyInterface object1 = new MyClass();
object1.methodA(); // Guaranteed to work

Мы видим что-то необычное выше, вместо чего-то вроде MyClass object1 = new MyClass(); (что вполне приемлемо) мы объявляем object1 типа MyInterface. Это работает, потому что MyClass является реализацией MyInterface, где бы мы ни вызывали метод, определенный в MyInterface, мы знаем, что MyClass предоставит реализацию. object1 — это ссылка на любой объект времени выполнения, который реализует MyInterface, в данном случае это экземпляр MyClass. Если бы мы попытались сделать MyInterface object1 = new MyInterface() мы бы получили ошибку компилятора, потому что вы не можете создать экземпляр интерфейса, что имеет смысл, потому что в интерфейсе нет деталей реализации — нет кода для выполнения.

Когда мы делаем вызов object1.methodA() мы выполняем тело метода, определенное в MyClass, потому что тип времени выполнения object1 — MyClass, даже если ссылка имеет тип MyInterface. Мы можем вызывать только методы для object1, которые определены в MyInterface, для всех намерений и целей мы можем ссылаться на object1 как на тип MyInterface, даже если тип времени выполнения — MyClass. Фактически, если MyClass определил другой метод с именем methodD() мы не смогли бы вызвать его для object1, потому что компилятор знает только, что object1 является ссылкой на MyInterface, а не то, что это конкретно MyClass.

Это важное различие заключается в том, что мы можем создавать различные классы реализации для наших интерфейсов, не заботясь о том, какой из них вызывается во время выполнения.

Возьмите следующий интерфейс:

1
2
3
4
5
public interface OneMethodInterface {
 
    void oneMethod();
 
}

Он определяет один метод void, который не принимает параметров.

Давайте реализуем это:

1
2
3
4
5
6
public class ClassA implements OneMethodInterface {
 
    public void oneMethod() {
        System.out.println("Runtime type is ClassA.");
    }
}

Мы можем использовать это в клиенте, как и раньше:

1
2
OneMethodInterface myObject = new ClassA();
myObject.oneMethod();

Выход:

1
Runtime type is ClassA.

Теперь давайте сделаем другую реализацию для OneMethodInterface:

1
2
3
4
5
6
public class ClassB implements OneMethodInterface {
     
    public void oneMethod() {
        System.out.println("The runtime type of this class is ClassB.");
}
}

И измените наш код выше:

1
2
3
4
OneMethodInterface myObject = new ClassA();
myObject.oneMethod();
myObject = new ClassB();
myObject.oneMethod();

Выход:

1
2
Runtime type is ClassA.
The runtime type of this class is ClassB.

Мы успешно использовали одну и ту же ссылку ( myObject ) для ссылки на экземпляры двух разных типов времени выполнения. Реальная реализация совершенно не важна для компилятора, она просто заботится о том, чтобы OneMethodInterface был реализован любым способом и любым способом. Что касается компилятора, myObject является OneMethodInterface, и oneMethod() доступен, даже если он переназначен другому объекту экземпляра другого типа класса. Эта способность предоставлять более одного типа времени выполнения и разрешать его во время выполнения, а не во время компиляции, называется полиморфизмом .

Интерфейсы определяют поведение без каких-либо подробностей реализации (игнорируя Java 8), а реализующие классы определяют все подробности реализации для классов, которые они определяют, но что произойдет, если мы хотим смешать два понятия? Если мы хотим смешать некоторое определение поведения и некоторую реализацию вместе в одном месте, мы можем использовать абстрактный класс.

3. Абстрактные классы

Абстрактный класс похож на неполный план, он определяет некоторые детали реализации класса, оставляя другие как простые определения поведения, которые будут реализованы позже.

Представьте себе проект дома, в котором дом полностью прорисован, но есть большой пустой участок, куда пойдет гараж. Мы знаем, что будет гараж, но мы не знаем, как он будет выглядеть. Кто-то еще должен будет взять копию нашего проекта и нарисовать в гараже. На самом деле несколько разных людей могут взять копии нашего проекта и нарисовать в разных типах гаражей. Дома, построенные с использованием этих чертежей, будут узнаваемыми вариантами нашего дома; входная дверь, планировка помещения и окна будут одинаковыми, однако гаражи будут разными.

Подобно чертежам над нашими абстрактными классами, будут полностью определены некоторые методы, и эти реализации методов будут одинаковыми во всех реализациях абстрактного класса. Абстрактный класс будет определять только сигнатуру для других методов, почти так же, как это делают интерфейсы. Реализации методов для этих методов будут различаться в классах реализации. Реализующий класс абстрактного класса обычно называют конкретным классом. Из-за отношения наследования между конкретным классом и абстрактным классом мы обычно говорим, что конкретный класс расширяет абстрактный класс, а не реализует его, как мы говорим с интерфейсами.

Как и с интерфейсами, любой клиентский код знает, что, если конкретный класс расширяет абстрактный класс, конкретный класс гарантирует предоставление тел методов для абстрактных методов абстрактного класса (абстрактный класс предоставляет свои собственные тела методов для неабстрактных методов, курс).

Опять же, так же, как интерфейсы, может быть несколько различных конкретных классов данного абстрактного класса, каждый из которых может определять очень разные поведения для абстрактных методов абстрактного класса при выполнении контракта абстрактного класса. Детали реализации скрыты от клиента.

3.1. Определение абстрактных классов

Ключевое слово abstract используется для определения класса и его методов как abstract.

01
02
03
04
05
06
07
08
09
10
11
public abstract class MyAbstractClass {
 
    protected int someNumber = 0;
 
    public void increment() {
        someNumber++;
    }
 
    public abstract void doSomethingWithNumber();
 
}

Здесь мы определили абстрактный класс MyAbstractClass который содержит целое число и предоставляет метод для увеличения этого целого числа. Мы также определяем абстрактный метод с именем doSomethingWithNumber() . Мы еще не знаем, что будет делать этот метод, он будет определен в любых конкретных классах, которые расширяют MyAbstractClass . doSomethingWithNumber() не имеет тела метода, потому что оно абстрактно.

В интерфейсах все методы общедоступны по умолчанию, однако область действия абстрактного метода может быть публичной, пакетной или защищенной.

Вы можете видеть, что некоторая поведенческая реализация в increment () смешана с некоторым поведенческим определением в doSomethingWithNumber() в этом абстрактном классе. Абстрактные классы смешивают некоторую реализацию с некоторым определением. Конкретные классы, расширяющие класс Abstract, будут повторно использовать реализацию increment() , гарантируя при этом предоставление собственных реализаций doSomethingWithNumber() .

3.2. Расширение абстрактных классов

Теперь, когда мы создали абстрактный класс, давайте сделаем для него конкретную реализацию. Мы делаем конкретные реализации из абстрактных классов, используя ключевое слово extends в классе, который сам не является абстрактным.

01
02
03
04
05
06
07
08
09
10
11
public class MyConcreteClass extends MyAbstractClass {
 
    public void sayHello() {
        System.out.println("Hello there!");
}
 
    public void doSomethingWithNumber() {
        System.out.println("The number is " + someNumber);
}
 
}

Мы создали конкретный класс MyConcreteClass и расширили MyAbstractClass . Нам нужно было только обеспечить реализацию для абстрактного метода doSomethingWithNumber() потому что мы наследуем не приватные переменные-члены и методы из MyAbstractClass. Если какой-либо клиент вызывает increment() для MyConcreteClass, будет выполнена реализация, определенная в MyAbstractClass. Мы также создали новый метод sayHello() который уникален для MyConcreteClass и не будет доступен ни в каком другом конкретном классе, который реализует MyAbstractClass.

Мы также можем расширить MyAbstractClass другим абстрактным классом, в котором мы не реализуем doSomethingWithNumber — это означает, что должен быть определен другой конкретный класс, который расширяет этот новый класс для реализации doSomethingWithNumber() .

1
2
3
4
5
6
public abstract class MyOtherAbstractClass extends MyAbstractClass {
 
    public void sayHello() {
        System.out.println("Hello there!");
}
}

Нам не нужно было ссылаться на doSomethingWithNumber () здесь, всякий раз, когда мы создаем конкретный класс для MyOtherAbstractClass, мы предоставляем реализацию для doSomethingWithNumber ().

Наконец, абстрактные классы могут сами реализовывать интерфейсы. Поскольку абстрактный класс не может быть реализован сам по себе, ему не нужно предоставлять реализацию для всех (или любых) методов интерфейса. Если абстрактный класс не обеспечивает реализацию для метода интерфейса, конкретный класс, который расширяет абстрактный класс, должен будет обеспечить реализацию.

1
2
3
4
5
6
public abstract MyImplementingAbstractClass implements MyInterface {
     
    public void methodA() {
        System.out.println("Method A has been implemented in this abstract class");
    }
}

Здесь мы видим, что MyImplementingAbstractClass реализует MyInterface, но предоставляет реализацию только для methodA (). Если какой-либо конкретный класс расширяет MyImplementingAbstractClass, он должен предоставить реализацию для methodB () и methodC (), как определено в MyInterface.

3.3. Использование абстрактных классов

Как и в случае с интерфейсами и обычными классами, для вызова методов абстрактного класса вы используете оператор точки (.).

1
2
3
MyAbstractClass object1 = new MyConcreteClass();
object1.increment();
object1.doSomethingWithNumber();

Мы снова видим, что object1 является ссылкой на экземпляр, который обеспечивает конкретную реализацию для MyAbstractClass, и типом времени выполнения этого экземпляра является MyConcreteClass. Для всех намерений и целей объект1 обрабатывается компилятором так, как если бы он был экземпляром MyAbstractClass. Если вы попытаетесь вызвать метод sayHello() определенный в MyConcreteClass, вы получите ошибку компилятора. Этот метод не виден компилятору через object1, потому что object1 является ссылкой MyAbstractClass. Единственная гарантия, которую предоставляет object1, состоит в том, что он будет иметь реализации для методов, определенных в MyAbstractClass, любые другие методы, предоставляемые типом времени выполнения, не будут видны.

1
object1.sayHello(); // compiler error

Как и в случае с интерфейсами, мы можем предоставлять разные типы времени выполнения и использовать их по одной и той же ссылке.

Давайте определим новый абстрактный класс

1
2
3
4
5
6
7
8
public abstract class TwoMethodAbstractClass {
     
    public void oneMethod() {
        System.out.prinln("oneMethod is implemented in TwoMethodAbstractClass.");
    }
 
    public abstract void twoMethod();
}

Он определяет один реализованный метод, другой абстрактный метод.

Давайте расширим это конкретным классом

1
2
3
4
5
6
public class ConcreteClassA extends TwoMethodAbstractClass {
     
    public void twoMethod() {
        System.out.println("twoMethod is implemented in ConcreteClassA.");
}
}

Мы можем использовать его в клиенте, как и раньше:

1
2
3
TwoMethodAbstractClass myObject = new ConcreteClassA();
myObject.oneMethod();
myObject.twoMethod();

Выход:

1
2
oneMethod is implemented in TwoMethodAbstractClass.
twoMethod is implemented in ConcreteClassA.

Теперь давайте создадим другой конкретный класс, который расширяет TwoMethodClass:

1
2
3
4
5
6
public class ConcreteClassB extends TwoMethodAbstractClass {
 
    public void twoMethod() {
        System.out.println("ConcreteClassB implements its own twoMethod.");
    }
}

И измените наш код выше:

1
2
3
4
5
6
TwoMethodAbstractClass myObject = new ConcreteClassA();
myObject.oneMethod();
myObject.twoMethod();
myObject = new ConcreteClassB();
myObject.oneMethod();
myObject.twoMethod();

Выход:

1
2
3
4
oneMethod is implemented in TwoMethodAbstractClass.
twoMethod is implemented in ConcreteClassA.
oneMethod is implemented in TwoMethodAbstractClass.
ConcreteClassB implements its own twoMethod.

Мы использовали одну и ту же ссылку (myObject) для ссылки на экземпляры двух разных типов времени выполнения. Когда типом выполнения myObject является ConcreteClassA, он использует реализацию twoMethod из ConcreteClassA. Когда типом выполнения myObject является ConcreteClassB, он использует реализацию twoMethod из ConcreteClassB. В обоих случаях используется общая реализация oneMethod из TwoMethodAbstractClass.

Абстрактные классы используются для определения общих поведений при предоставлении контрактов или обещаний, что другие поведения будут доступны из конкретного класса позже. Это позволяет нам создавать объектные модели, в которых мы можем повторно использовать общую функциональность и фиксировать различные требования в разных конкретных классах.

4. Работающий пример — система платежей

Нас попросили построить платежную систему для компании. Мы знаем, что в компании работают разные типы сотрудников, которым можно платить по-разному; наемным и с комиссией. Руководитель компании понимает, что его потребности изменятся, и система может быть изменена позже, чтобы приспособиться к различным типам организаций, которые будут получать платежи.

4.1. Интерфейс получателя

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

1
2
3
4
5
6
7
8
public interface Payee {
 
    String name();
 
    Double grossPayment();
 
    Integer bankAccount();
}

Здесь у нас есть интерфейс Payee, который гарантирует три поведения; возможность указать имя получателя платежа, возможность указать общую сумму платежа, которую следует уплатить, и возможность указать номер банковского счета, на который должны быть внесены средства.

4.2. Платежная система

Теперь, когда мы определили Payee, давайте напишем некоторый код, который его использует. Мы создадим класс PaymentSystem, который будет поддерживать список получателей и по требованию будет проходить через них и перечислять запрошенную сумму на соответствующий банковский счет.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PaymentSystem {
 
    private List<Payee> payees;
 
    public PaymentSystem() {
            payees = new ArrayList<>();
    }
 
    public void addPayee(Payee payee) {
            if (!payees.contains(payee)) {
                    payees.add(payee);
            }
    }
 
    public void processPayments() {
            for (Payee payee : payees) {
                    Double grossPayment = payee.grossPayment();
 
                    System.out.println("Paying to " + payee.name());
                    System.out.println("\tGross\t" + grossPayment);
                    System.out.println("\tTransferred to Account: " + payee.bankAccount());
            }
    }
}

Вы можете видеть, что PaymentSystem абсолютно независима от типов времени выполнения Payees, это не заботится и не должно заботиться. Он знает, что независимо от того, какой тип среды выполнения обрабатывает grossPayment() , grossPayment() гарантирует реализацию name() , grossPayment() и bankAccount() . Учитывая эти знания, это просто вопрос выполнения цикла for для всех Получателей и обработки их платежей с использованием этих методов.

4,3. Классы сотрудников

Босс сказал нам, что у него есть два разных типа сотрудников — наемные работники и уполномоченные сотрудники. Наемные работники имеют базовую зарплату, которая не меняется, в то время как уполномоченные сотрудники имеют базовую зарплату, а также могут получать комиссионные за успешные продажи.

4.3.1 Класс SalaryEmployee

Давайте начнем с класса для наемных работников. Он должен реализовывать интерфейс Получателя, чтобы он мог использоваться Платежной системой.

Давайте представим наши изменения, используя диаграммы классов в дополнение к коду. На диаграммах классов пунктирная стрелка означает отношение «реализует», а сплошная стрелка означает отношение «расширяет».

Диаграмма классов

Рисунок 1 Абстракция в Java

фигура 1

Код

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SalaryEmployee implements Payee {
 
    private String name;
    private Integer bankAccount;
    protected Double grossWage;
     
    public SalaryEmployee(String name, Integer bankAccount, Double grossWage) {
            this.name = name;
            this.bankAccount = bankAccount;
            this.grossWage = grossWage;
    }
 
    public Integer bankAccount() {
            return bankAccount;
    }
     
    public String name() {
            return name;
    }
     
    public Double grossPayment() {
            return grossWage;
    }
}

4.3.2 Класс CommissionEmployee

Теперь давайте создадим класс CommissionEmployee. Этот класс будет очень похож на SalaryEmployee с возможностью платить комиссионные.

Диаграмма классов

Рисунок 2 Абстракция в Java

фигура 2

Код

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CommissionEmployee implements Payee {
 
    private String name;
    private Integer bankAccount;
    protected Double grossWage;
    private Double grossCommission = 0.0;
 
    public CommissionEmployee(String name, Integer bankAccount, Double grossWage) {
            this.name = name;
            this.bankAccount = bankAccount;
            this.grossWage = grossWage;
    }
 
    public Integer bankAccount() {
            return bankAccount;
    }
 
    public String name() {
            return name;
    }
 
    public Double grossPayment() {
            return grossWage + doCurrentCommission();
    }
 
    private Double doCurrentCommission() {
            Double commission = grossCommission;
            grossCommission = 0.0;
            return commission;
    }
 
    public void giveCommission(Double amount) {
            grossCommission += amount;
    }
}

Как вы можете видеть, большая часть кода дублируется между SalaryEmployee и CommissionEmployee. Фактически, единственное, что отличается, это расчет для grossPayment, который использует комиссионное значение в CommissionEmployee. Некоторые функции одинаковы, а некоторые — разные. Это выглядит как хороший кандидат на абстрактный класс.

4.3.3 Абстрактный класс сотрудника

Давайте абстрагируем функциональность имени и банковского счета в абстрактный класс Employee. SalaryEmployee и CommissionEmployee могут затем расширить этот абстрактный класс и предоставить различные реализации для grossPayment() .

Диаграмма классов

Рисунок 3 Абстракция в Java

Рисунок 3

Код

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public abstract class Employee implements Payee {
 
    private String name;
    private Integer bankAccount;
    protected Double grossWage;
 
    public Employee(String name, Integer bankAccount, Double grossWage) {
            this.name = name;
            this.bankAccount = bankAccount;
            this.grossWage = grossWage;
    }
 
    public String name() {
            return name;
    }
 
    public Integer bankAccount() {
            return bankAccount;
    }
}

Обратите внимание, что Employee не нужно реализовывать метод grossPayment (), определенный в Payee, потому что Employee является абстрактным.

Теперь давайте перепишем два класса Employee:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SalaryEmployee extends Employee {
 
    public SalaryEmployee(String name, Integer bankAccount, Double grossWage) {
            super(name, bankAccount, grossWage);
    }
 
    public Double grossPayment() {
            return grossWage;
    }
}
 
public class CommissionEmployee extends Employee {
 
    private Double grossCommission = 0.0;
 
    public CommissionEmployee(String name, Integer bankAccount, Double grossWage) {
            super(name, bankAccount, grossWage);
    }
 
    public Double grossPayment() {
            return grossWage + doCurrentCommission();
    }
 
    private Double doCurrentCommission() {
            Double commission = grossCommission;
            grossCommission = 0.0;
            return commission;
    }
 
    public void giveCommission(Double amount) {
            grossCommission += amount;
    }
}

Намного аккуратнее!

4.4. Приложение

Давайте попробуем использовать наши новые типы сотрудников в приложении. Мы создадим класс приложения, который инициализирует систему, создав Платежную систему, некоторых сотрудников и имитируя неделю работы.

4.4.1 Класс ApplicationApplication

Класс приложения выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class PaymentApplication {
 
    public static void main(final String... args) {
            // Initialization
            PaymentSystem paymentSystem = new PaymentSystem();
 
            CommissionEmployee johnSmith = new CommissionEmployee("John Smith", 1111, 300.0);
            paymentSystem.addPayee(johnSmith);
 
            CommissionEmployee paulJones = new CommissionEmployee("Paul Jones", 2222, 350.0);
            paymentSystem.addPayee(paulJones);
 
            SalaryEmployee maryBrown = new SalaryEmployee("Mary Brown", 3333, 500.0);
            paymentSystem.addPayee(maryBrown);
 
            SalaryEmployee susanWhite = new SalaryEmployee("Susan White", 4444, 470.0);
            paymentSystem.addPayee(susanWhite);
 
            // Simulate Week
            johnSmith.giveCommission(40.0);
            johnSmith.giveCommission(35.0);
            johnSmith.giveCommission(45.0);
 
            paulJones.giveCommission(45.0);
            paulJones.giveCommission(51.0);
            paulJones.giveCommission(23.0);
            paulJones.giveCommission(14.5);
            paulJones.giveCommission(57.3);
 
            // Process Weekly Payment
            paymentSystem.processPayments();
    }

Мы создаем двух наемных работников и двух наемных работников, каждый со своими именами, базовой зарплатой и номерами банковских счетов. Мы загружаем каждого из сотрудников в созданную нами Платежную систему. Затем мы моделируем неделю, назначая комиссионные двум уполномоченным сотрудникам, и затем просим Платежную систему обработать все платежи за неделю.

Выход:

01
02
03
04
05
06
07
08
09
10
11
12
Paying to John Smith
    Gross    420.0
    Transferred to Account: 1111
Paying to Paul Jones
    Gross    540.8
    Transferred to Account: 2222
Paying to Mary Brown
    Gross    500.0
    Transferred to Account: 3333
Paying to Susan White
    Gross    470.0
    Transferred to Account: 4444

4,5. Обработка бонусов

Пока что босс очень доволен системой, но он сказал нам, что для мотивации своих сотрудников он хочет иметь возможность еженедельно выплачивать им процентные бонусы. Он говорит нам, что поскольку уполномоченные сотрудники обычно получают более низкую зарплату, мы должны дать им множитель бонуса в 1,5 раза, чтобы увеличить их процентный бонус. Бонус должен быть отражен в валовой оплате каждого работника.

4.5.1 Класс сотрудника для бонусов

Давайте добавим к сотруднику поле для сбора любых начисленных бонусов, защищенный метод возврата и сброса бонуса и абстрактный метод для получения бонуса. Метод doBonus() защищен, так что к нему могут обращаться конкретные классы Employee. Метод giveBonus является абстрактным, поскольку он будет реализован по-разному для наемных и уполномоченных сотрудников.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public abstract class Employee implements Payee {
 
    private String name;
    private Integer bankAccount;
 
    protected Double currentBonus;
    protected Double grossWage;
 
    public Employee(String name, Integer bankAccount, Double grossWage) {
            this.name = name;
            this.bankAccount = bankAccount;
            this.grossWage = grossWage;
            currentBonus = 0.0;
    }
 
    public String name() {
            return name;
    }
 
    public Integer bankAccount() {
            return bankAccount;
    }
 
    public abstract void giveBonus(Double percentage);
 
    protected Double doCurrentBonus() {
            Double bonus = currentBonus;
            currentBonus = 0.0;
            return bonus;
    }
}

Обновления SalaryEmployee:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class SalaryEmployee extends Employee {
 
    public SalaryEmployee(String name, Integer bankAccount, Double grossWage) {
            super(name, bankAccount, grossWage);
    }
 
    public void giveBonus(Double percentage) {
            currentBonus += grossWage * (percentage/100.0);
    }
 
    public Double grossPayment() {
            return grossWage + doCurrentBonus();
    }
}

Обновления для CommissionEmployee:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class CommissionEmployee extends Employee {
 
    private static final Double bonusMultiplier = 1.5;
 
    private Double grossCommission = 0.0;
 
    public CommissionEmployee(String name, Integer bankAccount, Double grossWage) {
            super(name, bankAccount, grossWage);
    }
 
    public void giveBonus(Double percentage) {
            currentBonus += grossWage * (percentage/100.0) * bonusMultiplier;
    }
 
    public Double grossPayment() {
            return grossWage + doCurrentBonus() + doCurrentCommission();
    }
 
    private Double doCurrentCommission() {
            Double commission = grossCommission;
            grossCommission = 0.0;
            return commission;
    }
 
    public void giveCommission(Double amount) {
            grossCommission += amount;
    }
}

Мы видим, что оба класса реализуют метод giveBonus () по-разному — CommissionEmployee использует множитель бонуса. Также можно видеть, что оба класса используют защищенный метод doCurrentBonus (), определенный в Employee, при разработке валового платежа.

Давайте обновим наше приложение, чтобы имитировать выплату некоторых еженедельных бонусов нашим Сотрудникам:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class PaymentApplication {
 
    public static void main(final String... args) {
            // Initialization
            PaymentSystem paymentSystem = new PaymentSystemV1();
 
            CommissionEmployee johnSmith = new CommissionEmployee("John Smith", 1111, 300.0);
            paymentSystem.addPayee(johnSmith);
 
            CommissionEmployee paulJones = new CommissionEmployee("Paul Jones", 2222, 350.0);
            paymentSystem.addPayee(paulJones);
 
            SalaryEmployee maryBrown = new SalaryEmployee("Mary Brown", 3333, 500.0);
            paymentSystem.addPayee(maryBrown);
 
            SalaryEmployee susanWhite = new SalaryEmployee("Susan White", 4444, 470.0);
            paymentSystem.addPayee(susanWhite);
 
            // Simulate Week
            johnSmith.giveCommission(40.0);
        johnSmith.giveCommission(35.0);
        johnSmith.giveCommission(45.0);
        johnSmith.giveBonus(5.0);
 
        paulJones.giveCommission(45.0);
        paulJones.giveCommission(51.0);
        paulJones.giveCommission(23.0);
        paulJones.giveCommission(14.5);
        paulJones.giveCommission(57.3);
        paulJones.giveBonus(6.5);
 
        maryBrown.giveBonus(3.0);
 
        susanWhite.giveBonus(7.5);
 
            // Process Weekly Payment
            paymentSystem.processPayments();
    }
}

Выход:

01
02
03
04
05
06
07
08
09
10
11
12
Paying to John Smith
    Gross    442.5
    Transferred to Account: 1111
Paying to Paul Jones
    Gross    574.925
    Transferred to Account: 2222
Paying to Mary Brown
    Gross    515.0
    Transferred to Account: 3333
Paying to Susan White
    Gross    505.25
    Transferred to Account: 4444

Суммы брутто теперь отражают премии, выплачиваемые сотрудникам.

4,6. Подрядные компании

Босс в восторге от платежной системы, однако он думает о ком-то еще, кому он должен заплатить. Время от времени он будет привлекать услуги подрядной компании. Очевидно, что у этих компаний нет заработной платы и не выплачиваются бонусы. Они могут получить несколько разовых платежей, и при обработке платежной системой должны быть оплачены все накопительные платежи и очищен их баланс.

4.6.1. Контрактная компания

Класс ContractingCompany должен реализовывать Payee, чтобы его можно было использовать в Платежной системе. У него должен быть метод оплаты услуг, который будет отслеживаться по совокупной сумме и использоваться для платежей.

Диаграмма классов

Рисунок 4 Абстракция в Java

Рисунок 4

Код

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ContractingCompany implements Payee {
 
    private String name;
    private Integer bankAccount;
    private Double currentBalance;
 
    public ContractingCompany(String name, Integer bankAccount) {
            this.name = name;
            this.bankAccount = bankAccount;
            currentBalance = 0.0;
    }
 
    public String name() {
            return name;
    }
 
    public Double grossPayment() {
            return doPayment();
    }
 
    private Double doPayment() {
            Double balance = currentBalance;
            currentBalance = 0.0;
            return balance;
    }
 
    public Integer bankAccount() {
            return bankAccount;
    }
 
    public void payForServices(Double amount) {
            currentBalance += amount;
    }
}

Теперь давайте добавим пару подрядных компаний в наше платежное приложение и смоделируем для них некоторые платежи:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class PaymentApplication {
 
    public static void main(final String... args) {
        // Initialization
        PaymentSystem paymentSystem = new PaymentSystemV1();
 
        CommissionEmployee johnSmith = new CommissionEmployee("John Smith", 1111, 300.0, 100.0);
        paymentSystem.addPayee(johnSmith);
 
        CommissionEmployee paulJones = new CommissionEmployee("Paul Jones", 2222, 350.0, 125.0);
        paymentSystem.addPayee(paulJones);
 
        SalaryEmployee maryBrown = new SalaryEmployee("Mary Brown", 3333, 500.0, 110.0);
        paymentSystem.addPayee(maryBrown);
 
        SalaryEmployee susanWhite = new SalaryEmployee("Susan White", 4444, 470.0, 130.0);
        paymentSystem.addPayee(susanWhite);
 
        ContractingCompany acmeInc = new ContractingCompany("Acme Inc", 5555);
        paymentSystem.addPayee(acmeInc);
 
        ContractingCompany javaCodeGeeks = new ContractingCompany("javacodegeeks.com", 6666);
        paymentSystem.addPayee(javaCodeGeeks);
 
        // Simulate Week
        johnSmith.giveCommission(40.0);
        johnSmith.giveCommission(35.0);
        johnSmith.giveCommission(45.0);
        johnSmith.giveBonus(5.0);
 
        paulJones.giveCommission(45.0);
        paulJones.giveCommission(51.0);
        paulJones.giveCommission(23.0);
        paulJones.giveCommission(14.5);
        paulJones.giveCommission(57.3);
        paulJones.giveBonus(6.5);
 
        maryBrown.giveBonus(3.0);
 
        susanWhite.giveBonus(7.5);
 
        acmeInc.payForServices(100.0);
        acmeInc.payForServices(250.0);
        acmeInc.payForServices(300.0);
 
        javaCodeGeeks.payForServices(400.0);
        javaCodeGeeks.payForServices(250.0);
 
        // Process Weekly Payment
        paymentSystem.processPayments();
    }

Выход:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
Paying to John Smith
    Gross    442.5
    Transferred to Account: 1111
Paying to Paul Jones
    Gross    574.925
    Transferred to Account: 2222
Paying to Mary Brown
    Gross    515.0
    Transferred to Account: 3333
Paying to Susan White
    Gross    505.25
    Transferred to Account: 4444
Paying to Acme Inc
    Gross    650.0
    Transferred to Account: 5555
Paying to javacodegeeks.com
    Gross    650.0
    Transferred to Account: 6666

Теперь мы успешно добавили новый тип получателя платежа в систему без необходимости изменять одну строку кода в классе PaymentSystem, который обрабатывает получателей платежа. Это сила абстракции.

4,7. Расширенная функциональность: налогообложение

Босс находится на седьмом небе с Платежной системой, однако налоговик отправил ему письмо, в котором сообщалось, что он должен включить в систему какой-то вид удержания налогов, иначе у него будут большие проблемы. Для системы должна быть установлена ​​глобальная налоговая ставка, и у каждого сотрудника должна быть индивидуальная налоговая льгота. Налог должен взиматься только с любых выплат, выполненных работнику сверх налоговой льготы. Затем сотруднику должна быть выплачена только чистая сумма, причитающаяся ему. Подрядные компании несут ответственность за уплату собственного налога, поэтому система не должна удерживать с них налог.

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

В Java мы можем расширить интерфейсы. Это позволяет нам добавлять определения поведения без изменения нашего исходного интерфейса. Все поведение, определенное в исходном интерфейсе, будет автоматически перенесено в новый интерфейс.

4.7.1. Интерфейс TaxablePayee

Мы расширим Payee для создания нового интерфейса TaxablePayee, затем мы попросим Employee реализовать этот интерфейс, оставив ContractingCompany как обычного, не облагаемого налогом Payee.

Диаграмма классов

Рисунок 5 Абстракция в Java

Рисунок 5

Код

1
2
3
4
5
public interface TaxablePayee extends Payee {
 
    public Double allowance();
 
}

Здесь мы видим, что получатель платежа расширен и определен дополнительный метод — allowance() который возвращает необлагаемое налогом пособие TaxablePayee.

Теперь нам нужно обновить Employee для реализации TaxablePayee, и мы увидим, что это повлияет на оба конкретных класса Employee.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public abstract class Employee implements TaxablePayee {
 
    private String name;
    private Integer bankAccount;
    private Double allowance;
 
    protected Double currentBonus;
    protected Double grossWage;
 
    public Employee(String name, Integer bankAccount, Double grossWage, Double allowance) {
        this.name = name;
        this.bankAccount = bankAccount;
        this.grossWage = grossWage;
        this.allowance = allowance;
        currentBonus = 0.0;
    }
 
    public String name() {
        return name;
    }
 
    public Integer bankAccount() {
            return bankAccount;
    }
 
    public Double allowance() {
            return allowance;
    }
 
    public abstract void giveBonus(Double percentage);
 
    protected Double doCurrentBonus() {
            Double bonus = currentBonus;
            currentBonus = 0.0;
            return bonus;
    }
 
}

Нам пришлось изменить конструктор Employee, что означает, что конструкторы наших двух абстрактных классов также должны будут измениться.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class SalaryEmployee extends Employee {
 
    public SalaryEmployee(String name, Integer bankAccount, Double grossWage, Double allowance) {
            super(name, bankAccount, grossWage, allowance);
    }
 
    @Override
    public void giveBonus(Double percentage) {
            currentBonus += grossWage * (percentage/100.0);
    }
 
    @Override
    public Double grossPayment() {
            return grossWage + doCurrentBonus();
    }
}

CommissionEmployee:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CommissionEmployee extends Employee {
 
    private static final Double bonusMultiplier = 1.5;
 
    private Double grossCommission = 0.0;
 
    public CommissionEmployee(String name, Integer bankAccount, Double grossWage, Double allowance) {
            super(name, bankAccount, grossWage, allowance);
    }
 
    @Override
    public void giveBonus(Double percentage) {
            currentBonus += grossWage * (percentage/100.0) * bonusMultiplier;
    }
 
    @Override
    public Double grossPayment() {
            return grossWage + doCurrentBonus() + doCurrentCommission();
    }
 
    private Double doCurrentCommission() {
            Double commission = grossCommission;
            grossCommission = 0.0;
            return commission;
    }
 
    public void giveCommission(Double amount) {
            grossCommission += amount;
    }
}

4.7.1. Налогообложение изменений в PaymentSystem

Теперь нам нужно обновить PaymentSystem, чтобы удерживать налог, и для этого нам понадобится какой-то способ определить, является ли данный Получатель TaxablePayee или обычным Получателем. Для этого мы можем использовать ключевое слово Java под названием instanceof .

Instanceof позволяет нам проверить, соответствует ли тип времени выполнения данной ссылочной переменной типу теста. Он возвращает логическое значение и может быть вызван так: if (object1 instanceof MyClass). Это вернет true, если тип времени выполнения object1 — MyClass или подкласс или реализующий класс (если MyClass — интерфейс) MyClass. Это означает, что мы можем тестировать в любом месте дерева наследования, что делает его очень мощным инструментом. Мы можем использовать этот инструмент, чтобы определить, является ли данный Получатель экземпляром TaxablePayee, и принять соответствующие меры на основе этих знаний.

Теперь мы обновляем систему платежей следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class PaymentSystem {
 
    private List<Payee> payees;
    private Double taxRate = 0.2;
 
    public PaymentSystem() {
            payees = new ArrayList<>();
    }
 
    public void addPayee(Payee payee) {
            if (!payees.contains(payee)) {
                payees.add(payee);
            }
    }
 
    public void processPayments() {
            for (Payee payee : payees) {
                    Double grossPayment = payee.grossPayment();
                    Double tax = 0.0;
                    if (payee instanceof TaxablePayee) {
                        Double taxableIncome = grossPayment - ((TaxablePayee)payee).allowance();
                        tax = taxableIncome * taxRate;
                    }
                    Double netPayment = grossPayment - tax;
 
                System.out.println("Paying to " + payee.name());
                System.out.println("\tGross\t" + grossPayment);
                System.out.println("\tTax\t\t-" + tax);
                System.out.println("\tNet\t\t" + netPayment);
                System.out.println("\tTransferred to Account: " + payee.bankAccount());
            }
    }
}

Новый код сначала проверяет, является ли обрабатываемый Получатель экземпляром TaxablePayee, если это так, то он выполняет приведение к Получателю, чтобы рассматривать его как ссылку на TaxablePayee для целей доступа к allowance()методу, определенному в TaxablePayee. Помнить;Если бы ссылка оставалась в качестве Получателя, allowance()метод не был бы виден для PaymentSystem, поскольку он определен в TaxablePayee, а не в Получателе. Актеры здесь безопасны, потому что мы уже подтвердили, что Получатель является экземпляром TaxablePayee. Теперь, когда у PaymentSystem есть надбавка, она может рассчитать налогооблагаемую сумму и удерживаемый налог на основе глобальной ставки налога 20%.

Если Получатель не TaxablePayee, система продолжает обрабатывать их как обычно, не предпринимая никаких действий, связанных с обработкой налогов.

Давайте обновим наш класс приложения, чтобы предоставить нашим сотрудникам различные налоговые льготы, и снова запустим приложение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class PaymentApplication {
 
    public static void main(final String... args) {
        // Initialization
        PaymentSystem paymentSystem = new PaymentSystemV1();
 
        CommissionEmployee johnSmith = new CommissionEmployee("John Smith", 1111, 300.0, 100.0);
        paymentSystem.addPayee(johnSmith);
 
        CommissionEmployee paulJones = new CommissionEmployee("Paul Jones", 2222, 350.0, 125.0);
        paymentSystem.addPayee(paulJones);
 
        SalaryEmployee maryBrown = new SalaryEmployee("Mary Brown", 3333, 500.0, 110.0);
        paymentSystem.addPayee(maryBrown);
 
        SalaryEmployee susanWhite = new SalaryEmployee("Susan White", 4444, 470.0, 130.0);
        paymentSystem.addPayee(susanWhite);
 
        ContractingCompany acmeInc = new ContractingCompany("Acme Inc", 5555);
        paymentSystem.addPayee(acmeInc);
 
        ContractingCompany javaCodeGeeks = new ContractingCompany("javacodegeeks.com", 6666);
        paymentSystem.addPayee(javaCodeGeeks);
 
        // Simulate Week
        johnSmith.giveCommission(40.0);
        johnSmith.giveCommission(35.0);
        johnSmith.giveCommission(45.0);
        johnSmith.giveBonus(5.0);
 
        paulJones.giveCommission(45.0);
        paulJones.giveCommission(51.0);
        paulJones.giveCommission(23.0);
        paulJones.giveCommission(14.5);
        paulJones.giveCommission(57.3);
        paulJones.giveBonus(6.5);
 
        maryBrown.giveBonus(3.0);
 
        susanWhite.giveBonus(7.5);
 
        acmeInc.payForServices(100.0);
        acmeInc.payForServices(250.0);
        acmeInc.payForServices(300.0);
 
        javaCodeGeeks.payForServices(400.0);
        javaCodeGeeks.payForServices(250.0);
 
        // Process Weekly Payment
        paymentSystem.processPayments();
    }
}

Выход:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Paying to John Smith
    Gross       442.5
    Tax     -68.5
    Net      374.0
    Transferred to Account: 1111
Paying to Paul Jones
    Gross       574.925
    Tax      -89.985
    Net      484.93999999999994
    Transferred to Account: 2222
Paying to Mary Brown
    Gross       515.0
    Tax      -81.0
    Net      434.0
    Transferred to Account: 3333
Paying to Susan White
    Gross       505.25
    Tax      -75.05
    Net      430.2
    Transferred to Account: 4444
Paying to Acme Inc
    Gross       650.0
    Tax      -0.0
    Net      650.0
    Transferred to Account: 5555
Paying to javacodegeeks.com
    Gross       650.0
    Tax      -0.0
    Net      650.0
    Transferred to Account: 6666

Как вы можете видеть, налог рассчитывается и удерживается для сотрудников, в то время как подрядные компании продолжают платить налог. Босс не может быть счастливее с системой и собирается подарить нам оплачиваемый отпуск на все расходы, чтобы он не попал в тюрьму!

5. Заключение

Абстракция является очень мощным инструментом в Java, когда используется правильно. Это открывает много возможностей для расширенного использования Java и для создания сложного, расширяемого и обслуживаемого программного обеспечения. Мы только что поцарапали поверхность того, что абстракция может сделать для нас, и, надеюсь, создали основу для изучения различных способов, которыми абстракция может использоваться более подробно.