DDD stan encji

0

Cześć,

wyobraźmy sobie program typu issue tracker. Mam encję (w rozumieniu DDD):

class Issue:
    id: UUID
    name: str
    description: str
    author_id: UUID
    status: IssueStatus
    assignee_id: Optional[UUID]
class IssueStatus(Enum):
    TODO = 1
    IN_PROGRESS = 2
    DONE = 3

**Pytanie 1** W encji chciałbym mieć metody `Issue.start()` i `Issue.finish()`, które zmieniałyby status odpowiednio z `TODO` na `IN_PROGRESS` i z `IN_PROGRESS` na `DONE`. Nie chcę pozwolić na jakiekolwiek inne zmiany statusu, np. nie można z `DONE` wrócić na `IN_PROGRESS` wywołując metodę `Issue.start()` ponownie. Jak do tego podejść? Wymyśliłem dwa rozwiązania: a) Rzucić odpowiedni wyjątek w metodach, np: ```python class Issue: [...] def start(self): if self.status != IssueStatus.TODO: raise InvalidIssueStatus() self.status = IssueStatus.IN_PROGRESS ```` b) Wyrzucić status z encji i użyć dziedziczenia: ```python class Issue: id: UUID name: str description: str author_id: UUID assignee_id: Optional[UUID]

class TodoIssue(Issue):
def start(self):
return StartedIssue(...)

class StartedIssue(Issue):
def finish(self):
return FinishedIssue(...)

class FinishedIssue(Issue):
pass

Które lepsze? A może warto do tego podejść jeszcze inaczej?
<hr />
**Pytanie 2**
`Issue.assignee_id` jest opcjonalnie. Załóżmy, że nie można wystartować issue, które nie jest do nikogo przypisane, ergo każde issue o statusie `IN_PROGRESS` i `DONE` ma przypisanego pracownika. Jak taki kontrakt zawrzeć w kodzie? Walidacja w konstruktorze encji? Znowu dziedziczenie (np. abstrakcyjne `Issue` i dzieci `UnassignedIssue` i `AssignedIssue`)?
1
iksde napisał(a):

W encji chciałbym mieć metody Issue.start() i Issue.finish(), które zmieniałyby status odpowiednio z TODO na IN_PROGRESS i z IN_PROGRESS na DONE. Nie chcę pozwolić na jakiekolwiek inne zmiany statusu, np. nie można z DONE wrócić na IN_PROGRESS wywołując metodę Issue.start() ponownie.

Co jeśli zadanie będzie przedwcześnie zamknięte i użytkownik będzie chciał ponownie zaznaczyć to samo issue jako in progress z done?

A co jeśli użytkownik się pomylił i weźmie nie to issue co trzeba i będzie chciał cofnąć się z in progress do to do?

Nawet jeśli zignorujemy powyższe edge case'y (a nie powinniśmy, szczególnie jeśli idziemy w DDD), to tak:

przykład a) - normalny prosty kod (nie mówię, czy dobry / zły, ale że nic specjalnego)

przykład b) - to mi wygląda jak emulacja programowania funkcyjnego za pomocą dziedziczenia. Jest to na pewno ciekawy kod, nigdy nie myślałem o tym, że można za pomocą dziedziczenia emulować to, co w programowaniu funkcyjnym rozwiązuje się za pomocą niemutowalnych obiektów.

Swoją drogą mam wrażenie, że twoją intencją było raczej zaemulowanie typu wyliczeniowy / enum za pomocą dziedziczenia (ale to akurat nie jest niczym oryginalnym, często fani OOP używają hierarchii klas do tego, żeby zaemulować wartości enum, gdzie każdy typ to jak jedna wartość wyliczeniowa. Nie mówię, że to jest dobre, ale jednak częste).

Walidacja w konstruktorze encji? Znowu dziedziczenie (np. abstrakcyjne Issue i dzieci UnassignedIssue i AssignedIssue)?

To jest ten moment, w którym powiedzenie "preferuj kompozycję zamiast dziedziczenia" zabiera nabierać sensu. Jakkolwiek ciekawy i sprytny jest kod z przykładu b) to jednak nadużywanie dziedziczenia nie ma wiele sensu.

Poza tym z perspektywy dziedziny issue zamknięte nie jest jakimś innym rodzajem issue od tego, które jest in progress. Raczej jest tak, że dla użytkownika jest to tylko właściwość danego issue, ew. miejscem, w którym się pojawia na liście. Tak samo jak to, kto jest do niego przypisany.

0
LukeJL napisał(a):
iksde napisał(a):

W encji chciałbym mieć metody Issue.start() i Issue.finish(), które zmieniałyby status odpowiednio z TODO na IN_PROGRESS i z IN_PROGRESS na DONE. Nie chcę pozwolić na jakiekolwiek inne zmiany statusu, np. nie można z DONE wrócić na IN_PROGRESS wywołując metodę Issue.start() ponownie.

Co jeśli zadanie będzie przedwcześnie zamknięte i użytkownik będzie chciał ponownie zaznaczyć to samo issue jako in progress z done?

A co jeśli użytkownik się pomylił i weźmie nie to issue co trzeba i będzie chciał cofnąć się z in progress do to do?

Sytuacja może niemająca sensu w prawdziwym świecie, chciałem tylko jakoś zobrazować problem.

LukeJL napisał(a):
iksde napisał(a):

