Ответ Калеба, хотя он на правильном пути, на самом деле неверен. Его Foo
класс выступает в качестве фасада базы данных и фабрики. Это две обязанности, и их не следует объединять.
Этот вопрос, особенно в контексте базы данных, задавался слишком много раз. Здесь я попытаюсь подробно показать вам преимущества использования абстракции (использования интерфейсов), чтобы сделать ваше приложение менее связанным и более универсальным.
Прежде чем читать дальше, я рекомендую вам прочитать и получить базовое представление о внедрении зависимости , если вы еще этого не знаете. Вы также можете проверить шаблон проектирования адаптера , который в основном означает скрытие деталей реализации за открытыми методами интерфейса.
Внедрение зависимостей в сочетании с шаблоном проектирования Factory является основой и простым способом кодирования шаблона проектирования Strategy , являющегося частью принципа IoC .
Не звоните нам, мы вам позвоним . (АКА Голливудский принцип ).
Отделение приложения с помощью абстракции
1. Создание слоя абстракции
Вы создаете интерфейс - или абстрактный класс, если вы программируете на языке, подобном C ++, - и добавляете универсальные методы в этот интерфейс. Поскольку и интерфейсы, и абстрактные классы ведут себя так, что вы не можете использовать их напрямую, но вы должны либо реализовать (в случае интерфейса), либо расширить (в случае абстрактного класса) их, сам код уже предполагает, что вы будете необходимо иметь конкретные реализации для выполнения контракта, заданного интерфейсом или абстрактным классом.
Ваш (очень простой пример) интерфейс базы данных может выглядеть следующим образом (классы DatabaseResult или DbQuery соответственно будут вашими собственными реализациями, представляющими операции с базой данных):
public interface Database
{
DatabaseResult DoQuery(DbQuery query);
void BeginTransaction();
void RollbackTransaction();
void CommitTransaction();
bool IsInTransaction();
}
Поскольку это интерфейс, он сам ничего не делает. Поэтому вам нужен класс для реализации этого интерфейса.
public class MyMySQLDatabase : Database
{
private readonly CSharpMySQLDriver _mySQLDriver;
public MyMySQLDatabase(CSharpMySQLDriver mySQLDriver)
{
_mySQLDriver = mySQLDriver;
}
public DatabaseResult DoQuery(DbQuery query)
{
// This is a place where you will use _mySQLDriver to handle the DbQuery
}
public void BeginTransaction()
{
// This is a place where you will use _mySQLDriver to begin transaction
}
public void RollbackTransaction()
{
// This is a place where you will use _mySQLDriver to rollback transaction
}
public void CommitTransaction()
{
// This is a place where you will use _mySQLDriver to commit transaction
}
public bool IsInTransaction()
{
// This is a place where you will use _mySQLDriver to check, whether you are in a transaction
}
}
Теперь у вас есть класс, который реализует Database
, интерфейс стал просто полезным.
2. Использование слоя абстракции
Где-то в вашем приложении у вас есть метод, давайте вызовем метод SecretMethod
, просто для удовольствия, и внутри этого метода вы должны использовать базу данных, потому что вы хотите получить некоторые данные.
Теперь у вас есть интерфейс, который вы не можете создать напрямую (ну, как мне тогда его использовать), но у вас есть класс MyMySQLDatabase
, который может быть создан с использованием new
ключевого слова.
ОТЛИЧНЫЙ! Я хочу использовать базу данных, поэтому я буду использовать MyMySQLDatabase
.
Ваш метод может выглядеть так:
public void SecretMethod()
{
var database = new MyMySQLDatabase(new CSharpMySQLDriver());
// you will use the database here, which has the DoQuery,
// BeginTransaction, RollbackTransaction and CommitTransaction methods
}
Это не хорошо. Вы непосредственно создаете класс внутри этого метода, и если вы делаете это внутри SecretMethod
, можно с уверенностью предположить, что вы будете делать то же самое в 30 других методах. Если вы хотите изменить MyMySQLDatabase
класс на другой, например MyPostgreSQLDatabase
, вам придется изменить его во всех ваших 30 методах.
Другая проблема заключается в том, что если создание MyMySQLDatabase
не удалось, метод никогда не завершится и, следовательно, будет недействительным.
Мы начнем с рефакторинга создания MyMySQLDatabase
, передав его в качестве параметра методу (это называется внедрением зависимости).
public void SecretMethod(MyMySQLDatabase database)
{
// use the database here
}
Это решает вам проблему, что MyMySQLDatabase
объект никогда не может быть создан. Поскольку объект SecretMethod
ожидает действительный MyMySQLDatabase
объект, если что-то случится и объект никогда не будет передан ему, метод никогда не запустится. И это совершенно нормально.
В некоторых приложениях этого может быть достаточно. Вы можете быть удовлетворены, но давайте сделаем рефакторинг еще лучше.
Цель другого рефакторинга
Вы можете видеть, прямо сейчас SecretMethod
использует MyMySQLDatabase
объект. Предположим, вы перешли с MySQL на MSSQL. Вы действительно не чувствуете , как меняется вся логика внутри вашего SecretMethod
, метод , который называет BeginTransaction
и CommitTransaction
методы по database
переменной , переданной в качестве параметра, так что вы создать новый класс MyMSSQLDatabase
, который также будет иметь BeginTransaction
и CommitTransaction
методы.
Тогда вы идете вперед и измените объявление SecretMethod
на следующее.
public void SecretMethod(MyMSSQLDatabase database)
{
// use the database here
}
А поскольку классы MyMSSQLDatabase
и MyMySQLDatabase
имеют одинаковые методы, вам не нужно ничего менять, и все равно будет работать.
Ой, подождите!
У вас есть Database
интерфейс, который MyMySQLDatabase
реализует, у вас также есть MyMSSQLDatabase
класс, который имеет те же методы, что и MyMySQLDatabase
, возможно, драйвер MSSQL мог бы также реализовать Database
интерфейс, поэтому вы добавляете его в определение.
public class MyMSSQLDatabase : Database { }
Но что, если я в будущем больше не буду использовать MyMSSQLDatabase
, потому что я перешел на PostgreSQL? Я должен был бы снова заменить определение SecretMethod
?
Да, вы бы. И это не звучит правильно. Прямо сейчас мы знаем, что MyMSSQLDatabase
и у MyMySQLDatabase
нас одни и те же методы и оба реализуют Database
интерфейс. Таким образом, вы рефакторинг SecretMethod
выглядит так.
public void SecretMethod(Database database)
{
// use the database here
}
Обратите внимание, как SecretMethod
больше не знает, используете ли вы MySQL, MSSQL или PotgreSQL. Он знает, что использует базу данных, но не заботится о конкретной реализации.
Теперь, если вы хотите создать свой новый драйвер базы данных, например, для PostgreSQL, вам не нужно менять его SecretMethod
вообще. Вы сделаете MyPostgreSQLDatabase
, сделаете так, чтобы он реализовывал Database
интерфейс, и как только вы закончите кодировать драйвер PostgreSQL, и он заработает, вы создадите его экземпляр и внедрите его в SecretMethod
.
3. Получение желаемой реализации Database
Вы все еще должны решить, прежде чем вызывать SecretMethod
, какую реализацию Database
интерфейса вы хотите (будь то MySQL, MSSQL или PostgreSQL). Для этого вы можете использовать шаблон фабричного дизайна.
public class DatabaseFactory
{
private Config _config;
public DatabaseFactory(Config config)
{
_config = config;
}
public Database getDatabase()
{
var databaseType = _config.GetDatabaseType();
Database database = null;
switch (databaseType)
{
case DatabaseEnum.MySQL:
database = new MyMySQLDatabase(new CSharpMySQLDriver());
break;
case DatabaseEnum.MSSQL:
database = new MyMSSQLDatabase(new CSharpMSSQLDriver());
break;
case DatabaseEnum.PostgreSQL:
database = new MyPostgreSQLDatabase(new CSharpPostgreSQLDriver());
break;
default:
throw new DatabaseDriverNotImplementedException();
break;
}
return database;
}
}
Как вы видите, фабрика знает, какой тип базы данных использовать из файла конфигурации (опять же, Config
класс может быть вашей собственной реализацией).
В идеале у вас будет DatabaseFactory
внутри вашего контейнера инъекций зависимости. Ваш процесс может выглядеть следующим образом.
public class ProcessWhichCallsTheSecretMethod
{
private DIContainer _di;
private ClassWithSecretMethod _secret;
public ProcessWhichCallsTheSecretMethod(DIContainer di, ClassWithSecretMethod secret)
{
_di = di;
_secret = secret;
}
public void TheProcessMethod()
{
Database database = _di.Factories.DatabaseFactory.getDatabase();
_secret.SecretMethod(database);
}
}
Посмотрите, как нигде в процессе вы создаете базу данных определенного типа. Мало того, вы вообще ничего не создаете. Вы вызываете GetDatabase
метод для DatabaseFactory
объекта, хранящегося в контейнере внедрения зависимостей ( _di
переменная), метод, который вернет вам правильный экземпляр Database
интерфейса на основе вашей конфигурации.
Если после 3 недель использования PostgreSQL вы захотите вернуться к MySQL, вы откроете один файл конфигурации и измените значение DatabaseDriver
поля с DatabaseEnum.PostgreSQL
на DatabaseEnum.MySQL
. И вы сделали. Внезапно остальная часть вашего приложения снова правильно использует MySQL, изменив одну строку.
Если вы все еще не удивлены, я рекомендую вам немного погрузиться в IoC. Как вы можете принимать определенные решения не из конфига, а из пользовательского ввода. Этот подход называется шаблоном стратегии и, хотя он может использоваться и используется в корпоративных приложениях, он гораздо чаще используется при разработке компьютерных игр.
DbQuery
объект, например. Предполагая, что этот объект содержит элемент для строки SQL-запроса, который может быть выполнен, как можно сделать это универсальным?_secret.SecretMethod(database);
как можно согласовать всю эту работу с тем фактом, что теперь мнеSecretMethod
еще нужно знать, с какой БД я работаю, чтобы использовать правильный диалект SQL ? Вы очень усердно работали, чтобы сохранить большую часть кода в неведении об этом факте, но затем, на 11-м часу, вы снова должны знать. Я сейчас в такой ситуации и пытаюсь выяснить, как другие решили эту проблему.DbQuery
класса, предоставить реализации указанного интерфейса и использовать его вместо этого, имея фабрику для созданияIDbQuery
экземпляра. Я не думаю, что вам понадобится универсальный тип дляDatabaseResult
класса, вы всегда можете ожидать, что результаты из базы данных будут отформатированы аналогичным образом. Дело в том, что при работе с базами данных и необработанным SQL вы уже находитесь на таком низком уровне в своем приложении (за DAL и репозиториями), что в этом нет необходимости ...