Объясните общие понимания работы программы с источниками данных
Пишу систему, у которой в начале входа нужно выбирать БД(и настроить ее) или файловое хранение(и тоже настроить по типу места базирования).
По выбранному источнику хранения программа должны работать от начала и до конца своей работы.
Исходя из этого возникли вопросы как заложить архитектуру использования программой - источника данных.
То есть:
- Как программа должна хранить состояние выбранного источника данных, чтобы можно им пользоваться из любой части программы.
- Как взаимодействовать с выбранным источником.
- Как взаимодействовать с выбранным источником если нужны разные действия, запросы, выборки, запись и т.д.
- Какие использовать паттерны для организации работы программы с источником данных
- и то что еще может быть при работе с источниками данных
Разъясните пожалуйста принципы работы программы при выборе разных источников данных или поделитесь статейками, темами, где можно про это прочитать.
Ответы (2 шт):
Центр документации по данным .NET
Центр документации Entity Framework
Работа с данными в Visual Studio
Паттерн Репозиторий - METANIT.COM
Ща как расскажу!
Для начала ответы на вопросы.
- Не должна. Состояние источника данных (ИД) можно получить в любой момент от него самого, и дублировать его в приложении не нужно.
- Для этого внутри приложения должен быть драйвер ИД - объект, через который происходит всё взаимодействие. В случае использования Entity Framework - это Context.
- Через драйвер. Драйвер может быть даже поверх Entity Framework. В драйвере нужно с удобным интерфейсом реализовать действия с данными, востребованные в приложении.
- Паттерн - это некий готовый алгоритм, "стандартное решение". Не всегда нужно реализовывать такое решение в полном объёме, от ненужных частей и функций стоит отказаться. Подходящий паттерн упомянул @AlexanderPetrov в своём комментарии.
- Абзац после пятого вопроса. Драйвер должен позволять использовать различные источники данных. Поэтому доступ к данным нужно сделать двухступенчатым - под "пользовательским" драйвером должны быть драйверы для каждого типа ИД.
Советую разрабатывать приложение, начиная с самых простых функций работы с ИД, и постепенно добавлять необходимые более сложные функции. Так будет выстраиваться интерфейс драйвера.
P. S. Про файловай источники данных. Начните с простого, например, с формата CSV. Это будет аналог таблицы в БД. Здесь будет очень легко создать драйвер, подходящий для обоих типов ИД. Не делайте большие объёмы, чтобы можно было начать с (временного) хранения массива данных в памяти.
Потом можно перейти к фомату XML (или JSON), который позволяет хранить иерархические структуры данных, что будет аналогом нескольких связанных таблиц (реляционной) БД.
В общем, это роман с продолжением, которое я писать не очень хочу. Евгений ниже правильно прокомментировал, в основном. Читайте по первым двум ссылкам, там найдёте много полезного.
Смотрите, основная суть тут в определении общих методов и свойств этих двух источников данных. К примеру, база и файл, нам надо "добавить" туда данные (Add
) и "получить" их (Get
), также наверно будут "изменить", "сохранить", "загрузить", и так далее, это уже зависит от вашего проекта. Разобравшись с тем, что связывает их, надо теперь создать "контракт", некий интерфейс, который будет требовать от класса реализацию всех этих методов и свойств. В итоге, у нас будет что-то такое:
public interface IDataRepository
{
void Add(string value);
string Get(string key);
}
Ну а дальше просто реализуйте под каждый источник данных свой класс, ну к примеру, пусть будет что-то такое:
public class DBRepository(string login, string password) : IDataRepository
{
private readonly Dictionary<string, string> _data = new();
public void Add(string value)
{
_data.Add(value, value);
}
public string Get(string key)
{
return _data.TryGetValue(key, out var value) ? value : string.Empty;
}
}
public class FileRepository(string filePath) : IDataRepository
{
public void Add(string value)
{
File.AppendAllText(filePath, $"{value}\n");
}
public string Get(string key)
{
return File.ReadAllText(filePath).Contains(key) ? key : string.Empty;
}
}
На реализацию не смотрите, я тут написал ее так, от балды, но суть думаю уловили, каждый класс имеет свою конкретную реализацию под конкретный источник данных.
Теперь, имея такое разделение, вы можете создать свойство/поле с типом данного интерфейса и в реальном времени задавать нужное значение. Например:
public IDataRepository? DataRepository { get; set; }
public void ChangeRepository(string type)
{
DataRepository = type switch
{
"DB" => new DBRepository("login", "password"),
"File" => new FileRepository("data.txt"),
_ => throw new Exception("Invalid repository type")
};
}
Собственно вот основной подход.
Теперь что касается остальных ваших вопросов:
Как программа должна хранить состояние выбранного источника данных.
Зависит очень сильно от того, как вы проектируете проект изначально. К примеру:У вас может быть свой UI под каждый источник данных, а это значит, что под каждый UI стоит сделать свою ViewModel в которой и хранить конкретный объект.
Или у вас могут задаваться эти данные за пределами приложения (например, в JSON), тогда уже вам нужна либо "фабрика" (если вы не можете хранить тип конкретного класса), или прям в файле устанавливаете тип и десериализуете его сразу в нужный репозиторий (
{ "$type" : "FileRepository", "Path": "file.txt" }
. Далее, получив конкретный тип, вы можете сделать его обертку как Singleton и обращаться к ней где вам надо. Аналогично вы можете сделать через IoC контейнеры (вроде вы их знаете), где вам просто достаточно зарегистрировать этот тип после получения.
Ну и так далее, вариантов тут действительно много. В вашем случае мне кажется лучше всего подойдет создание некого класса "обертки", который будет в себе хранить сам
IDataRepository
, а также иметь метод его изменения, что-то вроде этого:public class DataRepositoryService { public IDataRepository? Data { get; private set; } public void SetRepository(IDataRepository dataRepository) { Data = dataRepository; } }
Опять, пишу очень упрощенно, чтоб вы понимали логику. И вот у вас уже класс, который руководит всем этим, вам достаточно его один раз создать и дальше передавать куда хотите, ну или сделать его синглтоном, добавив к примеру что-то такое:
private static readonly Lazy<DataRepositoryService> _instance = new(() => new DataRepositoryService()); public static DataRepositoryService Instance => _instance.Value; private DataRepositoryService() { }
Взаимодействие тогда будет таким:
DataRepositoryService.Instance.SetRepository(...);
, а если этот класс будет в контейнере, то вам вовсе достаточно будет попросить в любом месте экземпляр данного класса. Собственно вот вам простое хранение.Как взаимодействовать с выбранным источником.
Как и с любым другим классом, получите объект, вызовите нужные свойства/методы, получите нужный результат. Самое главное, через интерфейс. Ну а если у вашего класса есть дополнительная логика (чего не желательно), то приведите к конкретному типу(FileRepisitory)Data
;.Остальные вопросы вроде и так затронул в ответе.
Тут у вас может возникнуть еще вопрос про UI, как его правильно создавать... Ох, а тут все довольно спорно, ибо у каждого свой подход и предпочтения, но давайте сделаем что-то простое.
Делаем интерфейс, который обобщит наши ViewModel (вы вроде знаете про них, если нет, пишите, объясню). В нем требуем событие, которое оповестит главную VM об изменении источника данных:
public interface IDataViewModelBase { event Action<IDataRepository>? Created; }
Реализуем под каждого источника свою VM, в которой прописываем все нужные данные для получения:
public partial class FileDataViewModel : IDataViewModelBase { public event Action<IDataRepository>? Created; public string FilePath { get; set; } = string.Empty; [RelayCommand] private void Create() { Created?.Invoke(new FileRepository(FilePath)); } } public partial class DBDataViewModel : IDataViewModelBase { public event Action<IDataRepository>? Created; public string Login { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; [RelayCommand] private void Create() { Created?.Invoke(new DBRepository(Login, Password)); } }
На
[RelayCommand]
внимание не обращайте, это генерацияICommand
от библиотекиCommunityToolkit.Mvvm
. Вы у себя сделайте свои команды (вроде вы проReactive
интересовались, вот можно вроде через него сделать). Но не суть... В коде как видите простые свойства и метод, который будет вызван по нажатию кнопки.Далее пишем главную VM, в ней будем хранить список с названием всех репозиториев (обычно он получается динамически, в моем примере это просто
string
), свойство текущего выбранного типа, ну и метод, который будет вызван при изменении свойства, и метод, который уже будет вызываться по событию конкретной VM.public partial class MainViewModel : ObservableObject { public string[] Repositories => [ "File", "DB" ]; [ObservableProperty] private IDataViewModelBase? _selectedViewModel; private string _selectedRepository = string.Empty; public string SelectedRepository { get => _selectedRepository; set { _selectedRepository = value; OnSelectionChanged(value); } } private void OnSelectionChanged(string value) { if (SelectedViewModel is not null) { SelectedViewModel.Created -= SelectedViewModelCreated; } SelectedViewModel = value switch { "File" => new FileDataViewModel(), "DB" => new DBDataViewModel(), _ => throw new NotImplementedException() }; SelectedViewModel.Created += SelectedViewModelCreated; } private void SelectedViewModelCreated(IDataRepository repository) { DataRepositoryService.Instance.SetRepository(repository); } }
Делаем UI. Подход тут как у страниц, где в ресурсах мы сопоставляем при помощи
DataTemplate
тип с нужным видом (обычно этот вид разносят по отдельнымUserControl
), а в самом XAML окна/страницы привязываемContentPresenter
к нужному свойству.<Window.Resources> <DataTemplate DataType="{x:Type local:DBDataViewModel}"> <StackPanel > <Grid Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" Text="Логин: " FontWeight="Medium" VerticalAlignment="Center"/> <TextBlock Grid.Row="1" Grid.Column="0" Text="Пароль: " FontWeight="Medium" VerticalAlignment="Center"/> <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Login}" /> <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Password}" /> </Grid> <Button Margin="0 5" HorizontalAlignment="Stretch" Content="Создать" Style="{DynamicResource DefaultAccentButtonStyle}" Command="{Binding CreateCommand}"/> </StackPanel> </DataTemplate> <DataTemplate DataType="{x:Type local:FileDataViewModel}"> <StackPanel > <Grid Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Grid.Column="0" Text="Путь: " FontWeight="Medium" VerticalAlignment="Center"/> <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding FilePath}" /> </Grid> <Button Margin="0 5" HorizontalAlignment="Stretch" Content="Создать" Style="{DynamicResource DefaultAccentButtonStyle}" Command="{Binding CreateCommand}"/> </StackPanel> </DataTemplate> </Window.Resources> <StackPanel Margin="5"> <ComboBox ItemsSource="{Binding Repositories}" SelectedItem="{Binding SelectedRepository}"/> <ContentPresenter Content="{Binding SelectedViewModel}"/> </StackPanel>
В итоге результат наш будет таким:
Как видите, простое изменение
ComboBox
меняет и вид, ну а там уже при заполнении данных и нажатию на кнопку, будет вызвано событие, а оно в свою очередь вызоветSelectedViewModelCreated
метод в котором я прописал изменение репозитория в сервисе (синглтон), ну а у вас своя реализация.
В общем, я вроде показал все, что вам потребуется, дальше уже разбирайтесь и подстраивайте под свои нужды. Главное, не лепите все в одну кучу, как говориться, разделяйте и властвуйте)
Удачи в изучении C#!