Recentemente sono incappato in un codice che sembrava sbagliato. L'ho modificato solo per vedere che le mie modifiche hanno infranto il codice. Ho cercato di strutturare il codice in diversi altri modi per far sì che il codice fosse e sia corretto, ma non ero soddisfatto di alcuna alternativa che avrei potuto trovare.
La logica del codice è la seguente:
- Un
Path
è composto da zero o piùSegment
s. - Un
Segment
contiene:- una o più proprietà (qui solo una per brevità) e
- un riferimento al
Path
in cui è attivo.
- Invarianti che devono essere rispettati:
- Il riferimento
Path
suSegment
è sempre inizializzato. - Un
Segment
è un elemento di alcuniPath
'sSegments
. - Il riferimento
Part
suSegment
fa riferimento aPart
, cheSegments
contieneSegment
.
- Il riferimento
Ecco il codice che ho visto, quando pensavo di aver trovato un bug:
public class Path
{
public List<Segment> Segments { get; } = new List<Segment>();
}
public class Segment
{
public int Property { get; }
public Path Path { get; }
...
}
public static class Test
{
public static Path CreatePath()
{
var path = new Path();
var segment = new Segment(path, 42);
return path;
}
}
Semplicemente intravvedendo a CreatePath
e senza ispezionare ulteriormente ho concluso che c'era chiaramente un bug, dal momento che segment
non è stato aggiunto a path.Segments
e quindi violando un'invarianza.
A quanto pare ho sbagliato perché il costruttore di Segment
ha fatto più lavoro di quanto mi aspettassi.
Il costruttore attuale assomigliava a questo:
public Segment(Path path, int property)
{
Property = property;
Path = path;
path.Segments.Add(this); // <--
}
Pur riconoscendo l'intento del costruttore di garantire gli invarianti, non mi è piaciuto il modo in cui è stato fatto principalmente per due motivi:
- Il costruttore fa più lavoro che convalidare argomenti e inizializzare i campi.
-
CreatePath
sembra corretto solo quando conosci i componenti interni del costruttore.
Questo per me rompe il principio di Almost Astonishment - almeno ero sbalordito.
Ho provato a trovare modi alternativi per raggiungere questo obiettivo, ma non sono completamente soddisfatto di nessuno di essi.
Il codice originale in cui la coerenza è garantita all'interno del costruttore di Segment
. Il problema è che segment
sembra inutilizzato in CreatePath
se non conosci i componenti interni del costruttore.
namespace PathExample0
{
public class Path
{
public List<Segment> Segments { get; } = new List<Segment>();
}
public class Segment
{
public int Property { get; }
public Path Path { get; }
public Segment(Path path, int property)
{
Property = property;
Path = path;
path.Segments.Add(this);
}
}
public static class Test
{
public static Path CreatePath()
{
var path = new Path();
var segment = new Segment(path, 42);
return path;
}
}
}
Questa prima alternativa estrae la parte di consistenza del costruttore e in CreatePath
. Il costruttore è ora privo di sorprese e segment
è chiaramente utilizzato in CreatePath
.
Il problema minore è che ora non è garantito che segment
sia aggiunto a Segments
del giusto Path
.
namespace PathExample1
{
public class Path
{
public List<Segment> Segments { get; } = new List<Segment>();
}
public class Segment
{
public int Property { get; }
public Path Path { get; }
public Segment(Path path, int property)
{
Property = property;
Path = path;
}
}
public static class Test
{
public static Path CreatePath()
{
var path = new Path();
var segment = new Segment(path, 42);
path.Segments.Add(segment);
return path;
}
}
}
Il mio secondo tentativo Aggiunge un AddSegment
a Path
che garantisce la coerenza.
Questo ancora non ti impedisce di chiamare new Segment(somePath, 42)
e di rompere la consistenza.
namespace PathExample2
{
public class Path
{
private readonly List<Segment> segments = new List<Segment>();
public IReadOnlyList<Segment> Segments => segments;
public void AddSegment(int property)
{
var segment = new Segment(this, property);
segments.Add(segment);
}
}
public class Segment
{
public int Property { get; }
public Path Path { get; }
public Segment(Path path, int property)
{
Property = property;
Path = path;
}
}
public static class Test
{
public static Path CreatePath()
{
var path = new Path();
path.AddSegment(42);
return path;
}
}
}
Il terzo e ultimo esempio potrei trovare le interfacce out Segment
in ISegment
e rende Segment
una classe privata a Path
.
Questo dovrebbe ora garantire la piena coerenza tra un Path
e il suo Segment
s.
Per me sembra un approccio ingombrante e quasi un uso improprio delle interfacce per nascondere il costruttore di Segment
.
namespace PathExample3
{
public interface ISegment
{
Path Path { get; }
int Property { get; }
}
public class Path
{
private readonly List<Segment> segments = new List<Segment>();
public IReadOnlyList<ISegment> Segments => segments;
public void AddSegment(int property)
{
var segment = new Segment(this, property);
segments.Add(segment);
}
private class Segment : ISegment
{
public int Property { get; }
public Path Path { get; }
public Segment(Path path, int property)
{
Property = property;
Path = path;
}
}
}
public static class Test
{
public static Path CreatePath()
{
var path = new Path();
path.AddSegment(42);
return path;
}
}
}
Quindi la mia domanda è come strutturare il codice in un modo, per garantire gli invarianti pur mantenendo il codice C # idiomatico. Forse mi sto perdendo un modello di design ben noto per realizzare questo?