Ваше ощущение абсолютно правильное. Я с тобой согласен.
Я думаю, что ваша проблема начинается с вашего стиля именования, поскольку вы, кажется, прекрасно понимаете, что означает SRP. Имена классов, такие как "...Service" или "...Manager" имеют очень расплывчатое значение или семантику. Они описывают более обобщенный контекст или концепцию. Другими словами, класс '...Manager' предлагает вам поместить все внутрь, и он по-прежнему кажется правильным, потому что это менеджер.
Когда вы станете более конкретными, пытаясь сосредоточиться на истинных концепциях ваших классов или их обязанностей, вы автоматически найдете более крупные имена с более сильным значением или семантикой. Это действительно поможет вам разделить классы и определить обязанности.
Рекомендуемая розничная цена:
Никогда не должно быть более одной причины для изменения определенного модуля.
Вы можете начать с переименования UserService
в UserDatabaseContext
. Теперь это автоматически заставит вас помещать в этот класс только операции, связанные с базой данных (например, операции CRUD).
Здесь даже можно уточнить. Что вы делаете с базой данных? Вы читаете из и пишете в него. Очевидно две обязанности, что означает два класса: один для операций чтения и другой ответственный за операции записи. Это могут быть очень общие классы, которые могут просто читать или писать что угодно. Давайте назовем их DatabaseReader
и DatabaseWriter
, и, поскольку мы пытаемся все разделить, мы будем использовать интерфейсы везде. Таким образом мы получаем два интерфейса IDatabaseReader
и IDatabaseWriter
. Эти типы очень низкого уровня, так как они знают базу данных (Microsoft SQL или MySql), как к ней подключиться и точный язык для запроса (используя, например, SQL или MySql):
// Knows how to connect to the database
interface IDatabaseWriter {
void create(Query query);
void insert(Query query);
...
}
// Knows how to connect to the database
interface IDatabaseReader {
QueryResult readTable(string tableName);
QueryResult read(Query query);
...
}
Кроме того, вы можете реализовать более специализированный уровень операций чтения и записи, например. данные, связанные с пользователем. Мы бы представили интерфейсы IUserDatabaseReader
и IUserDatabaseWriter
. Эти интерфейсы не знают, как подключиться к базе данных или какой тип базы данных используется. Эти интерфейсы знают только, какая информация требуется для чтения или записи сведений о пользователе (например, с использованием объекта Query
, который преобразуется в реальный запрос на низком уровне IDatabaseReader
или IDatabaseWriter
):
// Knows only about structure of the database (e.g. there is a table called 'user')
// Implementation will use IDatabaseWriter to access the database
interface IDatabaseWriter {
void createUser(User newUser);
void updateUser(User user);
void updateUserEmail(long userKey, Email emailInfo);
void updateUserCredentials(long userKey, Credential userCredentials);
...
}
// Knows only about structure of the database (e.g. there is a table called 'user')
// Implementation will use IDatabaseReader to access the database
interface IUserDatabaseReader {
User readUser(long userKey);
User readUser(string userName);
Email readUserEmail(string userName);
Credential readUserCredentials(long userKey);
...
}
Мы еще не закончили со слоем сохранения. Мы можем ввести еще один интерфейс IUserProvider
. Идея состоит в том, чтобы отделить доступ к базе данных от остальной части нашего приложения. Другими словами, мы объединяем операции запроса данных, связанные с пользователем, в этот класс. Таким образом, IUserProvider
будет единственным типом, имеющим прямой доступ к слою данных. Он формирует интерфейс к постоянному уровню приложения:
interface IUserProvider {
User getUser(string userName);
void saveUser(User user);
User createUser(string userName, Email email);
Email getUserEmail(string userName);
}
Реализация IUserProvider
. Единственный класс во всем приложении, который имеет прямой доступ к уровню данных, ссылаясь на IUserDatabaseReader
и IUserDatabaseWriter
. Он объединяет чтение и запись данных, чтобы сделать обработку данных более удобной. В обязанности этого типа входит предоставление пользовательских данных приложению:
class UserProvider {
IUserDatabaseReader userReader;
IUserDatabaseWriter userWriter;
// Constructor
public UserProvider (IUserDatabaseReader userReader,
IUserDatabaseWriter userWriter) {
this.userReader = userReader;
this.userWriter = userWriter;
}
public User getUser(string userName) {
return this.userReader.readUser(username);
}
public void saveUser(User user) {
return this.userWriter.updateUser(user);
}
public User createUser(string userName, Email email) {
User newUser = new User(userName, email);
this.userWriter.createUser(newUser);
return newUser;
}
public Email getUserEmail(string userName) {
return this.userReader.readUserEmail(userName);
}
}
Теперь, когда мы занялись операциями с базой данных, мы можем сосредоточиться на процессе аутентификации и продолжить извлечение логики аутентификации из UserService
, добавив новый интерфейс IAuthentication
:
interface IAuthentication {
void logIn(User user)
void logOut(User);
void registerUser(UserRegistrationRequest registrationData);
}
Реализация IAuthentication
реализует специальную процедуру аутентификации:
class EmailAuthentication implements IAuthentication {
EmailService emailService;
IUserProvider userProvider;
// Constructor
public EmailAuthentication (IUserProvider userProvider,
EmailService emailService) {
this.userProvider = userProvider;
this.emailService = emailService;
}
public void logIn(string userName) {
Email userEmail = this.userProvider.getUserEmail(userName);
this.emailService.sendVerificationEmail(userEmail);
}
public void logOut(User user) {
// logout
}
public void registerUser(UserRegistrationRequest registrationData) {
this.userProvider.createNewUser(registrationData.getUserName, registrationData.getEmail());
this.emailService.sendVerificationEmail(registrationData.getEmail());
}
}
Чтобы отделить EmailService
от класса EmailAuthentication
, мы можем удалить зависимость от UserRegistrationRequest
, позволив sendVerificationEmail()
вместо этого принимать объект параметра Email`:
class EmailService {
void sendVerificationEmail(Email userEmail) {
email.setToAddress(userEmail.getEmailId());
email.setContent("Hey User, this is your OTP + Random.newRandom(100000));
email.send();
}
Поскольку аутентификация определяется интерфейсом IAuthentication
, вы можете создать новую реализацию в любое время, когда решите использовать другую процедуру (например, WindowsAuthentication
), но без изменения существующего кода. Это также будет работать с IDatabaseReader
и IDatabaseWriter
, если вы решите переключиться на другую базу данных (например, Sqlite). Реализации IUserDatabaseReader
и IUserDatabaseWriter
будут по-прежнему работать без каких-либо изменений.
С таким дизайном класса у вас теперь есть ровно одна причина для изменения каждого существующего типа:
EmailService
когда вам нужно изменить реализацию (например, использовать другой API электронной почты)
IUserDatabaseReader
или IUserDatabaseWriter
, если вы хотите добавить дополнительные операции чтения или записи, связанные с пользователем (например, для обработки роли пользователя)
- предоставить новые реализации
IDatabaseReader
или IDatabaseWriter
, когда вы хотите переключить базовую базу данных или вам нужно изменить доступ к базе данных
- реализации
IAuthentication
при изменении процедуры (например, с использованием встроенной аутентификации ОС)
Теперь все четко разделено. Аутентификация не смешивается с операциями CRUD. У нас есть дополнительный уровень между приложением и уровнем сохраняемости, чтобы добавить гибкости в отношении базовой системы сохранения. Таким образом, операции CRUD не смешиваются с реальными операциями сохранения.
В качестве подсказки: в будущем вам лучше сначала начать с части мышления (дизайна): что должно делать мое приложение?
- обрабатывать аутентификацию
- обрабатывать пользователей
- обрабатывать базу данных
- обрабатывать электронную почту
- создавать ответы пользователей
- показывать страницы просмотра пользователю
- и т.п.
Как видите, вы можете начать реализовывать каждый шаг или требование отдельно. Но это не означает, что каждое требование реализуется ровно одним классом. Как вы помните, мы разделили доступ к базе данных на четыре обязанности или класса: чтение и запись в реальную базу данных (низкий уровень), чтение и запись в уровень абстракции базы данных, чтобы отразить конкретные варианты использования (высокий уровень). Использование интерфейсов добавляет гибкости и тестируемости приложению.
person
BionicCode
schedule
29.06.2019
UserService
использует делегированиеEmailService
). Также вы можете добавить методregisterUser
вEmailService
и снова использовать делегирование вUserService
(это касается только стороны дизайна) - person Traian GEICU   schedule 29.06.2019