naru
naru

Reputation: 95

Java Simple Factory with constructors using different parameters

I have two ways of saving data in my application: save to database and save to file. Since I don't want client code dealing with construction of objects I created a class that (to my understanding) is simple factory with a factory method. Code below:

public static DataPersister createDataPersister(Boolean saveToDb, Session session, String filename) {
    if (saveToDb) {
        return new DatabaseDataPersister(session);
    } else {
        return new FileDataPersister(filename);
    }
}

With this setup client code doesn't have to deal with constructing anything or deciding whether to save to DB or file - it can just call a save() method on an object returned by the factory like so:

DataPersister dataPersister = DataPersisterSimpleFactory.createDataPersister(this.savetoDb, this.session, this.filename);
dataPersister.save(this.data);

My question is - is this solution breaking SOLID principles? In order to create e.g. a DatabaseDataPersister client code needs to pass on a filename parameter, and this implementation of DataPersister won't have any use of it. I feel like it doesn't sit right with something similar to Interface-segregation principle but not quite that.

And if the solution is indeed a code smell - how do I go about cleaning it?

Upvotes: 6

Views: 3640

Answers (4)

inf3rno
inf3rno

Reputation: 26129

Well the right solution here is combining the dependency injection from weston and the factory pattern from OldCurmudgeon.

public class ExampleClient {

    private final DataPersister dataPersister;

    public ExampleClient(DataPersister dataPersister) {
        this.dataPersister = dataPersister;
    }

    public void methodThatUsesSave(){
        dataPersister.save(data);
    }
}

class PersisterFactory {
    public DataPersister createDatabasePersister(Session session) {
        return new DatabasePersister(session);
    }

    public DataPersister createFilePersister(String filename) {
        return new FilePersister(filename);
    }
}

The upper level code:

PersisterFactory = new PersisterFactory();
DataPersister dataPersister;
if (saveToDb)
    dataPersister = PersisterFactory.createDatabasePersister(new Session());
else
    dataPersister = PersisterFactory.createFilePersister("Hello");
ExampleClient example = new ExampleClient(dataPersister);

Usually the dataPersister comes from the DI container and the saveToDb comes from the config, but of course testing can be an exception.

Upvotes: 0

weston
weston

Reputation: 54781

The SOLID principle I think is in violation is DIP.

Your client classes, by having to depend on the static factory directly, have a compile-time dependency on actual implementations, DatabaseDataPersister and FileDataPersister, rather than just the abstraction DataPersister.

To solve, supply to the client the DataPersister you want them to use. The constructor is usually a good place for this:

public class ExampleClient {

    private final DataPersister dataPersister;

    public ExampleClient(DataPersister dataPersister) {
        this.dataPersister = dataPersister;
    }

    public void methodThatUsesSave(){
        dataPersister.save(data);
    }
}

This code compiles without the concrete implementations, i.e. it has no dependency on them. The client also doesn't need to know the filename or session so it solves that code smell too.

We can decide which concrete implementation to give it at construction time, here I use your existing method:

DataPersister dataPersister = DataPersisterSimpleFactory.createDataPersister(this.savetoDb, this.session, this.filename);
ExampleClient example = new ExampleClient(dataPersister);

Upvotes: 3

OldCurmudgeon
OldCurmudgeon

Reputation: 65813

This is a perfect opportunity to use the factory pattern

interface DataPersister {
    void persist(String s);
}

private class DatabasePersister implements DataPersister {
    final Session session;

    public DatabasePersister(Session session) {
        this.session = session;
    }

    @Override
    public void persist(String s) {
        System.out.println("Persist to database: " + s);
    }
}

private class FilePersister implements DataPersister {
    final String filename;

    public FilePersister(String filename) {
        this.filename = filename;
    }

    @Override
    public void persist(String s) {
        System.out.println("Persist to file: " + s);
    }
}

class PersisterFactory {
    public DataPersister createDatabasePersister(Session session) {
        return new DatabasePersister(session);
    }

    public DataPersister createFilePersister(String filename) {
        return new FilePersister(filename);
    }
}

public void test(String[] args) {
    DataPersister databasePersister = new PersisterFactory().createDatabasePersister(new Session());
    databasePersister.persist("Hello");
    DataPersister filePersister = new PersisterFactory().createFilePersister("Hello");
    filePersister.persist("Hello");
}

Upvotes: 2

Dave Newton
Dave Newton

Reputation: 160181

You already pass a bunch of stuff irrelevant to various persisters.

As it stands you need a method that takes a Session and one that takes a String and you're done. No need for a boolean, no need for useless params. That handles your decision making with no cruft.

Whether or not that's a good idea... I'm ambivalent. You're not saving much; might as well just have a static factory in each type so it's explicit in the code what type you're creating.

Consider what happens when you add a new persister, like a REST endpoint, that would take a URL (could be a string, could be an actual URL). You now need even more useless parameters etc. Or you could pass in a URI from the beginning, e.g., file:// or http:// and get around that problem.

There are any number of ways this could be done–I'm not convinced there's a "clearly correct" answer, and it may boil down to opinion.

Upvotes: 1

Related Questions