Zbyt wiele odpowiedzialności klasy - jak to podzielić

0

Piszę sobie na boku projekt, w którym postanowiłem z początku nie skupiać się za bardzo na architekturze, wzorcach, clean code, itp, chciałem po prostu dowieźć jak najszybciej założone MVP. Skończyło się na tym, ze backend dojechał faktycznie bardzo szybko, jednak póki kolega dłubie przy froncie i ja nie mam co robić, chciałem trochę poprawić to co napisałem.

No i jedna klasa szczególnie mi się nie podobna, aczkolwiek brak mi pomysłów jaki wzorzec tu zastosować, czy da się w ogóle ją jakoś zdekomponować, podzielić na mniejsze klasy?
Jest ona odpowiedzialna za tworzenie użytkownika, czyli musi:
-przeprowadzić walidację
-zapisać użyktownika na bazie
-wygenerować i zapisać na bazie token aktywacyjny
-wysłać maila z tym tokenem

Pierwsze co się rzuca to Single Responsible Principle które tutaj jest łamane... Z resztą wrzucam sam kod i z chęcią usłyszę wszelką krytykę i sugestie jak to poprawić

@Service
@RequiredArgsConstructor
public class UserCreator {
    private final UserRepository userRepository;
    private final ActivationTokenRepository tokenRepository;
    private final EmailSender emailSender;
    private final UserValidator userValidator;
    private final PasswordEncoder encoder;

    public MessageDto registerUser(RegisterUserDto registerUserDto) throws ApplicationException {
        userValidator.validate(registerUserDto);
        var user = saveUser(registerUserDto);
        var token = generateToken(user.getUsername());
        sendEmail(user.getEmail(), user.getUsername(), token.getId());
        return new MessageDto("User '" + registerUserDto.getUsername() + "' successfully created. Please check your email to activate your account");
    }

    private User saveUser(RegisterUserDto registerUserDto) {
        var user = buildUser(registerUserDto);
        return userRepository.save(user);
    }

    private ActivationToken generateToken(String username) {
        var token = buildToken(username);
        return tokenRepository.save(token);
    }

    private void sendEmail(String receiverAddress, String username, String tokenID) {
        var activationEmail = buildEmail(receiverAddress, username, tokenID);
        emailSender.sendEmail(activationEmail);
    }

    private User buildUser(RegisterUserDto registerUserDto) {
        return User.builder()
                .username(registerUserDto.getUsername())
                .password(encoder.encode(registerUserDto.getPassword()))
                .email(registerUserDto.getEmail())
                .active(false)
                .roles(Set.of("USER"))
                .build();
    }
    
    private UserActivationEmailDto buildEmail(String receiverAddress, String username, String tokenID) {
        return UserActivationEmailDto.builder()
                    .subject("Aktywacja konta na xxx.pl")
                    .receiver(receiverAddress)
                    .token(tokenID)
                    .username(username)
                    .build();
    }
    
    private ActivationToken buildToken(String username) {
        return ActivationToken.builder()
                    .username(username)
                    .expirationDateTime(LocalDateTime.now().plusHours(1))
                    .activated(false)
                    .build();
    }
}
1

Dlaczego SRP jest łamane skoro ładnie delegujesz, a w samej klasie jest ładny proceduralny kod obsługi rejestracji? Jedynie wysyłkę maila zrobiłbym w oparciu o event UserRegistered - wtedy zniknie zależność od EmailSender i nie będziesz generował maila w tej klasie. Reszta kroków po prostu musi się zadziać. Podoba mi się ten kod, czas i energię przeznaczyłbym na inne rzeczy :)

EDIT: może budowanie Usera bym jeszcze stąd wyniósł. Rejestracja mogła by operować po prostu na jakimś Userze bez wiedzy, w jaki sposób go zbudować.

0

Jest ona odpowiedzialna za tworzenie użytkownika, czyli musi:
-przeprowadzić walidację
-zapisać użyktownika na bazie
-wygenerować i zapisać na bazie token aktywacyjny
-wysłać maila z tym tokenem

