Letöltés mappa rendező program – 2. rész
Az előző részben átbeszéltük a program követelményeit, így a sorozat mai részében elkészítjük a konfigurációs fájl beolvasó részt és a program vázát.
A szükséges körök
Minden új projekt létrehozása a projekt struktúra és a verziókezelés beállításával kezdődik. Ebben az esetben sincs ez másként. Egy új konzol alkalmazás létrehozásával kezdjük. Ezt parancssorból az alábbi parancsok kiadásával tudjuk megtenni:
mkdir DloadOrganizer
cd DloadOrganizer
mkdir DloadOrganizer
cd DloadOrganizer
dotnet new console
A Dupla DloadOrganizer mappa létrehozás nem véletlen. A külső mappa itt a solution fájl és a GIT repó fő könyvtára lesz, amiből a további projektek elérhetőek. Már ilyenkor érdemes a GIT repót létrehozni és az eddigi projektstruktúrát kommitolni.
git init
curl https://raw.githubusercontent.com/github/gitignore/master/VisualStudio.gitignore --output .gitignore
git add .
git commit -m "Initial commit"
A második curl parancs egy Visual Studio számára létrehozott gitignore fájlt tölt le. Ez minden olyan fájlt ignorál, ami nem kell a projektünk fordításához.
Ezt követően elkezdhetünk kódolni. A projekt megnyitása és az sln fájl elmentését követően duplán kattintsunk a csproj fájlra a Solution Explorer-ben, majd a megnyíló XML szerkesztőben az első PropertyGroup csomópont alá (ahol a OutputType és TargetFramework csomópontok szerepelnek) tegyük be a következő csomópontot:
<Nullable>Enable</Nullable>
Ezzel engedélyeztük a C# 8 Nullable reference types használatát.
Konfigurációs fájl struktúrák
A kódolást a konfigurációs fájl struktúrájának kialakításával kezdjük. Ez egy domain osztály lesz. A domain osztályok olyan osztályok, amik a program által megoldott problémakör leírásához szükségesek.
Az objektumorientáltság megengedi, hogy az adatot keverjük a funkcionalitással, de ez nem teljesen célszerű. Ha keverjük a funkcionalitást az adatleírással, akkor egy osztálynak több felelősségi köre lesz és automatizáltan nem lesz tesztelhető a kód, vagy csak nagyon nehezen, lényegében ellent mondunk a S.O.L.I.D elveknek
A konfigurációs fájl leírására az alábbi két osztályt hoztam létre egy Configuration mappán (ami névteret is létrehoz) belül:
namespace DloadOrganizer.Configuration
{
internal sealed class Rule
{
public string[] Patterns { get; set; }
public string TargetDirectory { get; set; }
public Rule()
{
Patterns = new string[0];
TargetDirectory = string.Empty;
}
}
}
A Rule osztály egy csoportosítási szabályt ír le. A TargetDirectory a cél mappát jelöli, míg a Patterns tömb a célmappába helyezendő kiterjesztéseket írja le. Az osztály kapott egy alapértelmezett konstruktort, ami gondoskodik róla, hogy null értéket egyik property se tudjon felvenni.
namespace DloadOrganizer.Configuration
{
internal sealed class Config
{
public string SourceDirectory { get; set; }
public Rule[] Rules { get; set; }
public Config()
{
SourceDirectory = string.Empty;
Rules = new Rule[0];
}
}
}
A Config osztály a konfiguráció fő struktúráját írja le. A SourceDirectory a forrás könyvtár lesz, amiből rendezzük a fájlokat, a Rules tömb pedig a rendezési szabályok gyűjteménye. Ez az osztály is kapott egy konstruktort, ami szintén a null értékek elkerülését szolgálja.
Mint látható, mind a két osztály internal és sealed módosítót kapott. Ez azt jelenti, hogy csak a szerelvényen belülről használhatóak és nem lehet belőlük örököltetni, mivel jelen esetben nem is lenne értelme.
A konfiguráció beolvasása
A konfigurációs fájlok kezeléséért a ConfigurationManager osztályt tettem felelőssé. Ez három, a konfigurációs fájlhoz kapcsolódó dolgot fog megvalósítani:
- Konfigurációs fájl meglétének ellenőrzése (
IsConfigExistingproperty) - Konfigurációs fájl beolvasása (
ReadConfigurationFile()metódus) - Minta konfigurációs fájl létrehozása (
WriteExampleConfig()metódus)
A single responsibilty elv alapján gyanús, vagy félreérthető lehet, hogy három dolgot is csinál egy osztály. Ez az elv nem feltétlen azt jelenti, hogy egy osztály az egy metódus, mert annak nem sok értelme lenne a gyakorlatban.
Itt inkább arra kell gondolni, hogy egy osztály egy problémakörhöz szorosan tartozó dolgokat valósítson meg. Ha szőrszál hasogatók szeretnénk lenni, akkor a fájl olvasás, ami string típusban visszaadja a JSON fájl tartalmát, lehetne egy külön osztály is, ha a JSON jöhetne fájlból vagy mondjuk adatbázisból is.
Jelen esetben viszont a feladatának megfelel így a terv, ha a későbbiekben majd szükség lesz ilyesmire, akkor a feladatkörét szét lehet vágni. Ennek kapcsán megjegyezném, hogy ne tervezzünk olyan részeket meg előre, amik nem szükségesek, mert egyrészt csak az életünket bonyolítjuk, másrészt mire oda kerülünk, hogy szükség lenne rá, már rég megváltozott az elképzelésünk és a specifikáció is.
using DloadOrganizer.Configuration;
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
namespace DloadOrganizer
{
internal sealed class ConfigurationManager
{
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 = "Documents"
}
}
};
using (StreamWriter file = File.CreateText(Path.Combine(AppDir, ConfigFile)))
{
string json = JsonSerializer.Serialize(cfg, new JsonSerializerOptions
{
WriteIndented = true
});
file.Write(json);
}
}
}
}
Ahogy látható, a kód a System.Text.Json névtérben található JsonSerializer osztály segítségével olvas és ír JSON fájlokat. Ennek használatához a System.Text.Json NuGet csomagot telepítenünk kell a beépített csomagkezelő segítségével.
Az osztály konstruktorában, amikor az AppDir tulajdonság értéket kap, láthatunk egy ?? jeles null ellenőrzést. Erre azért van szükség, mert a Path.GetDirectoryName() null értéket ad vissza, ha nem sikerült neki az argumentumban meghatározott útvonalból kihámoznia a mappa nevét. Ebben az esetben a jelenlegi mappa nevet (ahonnan indították a programot) használjuk.
Az AppDir property a konfigurációs fájl helyének megállapításához szükséges.
A minta konfigurációs fájl létrehozásánál feltűnhet az argumentumnak adott JsonSerializerOptions osztály. Ez a JSON Serializer működését befolyásolja. Kiíráskor talán a legfontosabb beállítása a WriteIndented, ami szépen formázott JSON dokumentum létrehozására utasítja a Serializer-t.
Ennek hiányában a JSON dokumentumunk egy soros lenne, ami webes átvitel esetén kifejezetten hasznos, mert sallang mentes, de olvashatóság és szerkesztés szempontjából viszont borzalmas.
Validáció
Attól, hogy be sikerült olvasnunk a JSON konfiguráció tartalmát, még nem biztos, hogy azzal a programunk dolgozni is tudni fog. Elképzelhető, hogy a felhasználó hiányosan, vagy nem kellő körültekintéssel adta meg a dolgokat. Éppen ezért ellenőrizni kell a konfiguráció tartalmát. Ezt a folyamatot validációnak nevezzük.
Jelen esetben a validációért a ConfigurationValidator osztály fog felelni. Ez egy Config típusú objektumról eldönti, hogy érdemes-e a program további részeinek foglalkoznia vele.
Egy konfigurációs fájlt akkor tekintünk helyesnek, ha
- A
SourceDirectoryproperty ki van töltve és az ott megadott mappa létezik. - A konfiguráció legalább egy rendezési szabályt tartalmaz.
- A rendezési szabályok közül mindegyik rendelkezik kitöltött cél mappával és az létezik is.
- A rendezési szabályok közül mindegyik rendelkezik legalább egy kiterjesztéssel, amit a cél mappába kell helyeznie.
using DloadOrganizer.Configuration;
using DloadOrganizer.Properties;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace DloadOrganizer
{
internal sealed class ConfigurationValidator
{
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;
}
}
}
A fenti kódrészletből jól látható, hogy a validációt az IsValid(Config config) metódus végzi el, ami a hibákat az osztályban található _errors listába gyűjti. Később majd ezt fogjuk felhasználni a felhasználó értesítésére a problémákról.
Mint látható, a hiba üzenetek egy Resources osztályban kaptak helyet. Hiba és egyéb üzeneteket sosem érdemes beégetni a programba közvetlenül. Leginkább azért, mert ha több helyen kell megjeleníteni ugyanazt a hiba üzenetet és beégettük azokat az osztályokba, majd később ne talán módosítani kell, akkor a karbantartás több munka lesz, illetve ha egy helyen kimarad az átírás, akkor bizony hibát sikerült vinni a rendszerbe.
További érv a beégetés ellen, hogy így nem lehet fordítani a programot több nyelvre, ha erre szükség lenne.
A szöveges erőforrások tárolására vezették be resx fájlokat. Ilyen fájlból többet is tartalmazhat egy alkalmazás. A projekthez rendelten egyet a csproj Properties menüjében (Solution Explorer-ből érjük el) tudunk kreálni a Resources fül alatt.
Itt az erőforrásnak rendelkeznie kell egy névvel, ami egy tulajdonságot fog létrehozni a Resources osztályban, amivel könnyen el tudjuk majd kódból érni a Value oszlopban megadott szöveget.