How to create many singleton instances for a generic class?

89 Views Asked by At

I have such code:

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class FileManager<T> {

    private final File file;

    private FileManager(String property) throws IOException {
        this.file = new File(ConfigReader.getProperty(property + ".file.path")
                .orElseThrow(() -> new IOException(property + " file path is not specified in application.properties.")));
    }

    public void saveToFile(List<T> list) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
            oos.writeObject(list);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @SuppressWarnings("unchecked")
    private List<T> loadFromFile() {
        if (file.exists()) {
            try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
                return (List<T>) ois.readObject();
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return new ArrayList<>();
    }

}

And I have also two classes (ex. Manufacturer, Product), objects of which i need to serialize and deserialize. Basically it's the same code for these classes, so I came up with this generic class idea. I think it would be better if there were two and only two instances for two of my classes.

So, singleton pattern is not quite suitable here. The only solution I came up with is to use multitone pattern, but I think it may be quite bad. Are there any patterns or better solutions for this situation? Maybe I should consider to redesign this class or smth? What is the best to do here?

3

There are 3 best solutions below

0
Lajos Arpad On BEST ANSWER

You could create a base class where you would inherit Manager and Product from, so you will not duplicate your code and you can apply the singleton patter for the subclasses, maybe have the base class instantiate its subclasses and then you are all set.

1
hfontanez On

Although I am not 100% clear in what you want to do, I think I have an idea of what you need to do. Your current solution doesn't work precisely because you are trying to use a template to model creation when templates are designed to model behavior. You also seem to be attempting to solve too many problems with a single solution. If you examine Design Patterns, they are divided into categories related to the type of problem they are designed to solve. For example, you have Creational Patterns, Behavioral Patterns, etc.

Reading through your post, I came to the conclusion that you need to solve at least two fundamental issues.

  1. You need to create something.
  2. That something is actually a family of things.

The way I would go about solving this is to take the simplest approach instead of the most efficient approach. Remember, you should always optimize later (when you actually encounter the need for optimization). For me, the simplest approach is one I am more familiarized with or one I research that it's the easiest for me to understand and implement quickly. When it comes to creating family of things, the one for easiest for me to wrap my head around is the Factory Pattern.

Now that you have established the mechanism to create a family of things, you can decide that those things the factory is suppose to create. Do they need to be Singletons? That is open for debate and it is fundamentally irrelevant for this solution. Your factory doesn't care that the things it's creating can or not control instantiation of their individual types. In fact, your factory could create some Singleton objects and some regular objects.

Furthermore, you could apply the Template Pattern to model the behavior of the objects the factory is creating. You can combine this into the final solution because those two patterns are designed to address different issues.

2
HuyMaster On

My code works fine on Android 13, Java 17. Maybe it will help you

Sorry that my code is not very well optimized

import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.github.huymaster.controller.utils.PreferenceUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public abstract class Database<T> {
    private final List<T> database = new ArrayList<>();
    private final String TAG = this.getClass().getSimpleName();
    private final AtomicBoolean initialized = new AtomicBoolean(false);
    private final Context context;
    private final ContentResolver resolver;
    private final YAMLMapper mapper = new YAMLMapper();
    private DocumentFile dataDir;

    protected Database(@NonNull final Context context) {
        this.context = context;
        this.resolver = context.getContentResolver();

        init();

        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    }

    private void init() {
        final var uriStr = PreferenceUtils.getInstance(context).getSharedPreferences().getString("shared_storage", "");
        final var uri = Uri.parse(uriStr);

        var root = DocumentFile.fromTreeUri(context, uri);
        if (root != null && root.exists()) {
            if (root.isDirectory()) {
                initStorage(root);
            } else
                Log.w(TAG, "init: failed (required directory, not file)");
        } else
            Log.w(TAG, "init: failed (can't get root directory");
    }

    protected abstract T getDatabaseType();
    protected abstract TypeReference<List<T>> getDatabaseReference();
    protected abstract String primaryKey();

    private void initStorage(@NonNull final DocumentFile root) {
        final var dataName = getDatabaseType().getClass().getSimpleName().toLowerCase() + "_data";
        dataDir = root.findFile(dataName);
        if (dataDir == null) {
            dataDir = root.createFile("application/octet-stream", dataName);
        } else {
            if (!dataDir.exists()) {
                dataDir = null;
                Log.w(TAG, "initStorage: not found" + dataName);
            } else {
                if (dataDir.isDirectory()) {
                    dataDir.delete();
                    dataDir = root.createFile("application/octet-stream", dataName);
                }
            }
        }
        if (dataDir == null)
            Log.w(TAG, "initStorage: can't create " + dataName);
        else
            initialized.set(true);
    }

    private boolean checkInitialized() {
        if (initialized.get())
            return true;
        else {
            Log.w(TAG, "checkInitialized: storage not initialized");
            return false;
        }
    }

    protected final void load() {
        if (!checkInitialized())
            return;
        try (InputStream inputStream = resolver.openInputStream(dataDir.getUri())) {
            var read = mapper.readValue(inputStream, getDatabaseReference());
            if (read != null)
                mergeList(database, read, primaryKey());
        } catch (IOException e) {
            Log.w(TAG, "load: failed", e);
        }
    }

    protected final void save() {
        if (!checkInitialized())
            return;
        try (OutputStream outputStream = resolver.openOutputStream(dataDir.getUri(), "rwt")) {
            mapper.writeValue(outputStream, database);
        } catch (IOException e) {
            Log.w(TAG, "save: failed", e);
        }
    }

    protected final boolean mergeObject(@NonNull final T oldOne, @NonNull final T newOne) {
        var dataClass = getDatabaseType().getClass();
        String primaryKey = primaryKey();
        boolean skipCheckKey = primaryKey == null;
        final var fields = Arrays
                .stream(dataClass.getDeclaredFields())
                .collect(Collectors.toSet());
        try {
            if (!skipCheckKey) {
                var opt = fields
                        .stream()
                        .filter(field -> field.getName().equalsIgnoreCase(primaryKey))
                        .findAny();
                if (opt.isPresent()) {
                    var field = opt.get();
                    field.setAccessible(true);

                    var o = field.get(oldOne);
                    var n = field.get(newOne);
                    if (!Objects.equals(o, n)) {
                        Log.w(TAG, "mergeObject: can't merge 2 objects with different " + primaryKey);
                        return false;
                    }
                } else {
                    Log.w(TAG, "mergeObject: field not found: " + primaryKey);
                    return false;
                }
            } else Log.w(TAG, "mergeObject: skip key check");
            for (Field field : fields) {
                field.set(oldOne, field.get(newOne));
            }
        } catch (Exception e) {
            Log.w(TAG, "mergeObject: failed", e);
            return false;
        }
        return true;
    }

    protected final void mergeList(@NonNull final List<T> oldList, @NonNull final List<T> newList, @NonNull final String primaryKey) {
        var dataClass = getDatabaseType().getClass();
        Field field = null;
        try {
            field = dataClass.getDeclaredField(primaryKey);
        } catch (NoSuchFieldException e) {
            Log.w(TAG, "mergeList: can't find field", e);
            return;
        }
        final Field finalField = field;
        Predicate<T> predicate = t1 -> newList.stream().anyMatch(t2 -> {
            try {
                return !Objects.equals(finalField.get(t1), finalField.get(t2));
            } catch (IllegalAccessException e) {
                System.err.println(e.getMessage());
                return true;
            }
        });

        oldList.removeIf(predicate);
        for (var t : newList) {
            var opt = oldList.stream()
                    .filter(t1 -> {
                        try {
                            return Objects.equals(finalField.get(t), finalField.get(t1));
                        } catch (IllegalAccessException e) {
                            Log.w(TAG, "mergeList: skip", e);
                            return false;
                        }
                    }).findAny();
            if (opt.isPresent()) {
                var old = opt.get();
                if (!mergeObject(old, t)) {
                    Log.w(TAG, "mergeList: merge failed " + String.format("%s@%d", old.getClass().getSimpleName(), old.hashCode()));
                }
            } else
                oldList.add(t);
        }
    }

    public final boolean isInitialized() {
        return initialized.get();
    }

    public boolean add(T data) {
        var list = getDatabase();
        var result = list.add(data);

        save();
        return result;
    }

    public boolean remove(Predicate<T> condition) {
        var list = getDatabase();
        var result = list.removeIf(condition);

        save();
        return result;
    }

    public final List<T> getDatabase() {
        load();
        save();
        return database;
    }
}

Example child

import android.content.Context;

import androidx.annotation.NonNull;

import com.fasterxml.jackson.core.type.TypeReference;

import java.util.List;

public class ProductDatabase extends Database<Product> {
    protected ProductDatabase(@NonNull Context context) {
        super(context);
    }

    public static ProductDatabase getInstance(@NonNull Context context) {
        return new ProductDatabase(context);
    }

    @Override
    protected Product getDatabaseType() {
        return new Product();
    }

    @Override
    protected TypeReference<List<Product>> getDatabaseReference() {
        return new TypeReference<>() {
        };
    }

    @Override
    protected String primaryKey() {
        return "id";
    }

    public boolean exist(@NonNull String id) {
        return getDatabase()
                .stream()
                .anyMatch(product -> product.id.equalsIgnoreCase(id));
    }

    public boolean create(@NonNull String id, @NonNull String displayName, float cost, float fee) {
        var product = new Product();
        product.id = id;
        product.displayName = displayName;
        product.cost = cost;
        product.fee = fee;

        var result = add(product);
        save();
        return result;
    }

    public boolean delete(@NonNull String id) {
        if (exist(id)) {
            return remove(product -> product.id.equalsIgnoreCase(id));
        }
        return false;
    }
}