Используя стратегию или дополнительный класс?

#java #oop #design-patterns #strategy-pattern

#java #ооп #шаблоны проектирования #стратегия-шаблон

Вопрос:

У меня есть следующий пример кода (код ниже).

У меня есть интерфейсы OnlyReverse , OnlySplit которые определяют операции над классом Data . Иногда у меня будет класс, доступный только для реверсирования, а иногда я смогу выполнять реверсирование и разделение.

В коде у меня есть 2 подхода.

1-й подход заключается в использовании 2 отдельных классов для этих 2 отдельных вариантов использования ReverseAndSplitImpl и OnlyReverseImpl . Здесь мне не нравится, что мне нужен дополнительный класс, и что мне нужно дублировать часть кода между этими 2 классами.

2-й подход заключается в использовании 1 класса для обоих вариантов использования, SingleClassForReverseAndSplitImpl а затем использовании стратегии для введения либо NormalSplit или NoSplit . Здесь мне не нравится этот дополнительный NoSplit класс, который в основном искусственный.

В соответствии с принципом разделения интерфейса — нужно ли мне иметь ReverseAndSplit интерфейс присоединения, или я всегда должен использовать оба интерфейса отдельно (like SingleClassForReverseAndSplitImpl implements OnlyReverse, OnlySplit и not SingleClassForReverseAndSplitImpl implements ReverseAndSplit )?

Какой из этих подходов лучше в долгосрочной перспективе (более гибкий в будущем)?

 class Data{
  String a;
}

interface OnlyReverse{
  Data getData();
  OnlyReverse reverse();
}

interface OnlySplit{
  OnlySplit split();
}

interface ReverseAndSplit extends OnlyReverse, OnlySplit{
  @Override
  ReverseAndSplit reverse();
  @Override
  ReverseAndSplit split();
}

//------------------------- USE DISTINCT CLASSES; ONE HAS SPLIT OTHER NO

class ReverseAndSplitImpl implements ReverseAndSplit{
  Data data;

  public ReverseAndSplitImpl(Data data) {
    this.data = data;
  }

  @Override
  public Data getData() {
    return data;
  }

  @Override
  public ReverseAndSplit reverse() {
    //here reverse and return
    return new ReverseAndSplitImpl(data);
  }

  @Override
  public ReverseAndSplit split() {
    //here split and return
    return new ReverseAndSplitImpl(data);
  }
}

class OnlyReverseImpl implements OnlyReverse{
  Data data;

  public OnlyReverseImpl(Data data) {
    this.data = data;
  }

  @Override
  public Data getData() {
    return data;
  }

  @Override
  public OnlyReverse reverse() {
    return new OnlyReverseImpl(data);
  }
}

//------------------------- USE DISTINCT CLASSES; ONE HAS SPLIT OTHER NO

//------------------------- USE STRATEGY TO CHOOSE TO HAVE SPLITTING OR NO
interface SplitStrategy{
  Data split(Data data);
}
class NormalSplit implements SplitStrategy{
  @Override
  public Data split(Data data) {
    return new Data();
  }
}
//NullObject pattern
class NoSplit implements SplitStrategy{
  @Override
  public Data split(Data data) {
    return data;
  }
}

class SingleClassForReverseAndSplitImpl implements ReverseAndSplit{
  Data data;
  SplitStrategy splitStrategy;

  public SingleClassForReverseAndSplitImpl(Data data, SplitStrategy splitStrategy) {
    this.data = data;
    this.splitStrategy = splitStrategy;
  }

  @Override
  public Data getData() {
    return data;
  }

  @Override
  public ReverseAndSplit reverse() {
    //here reverse and return
    return new SingleClassForReverseAndSplitImpl(data, splitStrategy);
  }

  @Override
  public ReverseAndSplit split() {
    //here split and return
    SingleClassForReverseAndSplitImpl s = new SingleClassForReverseAndSplitImpl(data, splitStrategy);
    s.data = splitStrategy.split(data);
    return s;
  }
}
//------------------------- USE STRATEGY TO CHOOSE TO HAVE SPLITTING OR NO

public class Decorator {

  public static void main(String[] args) {

    ReverseAndSplit s11 = new SingleClassForReverseAndSplitImpl(new Data(), new NoSplit());
    s11 = s11.reverse();
    s11 = s11.split();  //has split operation, but NoSplit will do nothing

    OnlyReverse s12 = new OnlyReverseImpl(new Data());
    s12 = s12.reverse();
    //has no split operation present

    //Going from NoSplit to SplitAndReverse
    ReverseAndSplit s21 = new SingleClassForReverseAndSplitImpl(s11.getData(), new NormalSplit());
    s21 = s21.reverse();
    s21 = s21.split();  //has split and now it is using NormalSplit

    ReverseAndSplit s22 = new ReverseAndSplitImpl(s12.getData());
    s22 = s22.reverse();
    s22 = s22.split();

  }
}
  

Комментарии:

1. Предоставление альтернативы / ReverseAndSplit не обязательно плохо , плохо то, что ваш код зависит от этих более конкретных интерфейсов. Для целей реализации обычно проще объединить и использовать единый интерфейс

Ответ №1:

Я думаю, разумно использовать ваш первый подход (два более простых интерфейса), но не вызывать их OnlyReverse и OnlySplit , но Reverse и Split . Поэтому разумно их объединить, и в будущем вы также можете просто использовать один из них.

Но это также зависит от объектов вашего домена, поэтому вам сложно сделать выбор. Но я бы не стал создавать вызываемый интерфейс NoSplit , потому что вы все равно могли бы реализовать ReverseAndSplit и NoSplit который не имел бы для вас никакого смысла.

Ответ №2:

В соответствии с принципом разделения интерфейса, если у вас есть клиент, который никогда не разделяется, то вы не должны предоставлять этому клиенту интерфейс, включающий split метод. Подход Strategy / Null Object подходит для одного клиента, который иногда требует разделения, а иногда нет. Подход с отдельными интерфейсами подходит для двух разных клиентов, у которых разные требования.

Комментарии:

1. split это дополнительная функциональность, которая может быть выполнена для внутренних данных (или нет, если для стратегии установлено значение null object). все остальное, касающееся логики, должно оставаться неизменным. Я предполагаю, что это подход с одним клиентом…

2. По сути, используя этот подход, я хочу запретить некоторые из SingleClassForReverseAndSplitImpl split них, а для некоторых других разрешить это — например, на основе Data того, с помощью чего я его инициализирую. Если для данных установлено значение «1», «2», «3» — затем разрешите split , в противном случае используйте NoSplit .

3. @MarkoKraljevic Как клиент узнает, могут ли они вызывать split или нет? Как бы они справились с невозможностью разделить / отменить? Неясно, как клиенты будут использовать эти классы и интерфейсы.

4. @plalx Я думаю, они не будут. Поскольку у меня может быть логика для инициализации стратегии внутри моего класса, но метод split() всегда будет там. Просто иногда он ничего не будет делать, если его вызвать. Есть ли какой-нибудь лучший подход, подобный 1-му, но не дублировать один и тот же код? Например, делегировать все те же методы родительскому, и внутри обрабатывается только один split(). Но тогда — как получить доступ Data к делегированному классу?

5. @MarkoKraljevic Как бы вы отнеслись к вызову «string».split() и к тому, чтобы он ничего не делал? Будут ли клиенты писать алгоритмы для обработки данных? Если мы не знаем, как клиенты будут использовать эти интерфейсы и классы, мы не сможем помочь в разработке.