Мне очень нравится паттерн Fluent interface, за то, что он делает сложный и длинный код максимально читабельным. В статье хочу показать пример реализации этого паттерна при работе с ftp. Задача, что требуется сделать:
Получать имена файлов в определенном каталоге;
Скачивать файлы в поток/файл;
Загружать файлы из потока/файла;
Удалять файлы;
Настройки данных авторизации(ip, port, login, name).
Необходимо получить код, который будет лаконичным, читабельным и при помощи IntelliSense обеспечить легкое и удобное потребление кода. Пример:
_ftpService .OnConfigurate(pathSource) .Download(file) .ToFile(localFile);
и/или
_ftpService .OnConfigurate(pathSource) .Download(file) .ToSteam(memStream);
и/или
_ftpService .OnConfigurate(pathDestination) .Upload(fileNameDestination) .FromStream(memStream);
Для начала определяем интерфейсы по принципам SRP(единственной ответственности):
/// <summary> /// Интерфейс настройки фтп сервиса /// </summary> public interface ITransferFileService { string Url { get; } ITransferServiceAction OnConfigurate(string path); } /// <summary> /// Интерфейс записи данных с фтп /// </summary> public interface ITransferServiceWrite { void FromFile(string filePath); void FromStream(Stream stream); } /// <summary> /// Интерфейс чтения данных с фтп /// </summary> public interface ITransferServiceRead { void ToFile(string filePath); void ToStream(Stream stream); } /// <summaty> /// Интерфейс доступных действий с фтп /// </summary> public interface ITransferServiceAction { ITransferServiceRead Download(string fileName); ITransferServiceWrite Upload(string fileName); void Delete(string fileName); IEnumerable<string> GetNameFiles(); }
Теперь добавим класс с реализацией описанных выше интерфейсов.
public class FtpService : ITransferFileService, ITransferServiceAction, ITransferServiceRead, ITransferServiceWrite { private readonly Logger _logger; private string _fileName; public FtpService(string url, ILogger logger) { _logger = logger; Url = url; } public string Url { get; } /// <summary> /// Порт(по умолчанию 21) /// </summary> public int Port { get; private set; } = 21; /// <summary> /// Пароль для подключения к фтп /// </summary> private string Password { get; set; } /// <summary> /// Логин для подключения в фтп /// </summary> private string Login { get; set;} /// <summary> /// Путь /// </summary> private string Path { get; set; } public void SetCredential(string login, string password) { Login = login; Password = password; } public ITransferServiceAction OnConfigurate(string path) { Path = path; return this; } public ITransferServiceRead Download(string fileName) { _fileName = fileName; return this; } public ITransferServiceWrite Upload(string fileName) { _fileName = fileName; return this; } public void Delete(string fileName) { try { var request = (FtpWebRequest) WebRequest.Create($"{Url}/{Path}/{_fileName}"); request.Credentials = new NetworkCredential(Login,Password); request.Method = WebRequestMethods.Ftp.DeleteFile; request.GetResponse(); } catch (Exception ex) { _logger.Error($"Ошибка удаления файла с ftp сервера - {ex.Message} "); } } public void FromFile(string filePath) { if(string.IsNullOrEmpty(filePath)) return; try { using (var client = new WebClient()) { client.Credentials = new NetworkCredential(Login, Password); client.UploadFile($"{Url}/{Path}/{_fileName}", WebRequestMethods.Ftp.UploadFile, filePath); } } catch (Exception ex) { _logger.Error(ex); } } public void FromStream(Stream stream) { if(stream == null) return; try { var request = (FtpWebRequest)WebRequest.Create($"{Url}/{Path}/{_fileName}"); request.Credentials = new NetworkCredential(Login, Password); request.UsePassive = true; request.UsePassive = true; request.KeepAlive = true; request.Method = WebRequestMethods.Ftp.UploadFile; using (var ftpStream = request.GetRequestStream()) { stream.CopyTo(ftpStream); } request.GetResponse(); } catch (Exception ex) { _logger.Error(ex); } } private byte[] DownloadFile() { var ftpRequest = (FtpWebRequest)WebRequest.Create($"{Url}/{Path}/{_fileName}"); ftpRequest.Credentials = new NetworkCredential(Login, Password); ftpRequest.UseBinary = true; ftpRequest.UsePassive = true; ftpRequest.KeepAlive = true; ftpRequest.Method = WebRequestMethods.Ftp.DownloadFile; var ftpResponse = (FtpWebResponse)ftpRequest.GetResponse(); using (var ms = new MemoryStream()) { ftpResponse.GetResponseStream().CopyTo(ms); return ms.ToArray(); } } public void ToFile(string filePath) { try { var downloadedFile = DownloadFile(); File.WriteAllBytes(filePath, downloadedFile); } catch (WebException ex) { _logger.Error(Url); _logger.Error(ex); } } public void ToStream(Stream stream) { if (stream == null) return; try { using (var writer = new BinaryWriter(stream)) { var downloadedFile = DownloadFile(); writer.Write(downloadedFile); } } catch (WebException ex) { _logger.Error(Url); _logger.Error(ex); } } public IEnumerable<string> GetNameFiles() { var request = (FtpWebRequest)WebRequest.Create($"{Url}/{Path}"); request.Method = WebRequestMethods.Ftp.ListDirectory; request.Credentials = new NetworkCredential(Login, Password); var files = new List<string>(); using (var response = (FtpWebResponse)request.GetResponse()) { var responseStream = response.GetResponseStream(); using (var reader = new StreamReader(responseStream)) { var line = reader.ReadLine(); while (!string.IsNullOrEmpty(line)) { try { files.Add(line); line = reader.ReadLine(); } catch (Exception ex) { _logger.Error(ex); } } } } return files; } public void SetPort(int port) { Port = port; } }
Что можно сделать лучше - добавить асинхронный вариант цепочки. Ничего сложно в этом нет, достаточно добавить в интерфейсы методы с возвращаемым типом Task<T>. Для гибкой настройки сервиса добавим паттерн строитель:
/// <summary> /// Построитель Ftp сервиса /// </summary> public class BuilderFtpService { private FtpService ftpService { get; } /// <summary> /// Конструктор с ip адресом /// </summary> /// <param name="url">Адрес фтп</param> /// <param name="logger">Логгер</param> public BuilderFtpService(string url, ILogger logger) { ftpService = new FtpService(url,logger); } /// <summary> /// Построить экземпляр сервиса /// </summary> public ITransferFileService Build() => ftpService; /// <summary> /// Указать авторизационные данные /// </summary> /// <param name="login">Логин</param> /// <param name="password">Пароль</param> /// <returns>Построитель фтп сервиса</returns> public BuilderFtpService WithCredential(string login, string password) { ftpService.SetCredential(login, password); return this; } /// <summary> /// Указать авторизационные данные /// </summary> /// <param name="login">Логин</param> /// <param name="password">Пароль</param> /// <returns>Построитель фтп сервиса</returns> public BuilderFtpService WithPort(int port) { ftpService.SetPort(port); return this; } }
Таким образом у нас получилось реализовать функционал согласно поставленной задачи. Пример настройки и использования:
new BuilderFtpService(ipAddress, logger) .WithCredential(login, password) .Build() .OnConfigurate(pathSource) .Download(file) .ToFile(localFile);
Код выше приведен в качестве примера, в реальном приложении рекомендуется все зависимости реализовывать через IoC контейнеры.
Такая реализация функционала имеет лаконичный вид, высокую читабельность и повышает интуитивность использования кода. Как и в любом подходе, паттерн fluent interface имеет минусы - проблема отладки. В длинных цепочках вызовов трудно поставить точку остановки.
