Организация кода для работы с ftp средствами Fluent interface

от автора

Мне очень нравится паттерн 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 имеет минусы — проблема отладки. В длинных цепочках вызовов трудно поставить точку остановки.

ссылка на оригинал статьи https://habr.com/ru/post/567638/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *