Відповідь Калеба, поки він на правильному шляху, насправді помилкова. Його Fooклас виступає як фасадом бази даних, так і фабрикою. Це два обов'язки, і їх не слід включати в один клас.
Це питання, особливо в контексті бази даних, задавали занадто багато разів. Тут я спробую досконало показати вам перевагу використання абстракції (використання інтерфейсів), щоб зробити вашу програму менш сполученою та більш універсальною.
Перш ніж читати далі, я рекомендую вам прочитати та отримати базове розуміння ін'єкції Dependency , якщо ви цього ще не знаєте. Ви також можете перевірити шаблон дизайну адаптера , який в основному означає те, що приховує деталі реалізації за загальнодоступними методами інтерфейсу.
Введення залежності в поєднанні з заводською схемою дизайну є основним каменем і простим способом кодування схеми проектування стратегії , яка є частиною принципу IoC .
Не дзвоніть нам, ми зателефонуємо вам . (AKA голлівудський принцип ).
Роз'єднання програми за допомогою абстракції
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, методу, який викликає a 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. Ви зробите a 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, як можна зробити це загальним?