Walka z zasadą pojedynczej odpowiedzialności

11

Rozważ ten przykład:

Mam stronę internetową Pozwala użytkownikom tworzyć posty (mogą być dowolne) i dodawać tagi opisujące posty. W kodzie mam dwie klasy reprezentujące post i tagi. Nazwijmy te klasy Posti Tag.

Postzajmuje się tworzeniem postów, usuwaniem postów, aktualizowaniem postów itp. Tagzajmuje się tworzeniem tagów, usuwaniem tagów, aktualizowaniem tagów itp.

Brak jednej operacji. Łączenie tagów z postami. Walczę z tym, kto powinien wykonać tę operację. Może pasować równie dobrze w obu klasach.

Z jednej strony Postklasa może mieć funkcję, która przyjmuje Tagjako parametr, a następnie przechowuje ją na liście znaczników. Z drugiej strony Tagklasa może mieć funkcję, która przyjmuje Postjako parametr i łączy z Tagwłaściwością Post.

Powyższe to tylko przykład mojego problemu. Tak naprawdę mam do czynienia z wieloma podobnymi klasami. Może pasować równie dobrze w obu. Poza faktem umieszczenia funkcjonalności w obu klasach, jakie konwencje lub style projektowania istnieją, aby pomóc mi rozwiązać ten problem. Zakładam, że musi być coś poza wybraniem jednego?

Może umieszczenie go w obu klasach jest prawidłową odpowiedzią?

Wściekły ptak
źródło

Odpowiedzi:

11

Podobnie jak Kodeks Piracki, SRP jest raczej wytyczną niż zasadą, a nawet nie jest szczególnie dobrze sformułowana. Większość programistów zaakceptowała redefinicje Martina Fowlera (w Refactoring ) i Roberta Martina (w Clean Code ), co sugeruje, że klasa powinna mieć tylko jeden powód do zmiany (w przeciwieństwie do jednej odpowiedzialności).

To dobra, solidna wytyczna (przepraszam za kalambur), ale zawieszenie się na niej jest tak samo niebezpieczne, jak ignorowanie.

Jeśli możesz dodać post do tagu i odwrotnie, nie złamałeś zasady pojedynczej odpowiedzialności. Oba nadal mają tylko jeden powód do zmiany - jeśli zmieni się struktura tego obiektu. Zmiana struktury jednego z nich nie zmienia sposobu dodawania go do drugiego, więc nie dodajesz do niego nowej „odpowiedzialności”.

Ostateczna decyzja powinna być podyktowana funkcjonalnością wymaganą od interfejsu użytkownika. Prawdopodobnie w pewnym momencie konieczne będzie dodanie tagu do postu, więc zrób coś takiego:

// C-style-language pseudo-code
class Post {
    string _title;
    string _content;
    Date _date;
    List<Tag> _tags;

    Post(string title, string content) {
        _title = title;
        _content = content;
        _date = Now;
        _tags = new List<Tag>();
    }

    Tag[] getTags() {
        return _tags.toArray();
    }

    void addTag(Tag tag) {
        if (_tags.contains(tag)) {
            throw "Cannot add tag twice";
        }

        _tags.Add(tag);
        tag.referencePost(this);
    }

    // more stuff here, obviously
}

class Tag {
    string _name;
    List<Post> _posts;

    Tag(string name) {
        _name = name;
    }

    Post[] getPosts() {
        return _posts.toArray();
    }

    void referencePost(Post post) {
        if (!post.getTags().contains(this) || _posts.contains(post)) {
            throw "Only reference a post by calling Post.addTag()";
        }

        _posts.Add(post);
    }

    // more stuff here too
}

Jeśli później okaże się, że konieczne jest również dodawanie postów do tagów, wystarczy dodać metodę addPost do klasy Tag i metodę referenceTag do klasy Post. Oczywiście nadałem im różne nazwy, aby przypadkowo nie spowodować przepełnienia stosu, wywołując addTag z addPost i addPost z addTag.

pdr
źródło
Myślę, że związek między tagiem a postem jest wiele do wielu, w którym to przypadku sensowne jest utrzymywanie odniesień do wielu postów. Jak byś sobie z tym poradził, gdybyś zachował jedno odniesienie?
Andres F.,
@AndresF .: Zgadzam się z tobą, więc najwyraźniej nie napisałem zbyt dobrze mojej odpowiedzi. Zredagowałem znacznie. (Przepraszam poprzedniego zwycięzcę, jeśli to zmieni znaczenie tak, jak to widzieliście.)
pdr
6

Nie, nie w obu! Powinien być w jednym miejscu.

Niepokojące w moim pytaniu jest to, że mówisz „ Postdba o tworzenie postów, usuwanie postów, aktualizowanie postów” i to samo Tag. Cóż, to nie w porządku. Postmoże zająć się tylko aktualizacją, to samo dotyczy Tag. Tworzenie i usuwanie jest dziełem kogoś innego, zewnętrznego Posti Tag(nazwijmy to Store).

