Я бы предложил композицию и базовый IProvider
интерфейс (ваш второй подход).Каждый провайдер, вероятно, будет иметь некоторые специальные функции, которые являются индивидуальными и поэтому должны быть инкапсулированы (принцип разделения интерфейса):
Main()
{
Context context = new Context();
IFileDataProvider fileDataProvider = new FileDataProvider();
// Configure the provider
fileDataProvider->setPath("c:\data");
context->setProvider(fileDataProvider);
// Get data. Internally this data is read from the local filesystem
DataResultObject data = context->getData();
IDatabaseProvider databaseProvider = new DatabaseProvider();
// Configure the provider
databaseProvider->login();
context->setProvider(databaseProvider);
// Get data. Internally this data is now read from the database
DataResultObject data = context->getData();
// A test case would fake the data provider to reduce complexity and improve performance.
// To achieve this a third implementation (a dummy) would be required.
IProvider fakeDataProvider = new MockDataProvider();
context->setProvider(fakeDataProvider);
// Get test data. Internally this data is created by the fake data provider
DataResultObject data = context->getData();
}
interface IProvider
{
DataResultObject getData();
}
interface IFileDataProvider extends IProvider
{
void setPath(String path);
}
class FileDataProvider implements IFileDataProvider
{
void setPath(String path)
{
this->path = path;
}
DataResultObject getData()
{
return readFromFilesystem();
}
}
interface IDatabaseProvider extends IProvider
{
void login();
}
class DatabaseProvider implements IDatabaseProvider
{
private String credentials = "login credentials";
void login()
{
login(this->credentials);
}
DataResultObject getData()
{
return readFromDatabase();
}
}
class MockDataProvider implements IProvider
{
DataResultObject getData()
{
// return empty or dummy data
return new DataResultObject();
}
}
class Context
{
IProvider provider;
public Context(IProvider provider)
{
this.provider = provider;
}
public DataResultObject getData()
{
return this.provider->getData();
}
public void setContext(IProvider provider)
{
this.provider = provider;
}
}
Если экземпляр IProvider
изменится на том же объекте Context
, то я быдобавить сеттер для переключения экземпляров.В противном случае конструктор является лучшим выбором.
Использование выделенного интерфейса для каждого типа провайдера исключает возможность переключения между провайдерами, поскольку устраняет полиморфизм.Переключение может быть полезно при написании модульных тестов.Затем вы можете легко смоделировать базу данных или файловую систему.
И использование одного общего интерфейса заставит, например, DatabaseProvider
реализовать избыточные FileDataProvider
специфические методы.Это может быть очень раздражающим и запутанным, когда классы содержат фиктивные реализации.Принцип сегрегации интерфейса ('i' в SOLID ) рекомендует по ряду веских причин избегать такого рода дизайна.