Reputation: 95
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
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
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
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
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