Если вы обнаружите, что большинство ваших подпрограмм абсолютно одинаковы, за исключением определенных разделов, вы можете рассмотреть использование метода шаблонов, чтобы исключить подверженное ошибкам дублирование кода . Вот пример: ниже приведены два класса, которые делают похожие вещи:
- Создание и инициализация Reader для чтения из файла CSV.
- Прочитайте каждую строку и разбейте ее на жетоны.
- Распакуйте токены из каждой строки в сущность, Продукт или Клиент.
- Добавьте каждую сущность в набор.
- Верните набор.
Как вы можете видеть, только на третьем шаге есть различие — демаршаллинг тому или иному объекту. Все остальные шаги такие же. Я выделил строку, где код отличается в каждом из фрагментов.
ProductCsvReader.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public class ProductCsvReader { Set<Product> getAll(File file) throws IOException { Set<Product> returnSet = new HashSet<>(); try (BufferedReader reader = new BufferedReader( new FileReader(file))){ String line = reader.readLine(); while (line != null && !line.trim().equals( "" )) { String[] tokens = line.split( "\\s*,\\s*" ); Product product = new Product(Integer.parseInt(tokens[ 0 ]), tokens[ 1 ], new BigDecimal(tokens[ 2 ])); returnSet.add(product); line = reader.readLine(); } } return returnSet; } } |
CustomerCsvReader.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public class CustomerCsvReader { Set<Customer> getAll(File file) throws IOException { Set<Customer> returnSet = new HashSet<>(); try (BufferedReader reader = new BufferedReader( new FileReader(file))){ String line = reader.readLine(); while (line != null && !line.trim().equals( "" )) { String[] tokens = line.split( "\\s*,\\s*" ); Customer customer = new Customer(Integer.parseInt(tokens[ 0 ]), tokens[ 1 ], tokens[ 2 ], tokens[ 3 ]); returnSet.add(customer); line = reader.readLine(); } } return returnSet; } } |
В этом примере есть только две сущности, но в реальной системе могут быть десятки сущностей, так что это много подверженного ошибкам дублирующегося кода. Вы можете столкнуться с аналогичной ситуацией с DAO, где операции выбора, вставки, обновления и удаления каждого DAO будут выполнять одно и то же, работать только с разными сущностями и таблицами. Давайте начнем рефакторинг этого хлопотного кода. В соответствии с одним из принципов проектирования, изложенных в первой части книги «Шаблоны проектирования GoF», мы должны «инкапсулировать концепцию, которая меняется». Между ProductCsvReader и CustomerCsvReader различается выделенный код. Таким образом, наша цель состоит в том, чтобы инкапсулировать то, что варьируется в отдельных классах, в то же время перемещая то, что остается неизменным, в один класс. Давайте начнем редактировать только один класс, ProductCsvReader. Мы используем метод Extract для извлечения строки в свой собственный метод:
ProductCsvReader.java после метода извлечения
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class ProductCsvReader { Set<Product> getAll(File file) throws IOException { Set<Product> returnSet = new HashSet<>(); try (BufferedReader reader = new BufferedReader( new FileReader(file))){ String line = reader.readLine(); while (line != null && !line.trim().equals( "" )) { String[] tokens = line.split( "\\s*,\\s*" ); Product product = unmarshall(tokens); returnSet.add(product); line = reader.readLine(); } } return returnSet; } Product unmarshall(String[] tokens) { Product product = new Product(Integer.parseInt(tokens[ 0 ]), tokens[ 1 ], new BigDecimal(tokens[ 2 ])); return product; } } |
Теперь, когда мы разделили то, что зависит от того, что остается неизменным, мы создадим родительский класс, который будет содержать код, который остается одинаковым для обоих классов. Давайте назовем этот родительский класс AbstractCsvReader. Давайте сделаем это абстрактным, поскольку нет никакой причины для того, чтобы класс создавался сам по себе. Затем мы будем использовать рефакторинг Pull Up Method, чтобы переместить метод, который остается прежним, в этот родительский класс.
AbstractCsvReader.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
abstract class AbstractCsvReader { Set<Product> getAll(File file) throws IOException { Set<Product> returnSet = new HashSet<>(); try (BufferedReader reader = new BufferedReader( new FileReader(file))){ String line = reader.readLine(); while (line != null && !line.trim().equals( "" )) { String[] tokens = line.split( "\\s*,\\s*" ); Product product = unmarshall(tokens); returnSet.add(product); line = reader.readLine(); } } return returnSet; } } |
ProductCsvReader.java после метода подтягивания
1
2
3
4
5
6
7
8
|
public class ProductCsvReader extends AbstractCsvReader { Product unmarshall(String[] tokens) { Product product = new Product(Integer.parseInt(tokens[ 0 ]), tokens[ 1 ], new BigDecimal(tokens[ 2 ])); return product; } } |
Этот класс не будет компилироваться, так как он вызывает метод «unmarshall», который находится в подклассе, поэтому нам нужно создать абстрактный метод с именем unmarshall.
AbstractCsvReader.java с абстрактным методом unmarshall
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
abstract class AbstractCsvReader { Set<Product> getAll(File file) throws IOException { Set<Product> returnSet = new HashSet<>(); try (BufferedReader reader = new BufferedReader( new FileReader(file))){ String line = reader.readLine(); while (line != null && !line.trim().equals( "" )) { String[] tokens = line.split( "\\s*,\\s*" ); Product product = unmarshall(tokens); returnSet.add(product); line = reader.readLine(); } } return returnSet; } abstract Product unmarshall(String[] tokens); } |
Теперь на этом этапе AbstractCsvReader станет отличным родителем для ProductCsvReader, но не для CustomerCsvReader. CustomerCsvReader не будет компилироваться, если вы расширяете его из AbstractCsvReader. Чтобы исправить это, мы используем Generics.
AbstractCsvReader.java с обобщениями
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
abstract class AbstractCsvReader<T> { Set<T> getAll(File file) throws IOException { Set<T> returnSet = new HashSet<>(); try (BufferedReader reader = new BufferedReader( new FileReader(file))){ String line = reader.readLine(); while (line != null && !line.trim().equals( "" )) { String[] tokens = line.split( "\\s*,\\s*" ); T element = unmarshall(tokens); returnSet.add(product); line = reader.readLine(); } } return returnSet; } abstract T unmarshall(String[] tokens); } |
ProductCsvReader.java с обобщениями
1
2
3
4
5
6
7
8
9
|
public class ProductCsvReader extends AbstractCsvReader<Product> { @Override Product unmarshall(String[] tokens) { Product product = new Product(Integer.parseInt(tokens[ 0 ]), tokens[ 1 ], new BigDecimal(tokens[ 2 ])); return product; } } |
CustomerCsvReader.java с обобщениями
1
2
3
4
5
6
7
8
9
|
public class CustomerCsvReader extends AbstractCsvReader<Customer> { @Override Customer unmarshall(String[] tokens) { Customer customer = new Customer(Integer.parseInt(tokens[ 0 ]), tokens[ 1 ], tokens[ 2 ], tokens[ 3 ]); return customer; } } |
Вот и все! Нет больше повторяющегося кода! Метод в родительском классе является «шаблоном», который содержит код, который остается прежним. Вещи, которые меняются, остаются абстрактными методами, которые реализуются в дочерних классах. Помните, что при рефакторинге вы всегда должны иметь автоматические модульные тесты, чтобы убедиться, что вы не нарушите свой код. Я использовал JUnit для моего. В этом репозитории Github вы можете найти код, который я разместил здесь, а также несколько других примеров шаблонов проектирования. Прежде чем я уйду, я бы хотел кратко остановиться на недостатке метода Template. Шаблонный метод основан на наследовании, которое страдает от проблемы хрупкого базового класса . В двух словах, проблема хрупкого базового класса описывает, как изменения в базовых классах наследуются подклассами, часто вызывая нежелательные эффекты. Фактически, один из основополагающих принципов проектирования, который был найден в начале книги GoF, — «отдавай предпочтение композиции, а не наследованию», и многие другие шаблоны проектирования показывают, как избежать дублирования кода, его сложности или другого подверженного ошибкам кода с меньшей зависимостью. по наследству. Пожалуйста, дайте мне отзыв, чтобы я мог продолжать улучшать свои статьи.
Ссылка: | Шаблонный метод Пример шаблона Использование Java Generics от нашего партнера по JCG Калена Легаспи из блога |