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ùSegments. - Un
Segmentcontiene:- una o più proprietà (qui solo una per brevità) e
- un riferimento al
Pathin cui è attivo.
- Invarianti che devono essere rispettati:
- Il riferimento
PathsuSegmentè sempre inizializzato. - Un
Segmentè un elemento di alcuniPath'sSegments. - Il riferimento
PartsuSegmentfa riferimento aPart, cheSegmentscontieneSegment.
- 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.
-
CreatePathsembra 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?