Разделение логики службы из данных

Я просматривал несколько классов, которые у меня есть в проекте Android, и я понял, что я смешиваю логику с данными. Поняв, насколько плохо это может быть с точки зрения удобочитаемости и тестовой способности моего проекта, я решил сделать некоторые рефакторинг, чтобы абстрагировать логику всех служб на отдельные модули услуг. Однако, поскольку я полагался на полиморфизм Java, я потерялся и нуждаюсь в некоторых рекомендациях.

Предположим, что у меня есть этот "to-be-changed" макет для суперкласса данных и два подкласса:

public class DataItem {
    /* some variables */ 

    public saveToDB(/* Some Arguments */) {
        /* do some stuff */
    }

    public render() {
        /* render the class */
    }
}

public class ChildDataItemA extends DataItem {
    @Override
    public saveToDB(/* Some Arguments */) {
        super.saveToDB(); 
        /* more specific logic to ChildDataItemA */
    }

    @Override
    public render() {
        /* render logic for ChildDataItemA */
    }
}

public class ChildDataItemB extends DataItem {
    @Override
    public saveToDB(/* Some Arguments */) {
        super.saveToDB(); 
        /* more specific logic to ChildDataItemB */
    }

    @Override
    public render() {
        /* render logic for ChildDataItemB */
    }
}

Теперь я подумал о переносе методов saveToDB() и render() в класс службы. Однако иногда мне нужно иметь возможность вызвать этот метод в экземпляр скомпилированного типа DataItem, не зная его тип времени выполнения. Например, я могу сделать следующий вызов:

List<DataItem> dataList; 
for (DataItem item: dataList) {
    item.saveToDB();
    item.render();
}

Кроме того, я подумал о следующем:

public class ChildDataItemB extends DataItem {
    @Override
    public saveToDB(/* Some Arguments */) {
        super.saveToDB(); 
        /* more specific logic to ChildDataItemB */
         Service.saveToDBB();
    }

    @Override
    public render() {
        /* render logic for ChildDataItemB */
        Service.renderB();
    }
}

Где я все еще сохраняю методы 'dummy' в каждом подклассе, который вызовет соответствующий метод службы. Тем не менее, я не думаю, что это действительно достигает разделения, которое я хочу, поскольку классы данных все равно будут знать об услугах (плохо!).

Любые идеи о том, как это решить?

Изменить: обратите внимание, что render() и saveToDB() являются просто типичными примерами того, что эти методы могут быть, поэтому проблема заключается не в выборе методов ORM или SQL.

Ответы

Ответ 1

шаблон посетителя для спасения. Создайте интерфейс посетителя и каждый сервис реализует этот интерфейс:

public interface DataItemVisitor {
  // one method for each subtype you want to handle
  void process(ChildDataItemA item);
  void process(ChildDataItemB item);
}

public class PersistenceService implements DataItemVisitor { ... }
public class RenderService implements DataItemVisitor { ... }

Затем каждый DataItem реализует метод accept:

public abstract class DataItem {
  public abstract void accept(DataItemVisitor visitor);
}

public class ChildDataItemA extends DataItem {
  @Override
  public void accept(DataItemVisitor visitor) {
    visitor.process(this);
  }
}

public class ChildDataItemB extends DataItem {
  @Override
  public void accept(DataItemVisitor visitor) {
    visitor.process(this);
  }
}

Обратите внимание, что все реализации accept выглядят одинаково, но this относится к правильному типу в каждом подклассе. Теперь вы можете добавлять новые службы без изменения классов DataItem.

Ответ 2

Итак, вы хотите:

List<DataItem> dataList; 
for (DataItem item: dataList) {
    service.saveToDB(item);
    service.render(item);
}

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

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

ChildDataItemA.xml
<metaData>
   <column name="..." property="..."/>
</metaData>

Вы можете получить тот же результат через отражение и аннотации.

В вашем случае также может работать приложение Bridge pattern:

class DataItem {
    public describeTo(MetaData metaData){
       ...
    }    
}

