Laboratoria: 4 XII¶
Celem zajęć jest poznanie konceptu typestate programming w języku Rust. W ogólności jest to wzorzec umożliwiający implementację automatu skończonego, której poprawność gwarantuje system typów na etapie kompilacji.
Wzorzec projektowy builder¶
Na laboratoriach dostarczymy dwie różne implementacje buildera dla prostej struktury:
struct Point {
x: i32,
y: i32,
}
Docelowo chcemy móc napisać:
let point = PointBuilder::new()
.x(0)
.y(1)
.build();
let point = PointBuilder::new()
.y(1)
.x(0)
.build();
jednocześnie zabraniając wielokrotnego przypisania wartości jednemu polu lub utworzenia instancji Point
bez podania wartości wszystkich pól.
Krok 1. (runtime checking) (*)¶
Dostarczamy najprostszą implementację buildera dla typu Point
.
Na razie błąd nieprawidłowego użycia (wielokrotne przypisanie wartości jednemu polu lub próba zbudowania finalnego obiektu bez przypisania wartości wszystkim jego polom) zgłaszamy w runtime.
Sugerowane są dwie możliwości sygnalizacji błędu:
- metody buildera zwracają odpowiednio
Result<Self, String>
iResult<Point, String>
- napotkawszy niepoprawne użycie panikujemy
Dla pełności rozwiązania dorzucamy unit testy, które weryfikują implementację (przydatne może być makro #[should_panic]
).
Krok 2. (compile-time checking) (*)¶
Teraz zajmiemy się alternatywną implementacją, która korzystając z konceptu typestate programming przesunie weryfikację użycia buildera z runtime’u do etapu kompilacji.
Aby to osiągnąć, uczynimy typ PointBuilder
generycznym po stanie w jakim się znajduje.
(Przez stan, mamy tu na myśli status przypisania wartości do pól.)
Dla każdego stanu stworzymy nowy typ reprezentujący go:
enum NothingSet {}
enum XSetYNotSet {}
enum XNotSetYSet {}
enum AllSet {}
Typy tworzymy jako puste Enum
y zapewniając tym samym, że nie można utworzyć obiektu stanu.
Nie jest to konieczne, ale daje to pewną czystość semantyczną kodu.
Dostarczamy również wspólny mianownik dla wszystkich możliwych stanów:
trait BuilderState {}
impl BuilderState for NothingSet {}
...
Ostatecznie typ naszego Buildera może przyjąć postać (choć nie jest to jedyna możliwość):
struct PointBuilder<S: BuilderState> {
x: Option<i32>,
y: Option<i32>,
}
Pozostała część zadania polega na dostarczeniu implementacji dla PointBuilder
zależnej od jego argumentu generycznego.
Innymi słowy, dostarczamy bloki typu:
impl PointBuilder<NothingSet> {
...
}
Na koniec dopisujemy testy potwierdzające poprawność rozwiązania.
Tym razem, będziemy musieli zweryfikować, że konkretny kod się nie kompiluje.
Do tego, sugeruję użyć bibliotekę trybuild
(https://docs.rs/trybuild/).
Krok 3. (redukcja ilości stanów)¶
Rozszerzamy strukturę Point
tak, aby posiadała 3 koordynaty i zmieniamy reprezentację stanu tak, aby zamiast 8 rozłącznych bloków impl PointBuilder<JedenZOśmiuStanów>
móc pogrupować wspolną logikę dla podzbiorów stanów.
Sugerowane są dwie możliwości:
PointBuilder
ma trzy parametry generyczne, po jednym na stan każdej współrzędnej- wprowadzamy nowe
trait
y grupujące odpowiednie podzbiory stanów