Слайд 1Принцип разделения интерфейса (The Interface Segregation Principle)
«много интерфейсов, специально предназначенных для
клиентов, лучше, чем один интерфейс общего назначения.»
Слайд 3Содержание
ISP
Примеры
Лишняя абстракция в наследовании
«Жирный» интерфейс
Паттерн Adapter
Паттерн Стратегия
Литература
Слайд 4Пример
public interface IEmployee { bool AddDetailsEmployee(); }
Допустим все классы
Employee наследуют этот интерфейс для сохранения данных.
Теперь предположим, что
компания однажды сказала вам, что они хотят читать данные только для сотрудников в должности senior.
Что вы будете делать, просто добавьте один метод в этот интерфейс?
public interface IEmployee {
bool AddDetailsEmployee();
bool ShowDetailsEmployee(int id); }
Но теперь пусть необходимо, объектам JuniorEmployee читать свои данные из базы данных.
Решение заключается в том, чтобы передать эту ответственность другому интерфейсу:
public interface IOperationAdd { bool AddDetailsEmployee (); }
public interface IOperationGet { bool ShowDetailsEmployee (int id); }
Слайд 5Лишняя абстракция в наследовании
Проблема
Речь идет о базовых классах, которые вынуждают
своих наследников знать и делать слишком много.
Представим, - есть
базовый класс для аудиторов EntityAuditor.
Он унаследован от класса AuditorBase, который предоставляет ORM, и реализует метод AuditEntityFieldSet этого базового класса.
Также EntityAuditor добавляет свой абстрактный метод CreateLogRow, который используется в методе AuditEntityFieldSet и должен быть переопределен в конкретных реализациях.
Слайд 6EntityAuditor добавляет свой абстрактный метод CreateLogRow
public abstract class EntityAuditor extends
AuditorBase
{@Qverride
public void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object originalValue)
{
// ...
CreateLogRow(...
//
...
}
protected abstract LogRowEntity CreateLogRow(int fieldId, string oldValue, string newValue, IEntityCore entity);
}
Слайд 7После этого начинаем реализовывать наследников.
Например, создадим аудитор для класса
Product:
public class ProductAuditor extends EntityAuditor
{@override
protected LogRowEntity CreateLogRow(int fieldId, string oldValue,
string newValue, IEntityCore entity)
{
// ...
}
}
Сейчас добавлению наследников ничего не мешает.
Теперь представим, что в методе AuditEntityFieldSet понадобилась дополнительная логика, при которой нужно вызвать метод UpdateDuplicates.
Слайд 8UpdateDuplicates является абстрактным и требует реализации в наследниках
public abstract class
EntityAuditor extends AuditorBase{
public override void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object
originalValue) { // ...
CreateLogRow(...
UpdateDuplicates(… // … }
protected abstract LogRowEntity CreateLogRow(int fieldId, string oldValue, string newValue, IEntityCore entity);
protected abstract void UpdateDuplicates(IEntityCore entity, int fieldId, object current);}
public class ProductAuditor extends EntityAuditor{
protected LogRowEntity CreateLogRow(int fieldId , string oldValue, string newValue, IEntityCore entity) { // … }
protected void UpdateDuplicates(IEntityCore entity, int fieldId ,object current) {
// реализация }}
public class AccountAuditor extends EntityAuditor{
protected LogRowEntity CreateLogRow(int fieldId, string oldValue, string newValue, IEntityCore entity) { // … }
protected void UpdateDuplicates(IEntityCore entity, int fieldId, object current) { // здесь ничего нет! }}
Слайд 9EntityAuditor требует реализации метода
UpdateDuplicates даже в тех наследниках, где
он не нужен, как, например, в AccountAuditor.
Проблема в том,
что частный случай (UpdateDuplicates), который используется только в половине наследников, мы сделали общим, т.е. обязательным для всех наследников нашего EntityAuditor.
Получается, что чем больше наследников будет у EntityAuditor, тем больше бесполезного кода мы будем писать, тем больше наследники будут знать лишнего о своем базовом классе.
Это может сильно помешать нам в дальнейшем при рефакторинге или изменении логики в EntityAuditor.
Слайд 10Решение
Создадим новый интерфейсный тип
Interface IUpdateDuplicates{
protected void UpdateDuplicates(IEntityCore entity, int fieldId,
object current)}
public abstract class EntityAuditor extends AuditorBase implements IUpdateDuplicate
{
public
override void AuditEntityFieldSet(IEntityCore entity, int fieldIndex, object originalValue)
{ // ...
CreateLogRow(…
UpdateDuplicates(…
// … }
protected abstract LogRowEntity CreateLogRow(decimalfi eldId, string oldValue, string newValue, IEntityCore entity);
protected void UpdateDuplicates(IEntityCore entity, int fieldId, object current); }
Слайд 11«Жирный» интерфейс
Проблема
У нас есть интерфейс ISpecification. С помощью него мы
можем узнать подходит ли продукт заявке – метод IsSuitable и
является ли поле продукта измененным – метод IsFieldChanged:
public interface ISpecification
{
boolean IsSuitable(Product realty, Offer offer);
boolean IsFieldChanged(Product oldValue, Product newValue);
}
Слайд 12Клиенты
Чем является наш модуль для сторонних клиентов? Он является набором
интерфейсов, с помощью которых модуль может быть использован.
В данном
случае проблема заключается в том, что клиентом первой функции является консольное приложение, а второй – класс хранилища:
///
/// Хранилище для продуктов
/// public class ProductRepository implements IRepository
{
public void Save(Product product) {
// ...
specification.IsFieldChanged(… // … }}
///
/// Программа расчета подходящих продуктов
///
public class Program{
public static void Main(string[] args) { // ...
specification.IsSuitable(… }}
Слайд 13Допустим, что мы уже написали несколько конкретных спецификаций:
class PriceSpecification implements
ISpecification
{
public bool IsSuitable(Product realty, Offer offer)
{
// ...
}
public bool IsFieldChanged(Product oldValue,
Product newValue)
{
// ...
}
}
Слайд 14Полученный результат нас не устраивает
При рефакторинге или изменинии логики в
консольном приложении и методе IsSuitable, нам придется затронуть все классы,
которые реализовали интерфейс ISpecification.
Например, представьте, что будет если в метод IsSuitable мы захотим добавить еще один параметр?
А если конкретных спецификаций накопилось уже с десяток?
Основная мысль в том, что теперь различные части системы зависят друг от друга, хоть и косвенно.
Консольное приложение зависит от логики хранилища и наоборот.
Слайд 15Решение
Главное правило в данном случае звучит так: если клиенты интерфейса
разделены, то и интерфейс должен быть разделен соответствующим образом.
Слайд 16После разделения получаем:
public interface ISpecification
{
boolean IsSuitable(Product realty, Offer offer);
}
public
interface IChangeFieldDetector
{
boolean IsFieldChanged(Product oldValue, Product newValue);
}
Слайд 17Решение проблемы с зависимостью
Теперь консольное приложение работает c интерфейсом ISpecification,
а хранилище работает с интерфейсом IChangeFieldDetector. Проблема с зависимостью решена.
Кроме
этого, мы решили еще и проблему с наследниками первой реализации ISpecification. Теперь класс может реализовывать только один интерфейс, за счет чего его на много проще поддерживать:
Слайд 18Класс реализует только один интерфейс
class PriceSpecification implements ISpecification
{
public boolean IsSuitable(Product
realty, Offer offer)
{
// ...
}
}
class PriceChangeFieldDetector implements IChangeFieldDetector
{
public boolean IsFieldChanged(Product oldValue,
Product newValue)
{
// ...
}
}
Слайд 19Adapter (Wrapper)
Адаптер — это структурный паттерн проектирования, который обеспечивает совместную
работу классов с несовместимыми интерфейсами.
Проблема
К вашему приложению, работающему с данными
в XML, нужно прикрутить стороннюю библиотеку, работающую в JSON.
Например, ваше приложение скачивает биржевые котировки из нескольких источников в XML и рисует красивые графики.
В какой-то момент вы решаете улучшить приложение, применив стороннюю библиотеку аналитики. Предположим - библиотека поддерживает только JSON формат данных.
Слайд 20Решение
Вы смогли бы просто переписать её для поддержки XML.
Но
во-первых, это может нарушить работу существующего кода, который уже зависит
от библиотеки.
А во-вторых, у вас может просто не быть доступа к её исходникам.
Вы можете создать Адаптер. Это объект, который оборачивает другой объект с неудобным интерфейсом, но сам реализует интерфейс, понятный клиенту.
Адаптер получает вызов от клиента и переводит его в формат понятный обёрнутому объекту.
Адаптер может конвертировать не только данные из одного формата в другой, но и вызывать разные методы оборачиваемого объекта, если их интерфейсы кардинально отличаются.
Слайд 21Иногда возможно создать даже двухсторонний адаптер, который работал бы в
обе стороны
Таким образом, в приложении биржевых котировок, вы могли бы
создать класс XML_To_JSON_Adapter, который бы оборачивал класс библиотеки аналитики.
Ваш код посылал бы запросы этому объекту в XML, а адаптер бы сначала транслировал входящие данные в JSON, а затем передавал бы их определённым методам библиотеки.
Слайд 22Object Adapter
Object Adapter достигает своей цели с помощью композиции. На
диаграмме, представленной ниже, клиенту требуется использовать интерфейс TargetInterface.
Для этого
создается класс ObjectAdapter, который реализует интерфейс TargetInterface, а также хранит объект класса Adaptee.
При вызове метода targetMethod у Адаптера, осуществляется вызов соответствующего метода у адаптируемого интерфейса.
Слайд 23В самом простом случае реализация ObjectAdapter будет такой
public class ObjectAdapter
implements TargetInterface {
private Adaptee adaptee;
public void targetMethod() {
adaptee.method()
} }
Плюс такого подхода в том, что мы полностью отделяем клиентский интерфейс от адаптируемого интерфейса.
Слайд 24В случае с Class Adapter'ом, для достижения нашей цели используется
множественное наследование.
ClassAdapter наследуется от клиентского интерфейса и от
Адаптируемого интерфейса.
Так как в Java нет множественного наследования, то только один из предков может быть абстрактным/конкретным классом.
Второй предок будет интерфейсом, что не всегда удобно.
Диаграмма классов:
Слайд 25Реализация класса ClassAdapter:
public class ClassAdapter extends Adaptee implements TargetInterface {
public void targetMethod() {
method(); } }
При такой реализации
адаптера может возникнуть конфликт сигратур методов.
Такой проблемы у Object Adapter нет.
Class Adapter считается более простым решением в случае когда не требуется жесткого разделения клиентского и адаптируемого интерфейсов.
Слайд 26Применимость
Паттерн можно часто встретить в Java-коде, особенно там, где требуется
конвертация разных типов данных или совместная работа классов с разными
интерфейсами.
Примеры Адаптеров в стандартных библиотеках Java:
java.util.Arrays#asList()
java.util.Collections#list()
java.util.Collections#enumeration()
java.io.InputStreamReader(InputStream) (возвращает объект Reader)
java.io.OutputStreamWriter(OutputStream) (возвращает объект Writer)
javax.xml.bind.annotation.adapters.XmlAdapter#marshal() и #unmarshal()
Слайд 27Признаки применения паттерна
Адаптер получает конвертируемый объект в конструкторе или
через параметры своих методов.
Методы Адаптера обычно совместимы с интерфейсом
одного объекта.
Они делегируют вызовы вложенному объекту, превратив перед этим параметры вызова в формат, поддерживаемый вложенным объектом.
Слайд 28Практическая задача
Задан некоторый класс — SequenceGenerator, генерирующий последовательности целых чисел,
по определенному закону — это и есть наш клиент.
Есть
интерфейс — Generator, который использует клиент непосредственно для генерации каждого отдельного элемента последовательности — это наш клиентский интерфейс.
Есть класс RandomGenerator, который уже умеет генерировать случайные числа.
SequenceGenerator не может использовать RandomGenerator для генерации элементов, потому что он не соответствует клиентскому интерфейсу.
Наша задача — написать адаптер RandomGenerator к SequenceGenerator.
Слайд 29Class SequenceGenerator
Классическая реализация паттерна Class Adapter подразумевает использование множественного наследования.
В Java, можно воспользоваться имплементацией интерфейсов.
public interface Generator {
public int next();
}
public class SequenceGenerator {
private Generator generator;
public SequenceGenerator(Generator
generator) {
super();
this.generator = generator;
}
Слайд 30Class RandomGenerator
public int[] generate(int length) {
int ret[] = new int[length];
for (int i=0; i
{
public int getRandomNumber() {
return 4;
}
}
Слайд 31class RandomGeneratorAdapter
public class RandomGeneratorAdapter extends RandomGenerator implements Generator {
@Override
public int next() {
return getRandomNumber();
}
}
// Использование
public class Main {
public static void main(String[] args)
{
RandomGeneratorAdapter adapter = new RandomGeneratorAdapter();
SequenceGenerator generator = new SequenceGenerator(adapter);
for (int i: generator.generate(10)) {
System.out.print(i + " ");
}
}
}
Слайд 32Стратегия
Стратегия — это поведенческий паттерн, выносит набор алгоритмов в собственные
классы и делает их взаимозаменяемыми.
Другие объекты содержат ссылку на объект-стратегию
и делегируют ей работу.
Программа может подменить этот объект другим, если требуется иной способ решения задачи.
Слайд 33Применимость
Стратегия часто используется в Java коде, особенно там, где нужно
подменять алгоритм во время выполнения программы..
Примеры Стратегии в стандартных библиотеках
Java:
java.util.Comparator#compare(), вызываемые из Collections#sort().
javax.servlet.http.HttpServlet: метод service(), а также все методы doXXX()принимают объекты HttpServletRequest и HttpServletResponse в параметрах.
javax.servlet.Filter#doFilter()
Признаки применения паттерна: Класс делегирует выполнение вложенному объекту абстрактного типа или интерфейса.
Слайд 34Пример: Методы оплаты в интернет магазине
В этом примере Стратегия реализует выбор
платёжного метода в интернет магазине.
Когда пользователь сформировал заказ, он
получает выбор из нескольких платёжных средств: электронного кошелька или кредитной карты.
В данном случае конкретные стратегии платёжных методов не только проводят саму оплату, но и собирают необходимые данные на форме заказа.
Слайд 35PayStrategy.java: Общий интерфейс стратегий оплаты
package strategy.example.strategies;
/* Общий интерфейс всех стратегий.
*/ public interface PayStrategy {
boolean pay(int paymentAmount);
void collectPaymentDetails();
}
Слайд 36PayByPayPal.java: Оплата через PayPal
package strategy.example.strategies;
import java.io.*;
import java.util.*;
public class
PayByPayPal implements PayStrategy {
private static final Map DATA_BASE
= new HashMap<>();
private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
private String email;
private String password;
private boolean signedIn;
static {
DATA_BASE.put("amanda1985", "amanda@ya.com");
DATA_BASE.put("qwerty", "john@amazon.eu");
}
Слайд 37 Собираем данные от клиента.
@Override
public void collectPaymentDetails() {
try { while (!signedIn) {
System.out.print("Enter user email: ");
email
= READER.readLine();
System.out.print("Enter password: ");
password = READER.readLine();
if (verify()) { System.out.println("Data verification was successful"); }
else { System.out.println("Wrong email or password!"); }
} }
catch (IOException ex) { ex.printStackTrace();
} }
Слайд 38Если клиент уже вошел в систему, то для следующей оплаты
данные вводить * не придется
private boolean verify()
{ setSignedIn(email.equals(DATA_BASE.get(password)));
return
signedIn; }
@Override
public boolean pay(int paymentAmount) {
if (signedIn) { System.out.println("Paying " + paymentAmount + " using PayPal");
return true; }
else { return false; } }
private void setSignedIn(boolean signedIn) {
this.signedIn = signedIn; } }
Слайд 39strategies/PayByCreditCard.java: Оплата кредиткой
package strategy.example.strategies;
import java.io.*;
/** * Конкретная стратегия.
Реализует оплату корзины интернет магазина кредитной * картой клиента. */
public class PayByCreditCard implements PayStrategy {
private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
private CreditCard card;
Слайд 40/** Собираем данные карты клиента. */
@Override
public void collectPaymentDetails()
{
try { System.out.print("Enter card number: ");
String number =
READER.readLine();
System.out.print("Enter date 'mm/yy': ");
String date = READER.readLine();
System.out.print("Enter cvv code: ");
String cvv = READER.readLine();
card = new CreditCard(number, date, cvv);
Слайд 41// Валидируем номер карты.
} catch (IOException ex) { ex.printStackTrace();
} }
/** * После проверки карты мы можем совершить
оплату. Если клиент продолжает * покупки, мы не запрашиваем карту заново. */
@Override
public boolean pay(int paymentAmount) {
if (cardIsPresent()) { System.out.println("Paying " + paymentAmount + " using Credit Card");
card.setAmount(card.getAmount() - paymentAmount);
return true;
} else { return false; } }
private boolean cardIsPresent() { return card != null; } }
Слайд 42CreditCard.java: Кредитная карта
package strategy.example.strategies;
public class CreditCard {
private int amount;
private String number;
private String date;
private String cvv;
public
CreditCard(String number, String date, String cvv) {
this.amount = 100_000;
this.number = number;
this.date = date;
this.cvv = cvv; }
public void setAmount(int amount) {
this.amount = amount; }
public int getAmount() { return amount; } }
Слайд 43Order.java: Класс заказа
package strategy.example.order;
import strategy.example.strategies.PayStrategy;
/** * Класс заказа. Ничего
не знает о том каким способом (стратегией) будет * расчитыватся
клиент, а просто вызывает метод оплаты. Все остальное стратегия * делает сама. */
public class Order {
private static int totalCost = 0;
private boolean isClosed = false;
public void processOrder(PayStrategy strategy) {
strategy.collectPaymentDetails();
// Здесь мы могли бы забрать и сохранить платежные данные из стратегии.
}
Слайд 44Класс заказа
public void setTotalCost(int cost) {
this.totalCost += cost; }
public static int getTotalCost() {
return totalCost; }
public boolean
isClosed() {
return isClosed; }
public void setClosed() {
isClosed = true; } }
Слайд 45Клиентский код
package strategy.example;
import strategy.example.order.Order;
import strategy.example.strategies.PayByCreditCard;
import strategy.example.strategies.PayByPayPal;
import
strategy.example.strategies.PayStrategy;
import java.io.*;
import java.util.*;
public class Demo {
public
static Map priceOnProducts = new HashMap<>();
public static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
private static Order order = new Order();
private static PayStrategy strategy;
Слайд 46Начальные данные
static {
priceOnProducts.put(1, 2200);
priceOnProducts.put(2, 1850);
priceOnProducts.put(3, 1100);
priceOnProducts.put(4,
890); }
public static void main(String[] args) throws IOException {
while (!order.isClosed()) {
int cost; String continueChoice;
do { System.out.print("Select a product:" + "\n" + "1 - Mother board" + "\n" + "2 - CPU" + "\n" + "3 - HDD" + "\n" + "4 - Memory" + "\n");
int choice = Integer.parseInt(reader.readLine());
cost = priceOnProducts.get(choice);
System.out.print("Count: ");
int count = Integer.parseInt(reader.readLine());
order.setTotalCost(cost * count); System.out.print("You wish to continue selection? Y/N: ");
Слайд 47Выбор стратегий
continueChoice = reader.readLine(); }
while (continueChoice.equalsIgnoreCase("Y"));
if (strategy
== null) { System.out.println("Select a Payment Method" + "\n" +
"1 - PalPay" + "\n" + "2 - Credit Card");
String paymentMethod = reader.readLine();
// Клиент создаёт различные стратегии на основании
// пользовательских данных, конфигурации и прочих параметров.
if (paymentMethod.equals("1")) {
strategy = new PayByPayPal();
} else if (paymentMethod.equals("2")) {
strategy = new PayByCreditCard(); }
// Объект заказа делегирует сбор платёжных данных стратегии, т.к.
// только стратегии знают какие данные им нужны для
// приёма оплаты. order.processOrder(strategy); }
System.out.print("Pay " + Order.getTotalCost() + " units or Continue shopping? P/C: ");
String proceed = reader.readLine();
if (proceed.equalsIgnoreCase("P")) {
// И наконец, стратегия запускает приём платежа.
if (strategy.pay(Order.getTotalCost())) {
System.out.println("Payment has succeeded"); }
else { System.out.println("FAIL! Check your data");
} order.setClosed(); } } } }
Слайд 48Литература
https://refactoring.guru/ru/design-patterns