Walidacja w konstruktorze encji? Znowu dziedziczenie (np. abstrakcyjne Issue i dzieci UnassignedIssue i AssignedIssue)?

To jest ten moment, w którym powiedzenie "preferuj kompozycję zamiast dziedziczenia" zabiera nabierać sensu. Jakkolwiek ciekawy i sprytny jest kod z przykładu b) to jednak nadużywanie dziedziczenia nie ma wiele sensu.

Nie za bardzo widzę jak kompozycja rozwiąże tutaj problem, tzn. wydaje mi się, że będą z nią takie same problemy jak z dziedziczeniem. Mógłbyś rzucić jakimś pseudokodem?

Edit:
Znalazłem takie coś: https://medium.com/@martinezdelariva/explicit-state-modeling-f6e534c33508 (uwaga, PHP inside ;))
Faktycznie, w PPP of DDD stoi, że:
You reach this ideal by having a separate entity for each state, with only the applicable operations for the state modeled as part of the entity’s interface.
Więc wedle tego miałbym klasy TodoIssue, StartedIssue, FinishedIssue, tak jak w pierwszym poście. Tylko co, jeśli będę chciał dodać status BACKLOG, a issue w tym statusie może, ale nie musi być przypisane do pracownika (podobnie jak TodoIssue)? Z tego co rozumiem musiałbym mieć:

  • BacklogIssue z metodami assign() -> AssignedBacklogIssue i take_to_sprint() -> TodoIssue
  • AssignedBacklogIssue z metodą assign() -> AssignedTodoIssue
  • TodoIssue z metodą assign() -> AssignedTodoIssue
  • AssignedTodoIssue z metodą start() -> InProgressIssue
  • InProgressIssue z metodą finish() -> DoneIssue

Można by wymyślać kolejne statusy/pola (np. reviewer_id, które jest opcjonalne dla statusów BACKLOG, TODO, IN_PROGRESS, ale jest wymagane do przejścia z IN_PROGRESS na DONE) i szybko zrobi się z tego dwucyfrowa liczba klas, a chyba nie tędy droga.

0

Uwaga! Poniższy wpis ma charakter niepoważnych zabaw kodem. Nie radzę używać na produkcji.

W C# nie da się usunąć metody z generyka w zależności od typu, ale można pobawić się ograniczeniem widoczności aby efektywnie zabronić wykonania metod w zależności od stanu: https://dotnetfiddle.net/cspiya

using System;

	public interface IAssignedState {
	}
	
	public class Assigned : IAssignedState {
		Assigned() {}
	}
	
	public class Unassigned : IAssignedState{
		public Unassigned() {}
	}
	
	public interface IAssignable<T,U> where T : IAssignedState {
		U assign(T t);
	}

	public interface IBacklogState {
	}

	public class InBacklog : IBacklogState {
		public InBacklog() {}
	}

	public class Todo : IBacklogState {
		Todo() {}
	}

	public interface ITodoable<T, U> where T : IBacklogState {
		U addToSprint(T t);
	}


public class Issue<TAssignableState, TBacklogState> : IAssignable<TAssignableState, Issue<Assigned, TBacklogState>>, ITodoable<TBacklogState, Issue<TAssignableState, Todo>>
	where TAssignableState : IAssignedState 
	where TBacklogState : IBacklogState {
	
	public Issue<Assigned, TBacklogState> assign(TAssignableState t) {
		return new Issue<Assigned, TBacklogState>();
	}
	
	public Issue<TAssignableState, Todo> addToSprint(TBacklogState t){
		return new Issue<TAssignableState, Todo>();
	}
}
 
public class Program
{
	public static void onlyUnassignedIssues<TBacklogState>(Issue<Unassigned, TBacklogState> a) where TBacklogState : IBacklogState{
	}
	
    public static void Main()
    {
		Issue<Unassigned, InBacklog> unassigned = new Issue<Unassigned, InBacklog>();
		onlyUnassignedIssues(unassigned);
		Issue<Assigned, InBacklog> assigned = unassigned.assign(new Unassigned());
				
		onlyUnassignedIssues(assigned); // I cannot pass assigned issue
		Issue<Assigned, InBacklog> assigned2 = assigned.assign(new Assigned()); // I cannot assign issue again
		
		Issue<Unassigned, Todo> todo = unassigned.addToSprint(new InBacklog()); // I can first move it to sprint
		Issue<Assigned, Todo> assignedTodo = todo.assign(new Unassigned()); // And then assign
		
		Issue<Assigned, Todo> movedToSprintAgain = assignedTodo.addToSprint(new Todo()); // But I cannot move it to sprint again
    }
}

Podobne sztuczki da się zrobić traitami w Scali. W Scala in Action jest podany przykład z phantom types, ale tam znowu robiło się case classy, więc też było sporo dziedziczenia. Jednocześnie gdyby system typów pozwalał na higher kinded types (C# nie pozwala), to pewnie dałoby się to ograć bez takich sztuczek, jak powyższa.

3

Tu masz tylko 3 stany, więc możesz to ogarniać if'ami, ale jak będziesz miał bardziej złożone cykle życia dla różnych encji to może FSM? Powinno się to ładnie wpisać w koncepcję wykorzystania zdarzeń.

FSM w pythonie: http://code.activestate.com/recipes/578344-simple-finite-state-machine-class-v2/
DDD ożenione z FSM (w javie): https://github.com/statefulj/statefulj-framework-demo-ddd

1 użytkowników online, w tym zalogowanych: 0, gości: 1