Dear architekci,
jak sensownie zapewnić aby stan obiektu był poprawny?
Wyjdźmy od aktualnego trendu w OOP, a mianowicie modelus anemicus
public class Company
{
public string Name { get; set; }
public string NIP { get; set; }
}
var company = new Company
{
Name = "Fast Company",
NIP = "123"
};
company.NIP = "🐖🐖" // OK
Nie mamy tu żadnego zabezpieczenia przed wprowadzeniem obiektu w niepożądany stan, a zatem słabo.
public class Company
{
private string _name;
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException(nameof(value));
_name = value;
}
}
private string _nip;
public string NIP
{
get { return _nip; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException(nameof(value));
if (!NIPValidator.IsOK(value))
throw new ValidationException("Zly NIP");
_nip = value;
}
}
}
public static class NIPValidator
{
public static bool IsOK(string s)
{
return s.All(x => char.IsDigit(x));
}
}
var company = new Company
{
Name = "Fast Company",
NIP = "6123123406" // OK
};
company.NIP = "abc"; // FAIL
W tym podejściu nasz obiekt potrafi się wybronić, ale głównie dlatego że w/w przykłady są naïveté
Załóżmy że nagle potrzebujemy walidować czy np. NIP jest unikatowy i co teraz?
Z jednej strony gdyby NIPSetter miał dostęp do bazki przez jakąś abstrakcję, to w sumie by to działało.
public class Company
{
public Company(string name, string nIP, INIPValidator validator)
{
Name = name;
NIP = nIP;
Validator = validator;
}
public string Name { get; private set; }
public string NIP { get; private set; }
private INIPValidator Validator { get; }
public Result<bool> SetNIP(string s)
{
if (Validator.IsOk(s))
{
NIP = s;
return new Result<bool>();
}
else
{
return new Result<bool>("Zly NIP");
}
}
}
public class NIPValidator : INIPValidator
{
public NIPValidator(Bazka bazka)
{
Bazka = bazka;
}
public Bazka Bazka { get; }
public bool IsOk(string s)
{
return s.All(x => char.IsDigit(x)) && Bazka.IsUnique(s);
}
}
var company = new Company("Fast Company", "6123123406", new NIPValidator(new Bazka()));
var result = company.SetNIP("asd"); // FAIL.
Ten sposób wygląda dość przyzwoicie, ale bardzo utrudnia tworzenie obiektu, a w szczególności gdy on urośnie
oraz nadal ktoś może "wstrzyknąć" swój NIPValidator
i popsuć.
A w dodatku może jednak nie chcemy tego NIPValidatora mieć w tym obiekcie?
Podejście "service based"
public class Company
{
public string Name { get; set; }
public string NIP { get; set; }
}
public class CompanyService
{
public CompanyService(INIPValidator validator)
{
Validator = validator;
}
private INIPValidator Validator { get; }
public Result<bool> ChangeNIP(string companyId, string s)
{
var company = Bazka.FindCompany(companyId);
if (company is null)
return new Result<bool>("404");
if (!Validator.IsOk(s))
{
return new Result<bool>("Zly NIP");
}
company.NIP = s;
return new Result<bool>();
}
}
var company = new Company
{
Name = "Fast Company",
NIP = "123"
};
var service = new CompanyService(new NIPValidator(new Bazka()));
var result = service.ChangeNIP(company.Name, "ABC"); // fail;
Krótko, ale wszystko opiera się o założenie że każdy będzie wiedział o CompanyService
i faktycznie będzie go używał zamiast yolo.
Idąc dalej
public class Company
{
private Company(string name, string nIP)
{
Name = name;
NIP = nIP;
}
public string Name { get; }
public string NIP { get; private set; }
public class CompanyBuilder
{
public CompanyBuilder(Bazka bazka)
{
Bazka = bazka;
}
public Bazka Bazka { get; }
private string NIP_Candidate { get; set; }
private string Name_Candidate { get; set; }
public CompanyBuilder SetNIP(string s) { NIP_Candidate = s; return this; }
public CompanyBuilder SetName(string s) { Name_Candidate = s; return this; }
public Result<Company> TryBuild()
{
if (string.IsNullOrEmpty(NIP_Candidate) || !IsOk(NIP_Candidate) || !Bazka.IsUnique(NIP_Candidate))
{
return new Result<Company>("Zly NIP");
}
return new Result<Company>(new Company(Name_Candidate, NIP_Candidate));
}
private bool IsOk(string s)
{
return s.All(x => char.IsDigit(x));
}
}
}
var companyBuilder = new Company.CompanyBuilder(new Bazka());
var result = companyBuilder
.SetName("Fast Company")
.SetNIP("6123123406")
.TryBuild();
no, teraz to już jest state of art ;) /s, ale w sumie to jak zrobić Edit?
i tutaj wydaje mi się, że przychodzi taki szalony koncept, że obiekt po utworzeniu się nie zmienia :D
i (dla uproszczenia) do naszego companyBuilder dochodzi metoda
public class CompanyBuilder
{
public CompanyBuilder(Bazka bazka)
{
Bazka = bazka;
}
...
...
...
public Result<Company> ChangeCompanyNIP(Company c, string newNIP)
{
var builder = new CompanyBuilder(this.Bazka);
var result = builder
.SetName(c.Name)
.SetNIP(newNIP)
.TryBuild();
// jeszcze id by wypadało skopiować
return result;
}
}
I koniec końców mamy takie coś:
var companyBuilder = new Company.CompanyBuilder(new Bazka());
var result = companyBuilder
.SetName("Fast Company")
.SetNIP("6123123406")
.TryBuild(); // OK
var changeResult = companyBuilder.ChangeCompanyNIP(result.Company, "abc"); // FAIL
No i tak pi*drzwi to podejście w oparciu o builder mi się podoba,
Wady jakie widzę w Immutable + Builderze to takie że:
-
ktoś nadal może zepsuć podając swoją bazkę
-
performance overhead, no bo aby zmienić jedną rzecz, to trzeba "przeliczyc" cały obiekt.
Oczywiście ta implementacja jest trochę ugly, ale chodzi o sam koncept.
Jeżeli chodzi o implementacje z INIPValidator
w Company, to na + była jej mega prostota (przy jednym Validatorze :P).
A zatem wracając do pytania
Jak zgrabnie zapewnić aby nikt nie mógł zepsuł i aby system się bronił, a przy okazji był relatywnie elastyczny?
A może jakieś "mechanizmy z DDD" by tu były pomocne?