DDD stan encji

Odpowiedz Nowy wątek
2018-11-15 01:24
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:

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:

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?


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)?

edytowany 2x, ostatnio: iksde, 2018-11-15 01:26

Pozostało 580 znaków

2018-11-15 03:42
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.


((0b10*0b11*(0b10**0b101-0b10)**0b10+0b110)**0b10+(100-1)**0b10+0x10-1).toString(0b10**0b101+0b100);
edytowany 1x, ostatnio: LukeJL, 2018-11-15 03:44

Pozostało 580 znaków

2018-11-15 08:59
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/@martinezd[...]t-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.

edytowany 2x, ostatnio: iksde, 2018-11-15 11:14

Pozostało 580 znaków

2018-11-16 03:25

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.

edytowany 1x, ostatnio: Afish, 2018-11-16 03:34

Pozostało 580 znaków

2018-11-16 10:16
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/r[...]inite-state-machine-class-v2/
DDD ożenione z FSM (w javie): https://github.com/statefulj/statefulj-framework-demo-ddd

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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