Ваше чувство абсолютно верно. Я согласен с вами.
Я думаю, что ваша проблема начинается с вашего стиля именования, так как вы, кажется, прекрасно понимаете, что означает SRP . Имена классов, такие как «... Service» или «... Manager», имеют очень смутное значение или семантику. Они описывают более обобщенный контекст или концепцию. Другими словами, класс '... Manager' предлагает вам поместить все внутрь, и он все еще чувствует себя хорошо, потому что это manager .
Когда вы станете более конкретными, пытаясь сосредоточиться на истинных концепциях ваших классов или их обязанностях, вы автоматически найдете более крупные имена с более сильным значением или семантикой. Это действительно поможет вам разделить классы и определить обязанности.
SRP:
Не должно быть более одной причины для изменения определенного модуля.
Вы можете начать с переименования 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 не сочетаются с фактическими операциями сохранения.
В качестве подсказки: в будущем вам лучше начать сначала с размышления (разработки): что должно делать мое приложение?
- обрабатывать аутентификацию
- обрабатывать пользователей
- обрабатывать базу данных
- обрабатывать электронную почту
- создавать ответы пользователей
- показывать страницы просмотра пользователю
- и т. Д.
Как видите, вы можете приступить к реализации каждого шага или требования отдельно.Но это не значит, что каждое требование реализуется одним классом.Как вы помните, мы разделили доступ к базе данных на четыре обязанности или класса: чтение и запись в реальную базу данных (низкий уровень), чтение и запись в слой абстракции базы данных, чтобы отразить конкретные варианты использования (высокий уровень).Использование интерфейсов повышает гибкость и тестируемость приложения.