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


AliExpress RU&CIS

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




Комментарии (3):

  1. lair
    /#23259398

    (del, ошибся)

  2. navferty
    /#23260030

    Fluent-паттерн действительно очень красивый. Его удобно использовать для конфигурации: как в ASP.NET Core или паттерн test builder. Также очень удобно составлять запросы в LINQ, например используя IQueryable.

    Но нужно обратить внимание, что в том же LINQ запрос не выполнится, пока не будет вызван например ToListAsync - который и отдаст Task. В приведенном примере, если переделать метод BuilderFtpService.Download на асинхронную модель, нужно будет реализовывать дополнительно интерфейс вроде IDownloadableBuilderFtpService, который позволит сконфигурировать скачивание и дельнейшее сохранение - но позволит запустить эту задачу только при вызове последнего метода, который уже отдаст Task в запущенном состоянии.

    • MisterZ
      /#23260154

      Да, совершенно верно. Точнее добавить интерфейсы для асинхронных методов ITransferServiceReadAsync и ITransferServiceReadAsync, где будут возвращаться Task, вместо void, а в ITransferServiceAction, добавить асинхронные методы, которые возвращали бы "асинхронные интерфейсы".