Letöltés mappa rendező program – 3. rész
Az előző részben elkészítettük a konfiguráció betöltéséért és validációért felelős komponensünket, ideje befejeznünk a programot.
Előkészületek
A rendező komponens lesz a program „lelke”. Ez fogja a szabályok alapján a fájlokat áthelyezni a megfelelő mappákba. A feladatainak ellátásában épít a korábbi validációs és beolvasó komponensünkre, lényegében egy Façade tervezési minta-szerűséget valósít meg. Viszont a S.O.L.I.D elvek szerint, azon belül is az Interface Segregation szerint sosem jó ötlet, ha két osztály direktben függ egymástól.
Ha két osztály direktben függ, akkor csak együtt alkalmazhatóak, vagyis veszítünk a modularitásból. Ez még lehet nem is lenne akkora baj, de az sokkal nagyobb probléma, hogy a független tesztelési lehetőséget elveszítjük, mivel a direkt függés miatt csak együtt tudjuk tesztelni a két osztályt. A probléma értelemszerűen annál nagyobb, minél több osztály függésünk van.
Éppen ezért tanácsos az osztályok direkt függőségét megszüntetni úgy, hogy egy Interface mögé rejtjük a funkcionalitást.
Tételezzük fel, hogy X komponens függ Y komponenstől. Ebben az esetben ha kicseréljük Y komponenst egy IY komponensre, ami Y komponens X által használt részeit definiálja, akkor a direkt függőség megszűnt, mivel IY helyére tetszőleges implementáció helyettesíthető.
Éppen ezért a korábban bevezetett ConfigurationManager és ConfigurationValidator osztályokhoz be kell vezetnünk két felületet. Ezek lesznek a IConfigurationManager és IConfigurationValidator
using DloadOrganizer.Configuration;
namespace DloadOrganizer.Interfaces
{
internal interface IConfigurationManager
{
bool IsConfigExisting { get; }
Config ReadConfigurationFile();
void WriteExampleConfig();
}
}
using DloadOrganizer.Configuration;
using System.Collections.Generic;
namespace DloadOrganizer.Interfaces
{
internal interface IConfigurationValidator
{
IEnumerable<string> Errors { get; }
bool IsValid(Config config);
}
}
A két felület lényegében a két osztály publikus metódusait tartalmazza. Az interface definíciókat érdemes egyébként egy külön névtérbe tenni, illetve nagyobb programok esetén, amik több projektből állnak, több okból is érdemes őket szervezni. Ezekről majd egy másik alkalommal lesz szó.
A helyettesíthetőség kedvéért természetesen a meglévő osztályoknak implementálniuk kell az újonnan bevezetett felületeket:
using DloadOrganizer.Configuration;
using DloadOrganizer.Interfaces;
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
namespace DloadOrganizer
{
internal sealed class ConfigurationManager: IConfigurationManager
{
public string AppDir { get; }
private const string ConfigFile = "DloadOrganizerConfig.json";
public ConfigurationManager()
{
AppDir = Path.GetDirectoryName(Process.GetCurrentProcess().StartInfo.FileName) ?? Environment.CurrentDirectory;
}
public bool IsConfigExisting
{
get => File.Exists(Path.Combine(AppDir, ConfigFile));
}
public Config ReadConfigurationFile()
{
using (StreamReader file = File.OpenText(Path.Combine(AppDir, ConfigFile)))
{
string json = file.ReadToEnd();
return JsonSerializer.Deserialize<Config>(json);
}
}
public void WriteExampleConfig()
{
Config cfg = new Config
{
SourceDirectory = "Path to downloads...",
Rules = new Rule[]
{
new Rule
{
Patterns = new string[] { "*.pdf", "*.docx" },
TargetDirectory = "Docomuents"
}
}
};
using (StreamWriter file = File.CreateText(Path.Combine(AppDir, ConfigFile)))
{
string json = JsonSerializer.Serialize(cfg, new JsonSerializerOptions
{
WriteIndented = true
});
file.Write(json);
}
}
}
}
using DloadOrganizer.Configuration;
using DloadOrganizer.Interfaces;
using DloadOrganizer.Properties;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace DloadOrganizer
{
internal sealed class ConfigurationValidator: IConfigurationValidator
{
private List<string> _errors;
public IEnumerable<string> Errors => _errors;
public ConfigurationValidator()
{
_errors = new List<string>();
}
public bool IsValid(Config config)
{
_errors.Clear();
if (string.IsNullOrEmpty(config.SourceDirectory))
{
_errors.Add(Resources.ValidationNoSourceDir);
}
else if (!Directory.Exists(config.SourceDirectory))
{
_errors.Add(Resources.ValidationSourcetDirNotExist);
}
if (config.Rules.Length < 1)
{
_errors.Add(Resources.ValidationNoRules);
}
else if (config.Rules.Any(r => string.IsNullOrEmpty(r.TargetDirectory)))
{
_errors.Add(Resources.ValidationRuleNoTarget);
}
else if (config.Rules.Any(r => Directory.Exists(r.TargetDirectory)))
{
_errors.Add(Resources.ValidationRuleTargetNotExist);
}
else if (config.Rules.Any(r => r.Patterns.Length == 0))
{
_errors.Add(Resources.ValidationRuleNoExtensions);
}
return _errors.Count == 0;
}
}
}
További komponensek
A program a működése során jó lenne, ha a konzolon információkat jelenítene meg, illetve majd valamit kezdenie kell a fájlrendszerrel is. Ezen funkcionalitásokat szintén egy-egy osztályban érdemes megvalósítani. Viszont ezek a rendező komponensnek „alárendelt” osztályai. Vagyis a bennük megvalósítandó funkciók nagymértékben függeni fognak attól, hogy mire lesz éppen szükségünk.
Éppen ezért ilyen esetben, ha nem tudom, hogy milyen funkcionalitást fogok egy komponensbe tenni, mert az egy másiktól függ, akkor minden esetben interface bevezetéssel kezdem a feladatot. Így született meg a IConsole és IFileSystem felület, amik üres felületként kezdték az életüket és a metódusaik, funkcionalitásuk az Organizer osztály fejlesztésekor véglegesedett.
Az Organizer osztály
using DloadOrganizer.Configuration;
using DloadOrganizer.Interfaces;
using DloadOrganizer.Properties;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace DloadOrganizer
{
internal class Organizer
{
private readonly IConsole _console;
private readonly IConfigurationManager _configurationManager;
private readonly IConfigurationValidator _validator;
private readonly IFileSystem _fileSystem;
private Config? _configuration;
public Organizer(IConsole console,
IConfigurationManager configurationManager,
IConfigurationValidator validator,
IFileSystem fileSystem)
{
_console = console;
_configurationManager = configurationManager;
_validator = validator;
_fileSystem = fileSystem;
}
public void LoadConfig()
{
if (_configurationManager.IsConfigExisting)
{
_configuration = _configurationManager.ReadConfigurationFile();
if (!_validator.IsValid(_configuration))
{
_console.Info(Resources.ConfigFileErrors);
_console.Info(string.Join("\r\n", _validator.Errors));
_console.PressKeyAndExit();
}
}
else
{
_configurationManager.WriteExampleConfig();
_console.Info(Resources.ConfigFileCreated);
_console.PressKeyAndExit();
}
}
public void Run()
{
if (_configuration == null)
throw new InvalidOperationException(Resources.ConfigNotLoaded);
string[] files = _fileSystem.GetFiles(_configuration.SourceDirectory);
foreach (var rule in _configuration.Rules)
{
IEnumerable<string> filesForRule = GetFilesThatMatchExtension(files, rule.Patterns);
CopyFiles(filesForRule, rule.TargetDirectory);
}
}
private void CopyFiles(IEnumerable<string> filesForRule, string targetDirectory)
{
foreach (var sourceFile in filesForRule)
{
string fileName = Path.GetFileName(sourceFile);
string target = Path.Combine(targetDirectory, fileName);
_fileSystem.Move(sourceFile, target);
}
}
private IEnumerable<string> GetFilesThatMatchExtension(string[] files, string[] extensions)
{
foreach (var file in files)
{
string extension = Path.GetExtension(file);
if (extensions.Contains(extension))
{
yield return file;
}
}
}
}
}
Az osztály konstruktora csak a függőségek referenciáinak tárolásáért felelős. A működés két részre van választva. Az osztály példányosítása után meg kell hívni a LoadConfig() metódust, ami betölti a konfigurációt, majd a Run() metódus hívása ténylegesen végrehajtja a rendezést.
A Run() hívásakor ellenőrizni kell, hogy a LoadConfig() meg lett-e hívva. Ezt a _configuration változón keresztül ellenőrizzük. Ha ennek az értéke null, akkor valószínűleg programozói hiba történt és egy InvalidOperationException kivétel formájában jellezzük, hogy valami félrement.
A LoadConfig() abban az esetben, ha nem helyes konfigurációt talál, kiírja a hibákat és megszakítja a program végrehajtását. Amennyiben a konfigurációs fájl nem létezik, akkor létrehozunk egy minta konfigurációs fájlt és úgy szakítjuk meg a program végrehajtását.
A Run() metódus egyszer lekéri a forrás mappa összes fájlját, majd a szabályok szerint szétválogatja őket és a megfelelő célmappába másolja a fájlokat. A fájlok másolásához és azok elkéréséhez az IFileSystem felületre épít.
IFileSystem implementáció
Az IFileSystem felület szintén a direkt függőség elkerülése és a későbbi tesztelhetőség miatt került ki egy interface mögé. A megvalósítása:
using DloadOrganizer.Interfaces;
using System.IO;
namespace DloadOrganizer
{
internal class FileSystem : IFileSystem
{
public string[] GetFiles(string directory)
{
return Directory.GetFiles(directory);
}
public void Move(string file, string target)
{
string? targetDir = Path.GetDirectoryName(target);
if (targetDir != null
&& !Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
File.Move(file, target);
}
}
}
A megvalósításban semmi különleges nincs, a beépített fájl- és könyvtárkezelő rutinokat használja. A csavar csupán annyi, hogy a Move() metódus ellenőrzi, hogy a cél mappa létezik-e. Ha nem, akkor létrehozza a teljes útvonalat. Pontosabban megpróbálja létrehozni.
Itt esetlegesen lehetne javítani a programon extra hibakezelés beiktatásával. De mivel az alkalmazás lényege, hogy fájlokat mozgasson valahonnan valahova, ezért ha elfedjük a másolási, vagy úgy általánosságban az I/O hibákat, akkor az nem biztos, hogy jó lesz.
Az IConsole implementációja
using DloadOrganizer.Interfaces;
using DloadOrganizer.Properties;
using System;
namespace DloadOrganizer
{
internal class ProgramConsole : IConsole
{
public void Exception(Exception ex)
{
var currentColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Error: {0}", ex.Message);
Console.ForegroundColor = currentColor;
}
public void Info(string format, params object[] arguments)
{
Console.WriteLine(format, arguments);
}
public void PressKeyAndExit()
{
Console.WriteLine(Resources.PressKeyToExit);
Console.ReadKey();
Environment.Exit(0);
}
}
}
A konzol implementációja jelen esetben a beépített rendszer konzolt alkalmazza. Viszont azzal, hogy kiszerveztük egy interfész mögé, lehetőségünk lenne egy olyan „konzolt” implementálni, ami fájlba ír, vagy mondjuk VT100 terminál kódokkal kommunikál. Ez a rugalmasság lehetővé teszi, hogy a program betöltésekor valami logika alapján (Pl. parancssori argumentum feldolgozása) jelentősen befolyásoljuk a program viselkedését anélkül, hogy a lényegi, business logikához hozzá kellene nyúlnunk.
A főprogram
Ezen a ponton nincs más dolgunk, mint összelegózni a komponenseket. Ez lesz a Program osztály Main metódusának feladata.
using DloadOrganizer.Interfaces;
using System;
namespace DloadOrganizer
{
internal static class Program
{
public static void Main(string[] args)
{
IConsole console = new ProgramConsole();
IConfigurationManager configurationManager = new ConfigurationManager();
IConfigurationValidator validator = new ConfigurationValidator();
IFileSystem fileSystem = new FileSystem();
var organizer = new Organizer(console, configurationManager, validator, fileSystem);
try
{
organizer.LoadConfig();
organizer.Run();
}
catch (Exception ex)
{
console.Exception(ex);
console.PressKeyAndExit();
}
}
}
}
A Main metódusban, mint látható, nem történik sok minden, csupán a példányok létrehozása, valamint az Organizer két metódusának meghívása. Feltűnhet, hogy itt van egy általános Pokémon kivételkezelő, ami minden kivételt elkap.
A mindent elkapó kivételkezelő blokkok általában károsak, mivel komoly, illetve nem várt hibák elfedésére alkalmasak, ha azok nem jó helyen vannak. Bármilyen alkalmazást is készítünk, érdemes egy központi kivételkezelővel ellátni, ami legalább a hibát rögzíti vagy megjeleníti valahol, még mielőtt kilépne. Ez segíthet a problémák beazonosításában. Valamint ha van egy ilyen kivételkezelőnk, akkor a felhasználónak el tudjuk magyarázni, hogy mi történt és hol tud hibát bejelenteni. Ez kevésbé ciki, mint egyszerűen a Windows „A program működése leállt” ablakára támaszkodni.
Ha lehetséges, akkor úgy kell megírni a programunkat, hogy ne legyen szükség try–catch blokkokra, mégpedig azért, mert a kivétel dobás egy ugrást fog végrehajtani. Egy ilyen ugrás igen költséges művelet, ami a sebességet illeti, ezért a kivételkezelők túlzott használata tönkreteszi a felhasználói élményt és nem utolsó sorban nagyon komoly tervezési hibákra utalhat.
Legközelebb a teszteléssel folytatjuk. 🙂
2021.01.20. @ 07:07
Jó napot!
Érdeklődni szeretnék, hogy lesz-e befejező része a Letöltés mappa rendező programnak.
2021.01.20. @ 09:53
Üdözletem.
Igen, egy rövid kitérő után folytatódni fog a sorozat.