Nie jestem do końca przekonany czy to wszystko powinna robić jedna klasa. Masz 5 zależności, to już zaczyna być dużo. Na początek oddzieliłbym tworzenie samego obiektu użytkownika (walidacja, zapisanie obiektu), od tego co dzieje się potem (wygenerowanie tokenu, wysłanie maila).

No i zawsze mi smutno jak widzę metodę validate typu void :(

1

Lekko tę klasę odchudziłem (przy okazji zmieniłem jej nazwę)

@Service
@RequiredArgsConstructor
public class UserRegisterer {
    private final UserCreator userCreator;
    private final TokenGenerator tokenGenerator;
    private final EmailSender emailSender;

    public MessageDto registerUser(RegisterUserDto registerUserDto) throws ApplicationException {
        var user = userCreator.createUser(registerUserDto);
        var token = tokenGenerator.generateToken(user.getUsername());
        emailSender.sendEmail(EmailFactory.activationEmail(user.getEmail(), user.getUsername(), token.getId()));
        return new SuccessfulRegistrationDto(user.getUsername());
    }
}

Charles_Ray napisał(a):

Jedynie wysyłkę maila zrobiłbym w oparciu o event UserRegistered - wtedy zniknie zależność od EmailSender i nie będziesz generował maila w tej klasie. Reszta kroków po prostu musi się zadziać.

Sugerujesz tutaj coś jak np tu? https://www.baeldung.com/spring-events

danek napisał(a):

No i zawsze mi smutno jak widzę metodę validate typu void :(

Sugerujesz zapewne użycie vavra? Przyznaje że userValidator w środku po prostu rzuca dedykowane RuntimeException które są obsługiwane w @ControllerAdvice :( Jak wspomniałem, celem póki co było jak najszybsze dojechanie z działającym kodem, który teraz mogę refaktorować.

1

Teraz imo wygląda lepiej ;)

Sugerujesz zapewne użycie vavra?

Ale tylko po cichu bo mnie zaraz dojadą :D mam gdzieś serie wpisów o obsłudze wyjątków (na moim mikroblogu tutaj). ale. Jeśli czujesz się jeszcze początkującym i dopiero zaczynasz i nie chcesz jakoś mocno mieszać sobie w głowie to to póki co olej (acz imo warto znać alternatywę ;) )

0

Single Responsible Principle - większość programistów niezrozumiała Martina w pierwszych książkach (ja też nie) i musi się on teraz z tej reguły tłumaczyć:
https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html

W skrócie:

And this gets to the crux of the Single Responsibility Principle. This principle is about people.

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function. You want to isolate your modules from the complexities of the organization as a whole, and design your systems such that each module is responsible (responds to) the needs of just that one business function.

Why? Because we don’t want to get the COO fired because we made a change requested by the CTO. Nothing terrifies our customers and managers more that discovering that a program malfunctioned in a way that was, from their point of view, completely unrelated to the changes they requested. If you change the calculatePay method, and inadvertently break the reportHours method; then the COO will start demanding that you never change the calculatePay method again.

Imagine you took your car to a mechanic in order to fix a broken electric window. He calls you the next day saying it’s all fixed. When you pick up your car, you find the window works fine; but the car won’t start. It’s not likely you will return to that mechanic because he’s clearly an idiot.

Powoływanie się na poprawność lub niepoprawność reguły SRP bez znajomości biznesu/domeny jest na ogół błędne.

0

W sumie SRP to jest chyba najtrudniejsza z zasad w SOLID, nie sądze jednak żeby ta klasa łamała zasady SRP nawet przy 5 zależnościach. Szczerze mówiąc to jak juz raczej bym zamienił EmailSender na coś bardziej abstrakcyjego, ale może to dlatego że za bardzo hexagonalna weszła za bardzo xD

@RequiredArgsConstructor

Czy tylko ja mam odruchy wymiotne jak to widzę? xD

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