Dobra odpowiedzialność za Postto „zna autora, treść i datę ostatniej aktualizacji”. Dobra odpowiedzialność za Tagto „zna jego nazwę i cel (czytaj: opis)”. Dobra odpowiedzialność za Storeto: „zna wszystkie posty i wszystkie tagi oraz może je dodawać, usuwać i wyszukiwać”.

Jeśli spojrzysz na tych trzech uczestników, kto w naturalny sposób powinien mieć wiedzę na temat relacji Post-Tag?

(dla mnie jest to Post, wydaje się naturalne, że „zna swoje tagi”; wyszukiwanie wsteczne (wszystkie posty dla tagu) wydaje się być zadaniem Sklepu; chociaż mogę się mylić)

herby
źródło
Jeśli każdy post ma listę tagów i / lub każdy tag ma listę postów, do których należą, można łatwo odpowiedzieć na pytanie „czy post x zawiera tag y”. Czy istnieje jakikolwiek sposób na skuteczne udzielenie odpowiedzi na takie pytanie bez ponoszenia odpowiedzialności przez którąkolwiek klasę, poza wykorzystaniem czegoś takiego ConditionalWeakTable(zakładając, że ktoś ma szczęście, aby mieć strukturę, w której istnieje)?
supercat
3

W równaniu brakuje ważnego szczegółu. Dlaczego tag zawiera post i visa-versa? Odpowiedź na to pytanie określa rozwiązanie dla każdego zestawu.

Ogólnie mogę sobie wyobrazić podobną sytuację. Pudełko i zawartość. Pudełko ma zawartość, więc odpowiedni jest związek typu „ma związek”. Czy zawartość może mieć pudełko? Jasne, pudełko z pudełkiem. Pudełko to zawartość. Ale IS-A nie jest dobrym projektem dla wszystkich pudeł. W takim przypadku rozważyłbym wzór dekoratora. W ten sposób pudełko jest w razie potrzeby ozdabiane zawartością w czasie wykonywania.

Tagi mogą mieć również posty, ale dla mnie nie jest to związek statyczny. Raczej może to być raport o wszystkich postach z tym tagiem. W tym przypadku jest to nowy byt, nie ma-a.

P.Brian.Mackey
źródło
2

Podczas gdy w teorii takie rzeczy mogą pójść w obie strony, w praktyce, gdy zabierzesz się do implementacji, jedna droga jest prawie zawsze lepsza niż druga. Mam przeczucie, że będzie lepiej pasować do Postklasy, ponieważ skojarzenie zostanie utworzone podczas tworzenia lub edycji postu, gdy inne rzeczy dotyczące postu zmieniają się w tym samym czasie.

Ponadto, jeśli kojarzysz wiele tagów i chcesz to zrobić w ramach jednej aktualizacji bazy danych, przed wykonaniem aktualizacji musisz utworzyć listę wszystkich tagów powiązanych z tym samym postem. Ta lista pasuje znacznie lepiej w Postklasie.

Karl Bielefeldt
źródło
1

Osobiście nie dodałbym tej funkcji do żadnego z nich.

Dla mnie oba Posti Tagsą obiektami danych, więc nie powinno być funkcjonalność obsługi bazy danych. Powinny po prostu istnieć. Są przeznaczone do przechowywania danych i mogą być wykorzystywane przez inne części aplikacji.

Zamiast tego miałbym inną klasę odpowiedzialną za logikę biznesową i dane dotyczące Twojej strony internetowej. Jeśli twoja strona wyświetla post i pozwala użytkownikom dodawać tagi, klasa miałaby Postobiekt i zawierałaby funkcjonalność do dodania Tagsdo niego Post. Jeśli strona wyświetla tagi i pozwala użytkownikom dodawać posty do tych tagów, zawierałby Tagobiekt i miałby funkcję dodawania Postsdo niego Tag.

Ale to tylko ja. Jeśli uważasz, że musisz obsłużyć funkcjonalność bazy danych w swoich obiektach danych, polecam odpowiedź pdr

Rachel
źródło
0

Gdy czytam to pytanie, pierwszą rzeczą, która przyszła mi do głowy, była relacja bazy danych Many To Many . Posty mogą mieć wiele tagów ... Tagi mogą mieć wiele postów .... Wydaje mi się, że obie klasy potrzebują do pewnego stopnia zarządzania tą relacją.

Z punktu widzenia Publikuj ...
Jeśli podczas edytowania lub tworzenia postu działaniem dodatkowym staje się zarządzanie relacjami z tagami .

  1. Dodaj istniejący tag do postu
  2. Revove Tag from Post

IMO, stworzenie zupełnie nowej TAG nie należy tutaj.

Z punktu widzenia tagu ...
Możesz utworzyć tag bez konieczności przypisywania go do posta. Jedyne działanie, które widzę, które dotyczy interakcji z postem, to funkcja usuwania znaczników. Jednak ta funkcja powinna być niezależną niezależną funkcją.

Działa to tylko wtedy, gdy istnieje tabela łączy bazy danych, która rozwiązuje relację wiele do wielu

Michael Riley - AKA Gunny
źródło