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