Во-первых, это мой первый пост на сайте обмена стеками. Так что проявите ко мне терпение. Если что-то не так или вы что-то упускаете, сообщите мне. Я добавлю как можно скорее.
В настоящее время я работаю с принципами SOLID и пытаюсь реализовать их на практике. Я видел на YouTube практические видео Тима Кори, которые хорошо объясняют принципы. Теперь у меня есть проект, в котором я хотел бы это реализовать, и я немного борюсь с реализацией принципа инверсии зависимостей в контексте SQL-запросов к SQL-серверу. Здесь часто требуется несколько экземпляров разных классов, например SqlConnection
, SqlDataAdapter
или же DataSet
. Возможно заранее. я знаю это Внедрение зависимости может упростить задачу. Но сначала я хотел понять принцип инверсия зависимости.
Я создал (мини) проект, в котором мой подход становится понятным (проект с полным кодом также доступен на Github https://github.com/dnsnx/Dependency_Inversion_SQL).
Я сначала создал интерфейс IDatabase
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dependency_Inversion_SQL.Interfaces
{
public interface IDatabase
{
void AddParameters((string name, object value)[] parameters);
System.Data.DataSet GetData(string commandText, System.Data.CommandType commandType = System.Data.CommandType.Text, string sourceTable = null);
}
}
Для простоты я просто хочу прочитать данные на данный момент — обновлений пока нет. Итак, я определил метод настройки параметров (AddParameters
) и один метод чтения данных (GetData
). Первая трудность здесь: держите интерфейс неспецифичным, то есть не предназначенным непосредственно для SQL Server. В будущем система БД может быть изменена! Затем я создал класс SQLDatabase
который реализует этот интерфейс:
using Dependency_Inversion_SQL.Interfaces;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
namespace Dependency_Inversion_SQL.Classes
{
public class SQLDatabase : IDatabase
{
#region Variables
private readonly string _SqlServer = "Integrated Security=true;Initial Catalog=Test;Server=localhost\localhost;";
private SqlConnection _SqlConnection;
private SqlCommand _SqlCommand;
private SqlDataAdapter _SqlDataAdapter;
private DataSet _DataSet;
#endregion
#region Properties
public string SqlServer
{
get
{
return _SqlServer;
}
}
#endregion
#region Constructors
public SQLDatabase(SqlConnection sqlConnection, SqlDataAdapter sqlDataAdapter, DataSet dataSet)
{
_SqlConnection = sqlConnection;
_SqlConnection.ConnectionString = _SqlServer;
_SqlCommand = _SqlConnection.CreateCommand();
_SqlDataAdapter = sqlDataAdapter;
_DataSet = dataSet;
}
#endregion
#region Methods
public void AddParameters((string name, object value)[] parameters)
{
for (var i = 0; i < parameters.Length; i++)
{
_SqlCommand.Parameters.AddWithValue(parameters[i].name, parameters[i].value);
}
}
public System.Data.DataSet GetData(string commandText, System.Data.CommandType commandType = System.Data.CommandType.Text, string sourceTable = null)
{
if (sourceTable == null)
{
sourceTable = "someName";
}
_DataSet.Clear();
_SqlCommand.CommandText = commandText;
_SqlCommand.CommandType = commandType;
_SqlDataAdapter.SelectCommand = _SqlCommand;
_SqlDataAdapter.SelectCommand.Connection.Open();
_SqlDataAdapter.Fill(_DataSet, sourceTable);
_SqlDataAdapter.SelectCommand.Connection.Close();
_SqlCommand.Parameters.Clear();
_SqlCommand.CommandText = string.Empty;
return _DataSet;
}
#endregion
}
}
Теперь, чтобы реализовать инверсию зависимостей, нам понадобится factory
класс, определяющий, какие экземпляры создаются:
Factory
:
using Dependency_Inversion_SQL.Classes;
using Dependency_Inversion_SQL.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dependency_Inversion_SQL
{
public static class Factory
{
public static ICar CreateCar()
{
return new Car(CreateDatabase());
}
public static IDatabase CreateDatabase()
{
return new SQLDatabase(CreateDatabaseConnection(), CreateDatabaseAdapater(), CreateDatabaseDataSet());
}
public static System.Data.SqlClient.SqlConnection CreateDatabaseConnection()
{
return new System.Data.SqlClient.SqlConnection();
}
public static System.Data.SqlClient.SqlDataAdapter CreateDatabaseAdapater()
{
return new System.Data.SqlClient.SqlDataAdapter();
}
public static System.Data.DataSet CreateDatabaseDataSet()
{
return new System.Data.DataSet();
}
}
}
Затем мне понадобился класс, который запрашивает данные SQL. Я создал простой класс Car
, который «читает» название машины.
using Dependency_Inversion_SQL.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dependency_Inversion_SQL.Classes
{
class Car : ICar
{
IDatabase _Database;
public Car(IDatabase database)
{
_Database = database;
}
public string GetCarName()
{
var result = _Database.GetData("SELECT 'Audi R8' as CarName");
return result.Tables[0].Rows[0].ItemArray[0].ToString();
}
}
}
Теперь мое консольное приложение:
using Dependency_Inversion_SQL.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dependency_Inversion_SQL
{
class Program
{
static void Main(string[] args)
{
ICar car = Factory.CreateCar();
Console.WriteLine($"My dream car is an { car.GetCarName() }");
Console.ReadLine();
}
}
}
Как я уже писал выше, мне было интересно, есть ли более простой способ реализовать SQLDatabase
учебный класс. Ему нужно все, что есть в конструкторе (SqlConnection
, SqlDataAdapter
, DataSet
) и так далее. Чего я тоже не знаю, правильно ли я реализовал генерацию этих классов в Factory
учебный класс.
public static System.Data.SqlClient.SqlConnection CreateDatabaseConnection()
{
return new System.Data.SqlClient.SqlConnection();
}
public static System.Data.SqlClient.SqlDataAdapter CreateDatabaseAdapater()
{
return new System.Data.SqlClient.SqlDataAdapter();
}
public static System.Data.DataSet CreateDatabaseDataSet()
{
return new System.Data.DataSet();
}
Я понял, что возвращаемый тип должен быть интерфейсом как нельзя лучше. На мой взгляд, это невозможно с классами SQL.
Если этот вопрос слишком велик или я неправильно понял сайт обмена стеками, дайте мне знать. Тогда я удалю вопрос. Большое спасибо за любой комментарий по этому поводу.
2 ответа
Я думаю, у вас есть разумное понимание технических особенностей шаблонов проектирования, но вы изо всех сил пытаетесь использовать их разумно. Итак, этот ответ фокусируется на намерении, а не на конкретном синтаксисе.
Я просто хочу уделить здесь немного времени, чтобы указать на то, что вы можете сделать неверный вывод о моем отзыве. То, что я называю ваши реализации плохими, не означает, что я называю вас плохим разработчиком. У вас есть четко вложил в это много мыслей и усилий, и я определенно не обвиняю вас в том, что вы не проявили должную осмотрительность или сомневаетесь в вашей способности писать код.
Вы умеете класть кирпичи, но не работаете по правильной схеме. Я часто использую пример Форрест Гамп играет в американский футбол. Он прекрасно умеет ставить одну ногу впереди другой, но он не понимает, куда он должен и не должен идти и когда остановиться.
Вы сделали то же самое. Ваш код хорош, если вы посмотрите на него построчно, но он упускает из виду общую картину того, чего он пытается достичь.
Заводской узор
Фабрика — это место, где создаются вещи. Вы справились с этой частью.
Однако зачем нам использовать фабрику, а не просто конструктор? Потому что мы можем просто позвонить new Foo()
без необходимости создавать FooFactory.CreateFoo
.
У фабричного шаблона есть три основных назначения:
- Если конструкция объекта сложнее, чем нужно заботиться потребителю, фабрика выступает в качестве удобного для потребителя упрощения.
- Если потребитель не может решить, какой конкретный класс следует использовать, а вместо него это решает фабрика. Это то, что я называю «умной» фабрикой, она делает выбор за потребителя.
- Специфично для DI, когда классу необходимо иметь возможность многократно создавать новые экземпляры внедренной зависимости вместо того, чтобы иметь один экземпляр, внедренный в его конструктор.
Ваш код, который является фабричной оберткой даже не вокруг вашего собственного кода (они System.Data
classes), на самом деле ничего значимого не добавляет. Он ничего не делает, кроме вызова пустого конструктора. Это не соответствует целям фабрики, поэтому фабрика не нужна.
Несколько быстрых примеров того, как фабрики могут быть полезны:
Цель 1
public class CarFactory
{
public ICar CreateCar()
{
return new Car()
{
Engine = new PetrolEngine(),
Wheels = new[] { new Wheel(), new Wheel(), new Wheel(), new Wheel() }
}
}
}
В этом примере вы не хотите, чтобы ваш покупатель знал, как собрать автомобиль, поэтому вы поручите фабрике сделать это за него.
Цель 2
public class CarFactory
{
public ICar CreateAppropriateCar(CarGoal goal)
{
switch(goal)
{
case CarGoal.Racing:
case CarGoal.ShowingOff:
return new SportsCar();
case CarGoal.Transport:
case CarGoal.LivingIn:
return new Van();
case CarGoal.Offroading:
return new Jeep();
case CarGoal.Tourism:
return new TourBus();
default:
return new Sedan();
}
}
}
Вы не хотите, чтобы потребителю приходилось решать, какой автомобиль ему больше всего подходит, он знает только то, что он собирается делать с автомобилем (CarGoal
). Завод принимает правильное решение, исходя из заявленной цели.
Цель 3 нелегко продемонстрировать.
Я понял, что возвращаемый тип должен быть интерфейсом как нельзя лучше.
Для цели 2 это существенный что ваш возвращаемый тип не является конкретным типом, иначе он не справился бы с целью.
Для целей 1 и 3 в этом нет необходимости, но все же полезно иметь в виду чистое кодирование. Не столько для валидности фабричного шаблона как такового, а скорее потому, что он подразумевает, что вы правильно используете свои интерфейсы в своей кодовой базе.
На мой взгляд, это невозможно с классами SQL.
Поскольку они не реализуют интерфейс, это не так.
Бывают случаи, когда вы оборачиваете библиотечный класс в один из своих, и вы можете реализовать интерфейс для этого настраиваемого класса, но не начинайте делать это вслепую, поскольку это приводит к кучке неиспользуемого чрезмерно замысловатого шаблонного кода.
Когда все, что у вас есть, это молоток, все выглядит как гвоздь.
public static ICar CreateCar()
{
return new Car(CreateDatabase());
}
Вы переборщили с использованием шаблона factory до такой степени, что пытаетесь обернуть все в factory. Это относится к моему начальному абзацу в этом ответе: вы знаете как реализовать фабрику, но вы не знаете когда и почему реализовать фабрику.
Более серьезная проблема здесь в том, что создание экземпляра автомобиля не должно запускать создание экземпляра соединения с базой данных (или объекта, который в конечном итоге установит соединение). Создание объекта в памяти не должно быть связано с подключением к базе данных. Это побеждает цель делать что-то в памяти. Концептуально это очень отдельные шаги.
Is-a против has-a
Цель состоит в том, чтобы использовать SqlDatabase
в вашей кодовой базе, но без тесной связи с этим конкретным поставщиком данных. Это ясно.
Но похоже, что единственные попытки, которые вы предприняли для этого, были связаны с использованием интерфейсов.
Я не могу и не буду объяснять это здесь полностью, но вам нужно познакомиться с концепцией композиция выше наследования. Хотя наследование — это не совсем то же самое, что реализация интерфейса, в контексте сравнения композиции и наследования вы можете объединить интерфейсы с наследованием.
Реализация интерфейсов и наследование классов являются отношениями «есть». Car
является ан ICar
. SQLDatabase
является ан IDatabase
.
Но в композициях используется отношение «имеет». Возьмем, к примеру, меня, человека. У меня есть машина. Как бы вы отразили это в коде? Так?
public class Person : Car { }
Нет. Это не делается с помощью наследования или интерфейсов, потому что «A Person
это Car
» не является правильным.
public class Person
{
public Car Car { get; set; }
}
Это правильный подход, потому что «A Person
имеет а Car
«
Примечание: поскольку у автомобилей есть интерфейсы, действительно правильно использовать здесь интерфейс вместо конкретного типа, поэтому лучшая версия будет:
public class Person
{
public ICar Car { get; set; }
}
Но в случаях, когда интерфейса нет, использование конкретного типа все равно будет правильным использованием композиции вместо наследования.
Предлагаю вам заглянуть в шаблон репозитория чтобы понять, как использовать композицию для работы с вашим SqlDatabase
, в то же время скрывая его от глаз потребителя.
Хотя я обычно больше не использую репозитории, это действительно хорошее начало для новичка, чтобы понять основы разделения и инкапсуляции слоев. По мере накопления опыта вы узнаете о других возможных (и более продвинутых) подходах.
Инверсия зависимостей
Но сначала я хотел понять принцип инверсии зависимостей.
Я не могу вдаваться в подробности, но я воспользуюсь аналогией, чтобы помочь вам понять намерение. Предположим, у нас есть магазин сэндвичей:
public class SandwichShop
{
public Sandwich MakeSandwich()
{
var bread = new WhiteBread();
bread.Cut();
var toppings = new List<Topping>() { new Bacon(), new Lettuce, new Tomato() };
bread.Add(toppings);
bread.Wrap();
return bread;
}
}
Покупатель зависит от магазина сэндвичей, а магазин сэндвичей зависит от белого хлеба. Это означает, что покупатель косвенно зависит от белого хлеба. Точно так же покупатель может получить только те начинки, которые жестко запрограммированы в магазине сэндвичей.
Если мы предоставим клиенту контроль над тем, какой хлеб использовать, это будет инвертировать нормальный порядок зависимости..
Это тот же класс, но теперь он сделан с учетом инверсии зависимостей:
public class SandwichShop
{
public Sandwich MakeSandwich(Bread bread, List<Topping> toppings)
{
bread.Cut();
bread.Add(toppings);
bread.Wrap();
return bread;
}
}
Теперь клиент (т. Е. Актер, который звонит MakeSandwich
решает хлеб и начинки). Обратной стороной является то, что теперь ответственность за предоставление эти зависимости тоже.
В этом примере используются метод и параметры метода. В разработке программного обеспечения более распространенная реализация инверсии зависимостей связана с аргументом конструктора, но по тому же принципу: вызывающая сторона (то есть субъект, вызывающий конструктор) предоставляет зависимости создаваемого им объекта.
Просто чтобы устранить неоднозначность:
- Зависимость инъекция обычно относится к использованию фреймворка или контейнера для автоматического обеспечения правильной зависимости для любого объекта в зависимости от того, что он запрашивает в конструкторе.
- Инвертированные зависимости — это (что досадно) совершенно другое дело, чем инверсия зависимостей, и нечто значительно более продвинутое, о чем я не буду здесь вдаваться.
Несколько быстрых замечаний:
IDatabase
ИМХО плохая репутация, и вы можете видеть это с таким именем метода, какCreateDatabase
. Та же проблема относится и к другим вещам, напримерSqlServer
который на самом деле является строкой подключения (которую никогда не следует жестко программировать, она должна быть частью файла конфигурации). И так далее.Я не проверял ваш github, но подозреваю, что вы неправильно распоряжаетесь своими соединениями с БД.
GetCarName()
неправильно на очень многих уровнях. Правильный способ — получитьCar
объект из БД, а затем посмотрите на егоName
свойство, напримерvar carName = car.Name;
.
Но все это бессмысленная критика деталей. Мой совет: выбросьте весь этот код. Вы пишете свой собственный фреймворк БД, и он никогда не будет так хорош, как Entity Framework или Dapper. Вы зря теряете время; вам следует научиться правильно использовать Entity Framework или Dapper.
WRT SOLID и т. Д., Пожалуйста, поймите, что это рекомендации, а не незыблемые законы.
Внедрение зависимостей WRT и т. Д .: следуйте многочисленным руководствам о том, как комбинировать внедрение зависимостей и Entity Framework, которые доступны. Эти вещи были решены давно, и ни одна компания никогда не потребует от вас написания собственной реализации, они просто захотят, чтобы вы следовали их способу работы (который, вероятно, будет общим способом, описанным в многочисленных руководствах).
- 1
Спасибо за ваш отзыв. Я знаю, что соединение должно храниться в файле конфигурации, а также знаю, что
GetCarName
метод — плохой метод. Это было просто, чтобы продемонстрировать мою проблему. Я бы никогда не реализовал этот метод в производственном коде. Как я уже писал, я создал этот проект, чтобы продемонстрировать свою проблему. Но большое спасибо за подсказку для Entity Framework. Я уже реализовал это в другом проекте, но мне было интересно, могу ли я использоватьSqlConnection
занятия с DI тоже.— dns_nx
Большое спасибо за подробный отзыв. Я внимательно прочту ваш ответ, а затем дам вам отзыв.
— dns_nx