class Service {
   public void saveToDB(DataItem item) {
      MetaData metaData = new MetaData();
      item.describeTo(metaData);
      ...
   }
}

Ваши метаданные могут быть отделены от сохранения или рендеринга, поэтому вы можете сделать то же самое для обоих.

Ответ 3

Я бы очистил классы "данных" методов render и saveToDB.

Вместо этого я создам иерархию оболочек для DataItem (ей не нужно точно имитировать иерархию DataItem). Этими обертками будут те, которые реализуют эти методы.

Кроме того, я предлагаю (если можно), вы переходите к некоторому ORM (объектно-реляционное сопоставление), например Hibernate или JPA, чтобы избавиться от метода saveToDB.

Ответ 4

Прежде всего, класс DataItem должен быть чистым, только с геттерами и установщиком и без какой-либо логики, как POJO. Кроме того, ваш DataItem может быть абстрактным.

Теперь, поскольку логика, как и другие, предположила, что я бы использовал некоторую структуру ORM для части saveToDB, но вы сказали, что это не помогает вам вызывать проект android, и у вас есть другие методы, подобные этому.

Итак, что бы я сделал, это создать интерфейс IDataItemDAO со следующей логикой:

public interface IDataItemDAO<T extends DataItem > {
    public void saveToDB(T data, /* Some Arguments */);
    ... other methods that you need ...
}

Я бы создал абстрактную DAO для DataItem и поместил бы ее весь похожий код всех DataItems:

public abstract class ChildDataItemADAO impelemets IDataItemDAO<DataItem> {
    @Override
    public void saveToDB(DataItem data, /* Some Arguments */); {
        ...
    }
}

чем я бы создал DAO для каждого класса DataItem, который у вас есть:

public class ChildDataItemADAO extends DataItemDAO impelemets IDataItemDAO<ChildDataItemA> {
    @Override
    public void saveToDB(ChildDataItemA data, /* Some Arguments */); {
        super(data, ...);
        //other specific saving
    }
}

другая часть - это то, как использовать правильный DAO для правильного экземпляра, для этого я бы создал класс, который принесет мне правильный DAO для данного экземпляра, это очень простой метод, если инструкции if-else ( или вы можете сделать это динамически с помощью карты класса и DAO)

public DataItemDAO getDao(DataItem item) {
    if (item instanceof ChildDataItemA) {
        //save the instance ofcourse
        return new ChildDataItemADAO();
    }
}

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

List<DataItem> dataList; 
for (DataItem item: dataList) {
    factory.getDao(item).saveToDB(item);
}

Ответ 5

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

Создайте свой класс данных DataItem, ChildDataItemA, ChildDataItemB без метода, работающего с данными

Создайте интерфейс для некоторых операций над вашим классом данных, например

public interface OperationGroup1OnDataItem {
        public void saveToDB(DataItem dataItem/*plus other params*/) {

        }
        public void render(DataItem dataItem/*plus other params*/) {

        }
        ......
}

Создайте factory для реализации оператора OperationGroup

public class OperationFactoryProvider {
    public static OperationGroup1OnDataItem getOperationGroup1For(Class class) {
    ....
    }
}

Используйте его в своем коде:

List<DataItem> dataList; 
for (DataItem item: dataList) {
    OperationGroup1OnDataItem  provider OperationFactoryProvider.getOperationGroup1For(item.class);
    provider.saveToDB(item);
    provider.render(item);
}

Вы можете реализовать factory с помощью простой статической карты, где вы кладете класс (или класс fullName) в качестве ключа и объект, реализующий интерфейс в качестве значения; что-то вроде

 Map<String,OperationGroup1OnDataItem> factoryMap= new HashMap<String,OperationGroup1OnDataItem>();
 factoryMap.put(DataItem.class.getName(),new SomeClassThatImplementsOperationGroup1OnDataItemForDataItem());
 factoryMap.put(ChildDataItemA.class.getName(),new SomeClassThatImplementsOperationGroup1OnDataItemForChildDataItemA());

Реализация getOperationGroup1For:

return factoryMap.get(item.getClass().getName());

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