Man*_*DIP 5 java oop design-patterns single-responsibility-principle solid-principles
我试图了解 SRP 原理,但大多数软线程没有回答我遇到的这个特定查询,
每当用户尝试在网站中注册/创建用户帐户时,我都会尝试向用户的电子邮件地址发送一封电子邮件来验证自己。
class UserRegistrationRequest {
String name;
String emailId;
}
class UserService {
Email email;
boolean registerUser(UserRegistrationRequest req) {
//store req data in database
sendVerificationEmail(req);
return true;
}
//Assume UserService class also has other CRUD operation methods()
void sendVerificationEmail(UserRegistrationRequest req) {
email.setToAddress(req.getEmailId());
email.setContent("Hey User, this is your OTP + Random.newRandom(100000));
email.send();
}
}
Run Code Online (Sandbox Code Playgroud)
上述类“UserService”违反了 SRP 规则,因为我们将“UserService”CRUD 操作和触发验证电子邮件代码合并到 1 个类中。
因此我这样做,
class UserService {
EmailService emailService;
boolean registerUser(UserRegistrationRequest req) {
//store req data in database
sendVerificationEmail(req);
return true;
}
//Assume UserService class also has other CRUD operation methods()
void sendVerificationEmail(UserRegistrationRequest req) {
emailService.sendVerificationEmail(req);
}
}
class EmailService {
void sendVerificationEmail(UserRegistrationRequest req) {
email.setToAddress(req.getEmailId());
email.setContent("Hey User, this is your OTP + Random.newRandom(100000));
email.send();
}
Run Code Online (Sandbox Code Playgroud)
但即使“使用 SRP”,UserService 作为一个类也再次保留了 sendVerificationEmail() 的行为,尽管这次它没有保留发送电子邮件的整个逻辑。
即使在应用 SRP 之后,我们不是又将 CRUD 操作和 sendVerificationEmail() 合并到 1 个类中吗?
你的感觉绝对正确。我同意你的看法。
我认为你的问题始于你的命名风格,因为你似乎很清楚SRP 的含义。像“...Service”或“...Manager”这样的类名称具有非常模糊的含义或语义。它们描述了更普遍的背景或概念。换句话说,“...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);
...
}
Run Code Online (Sandbox Code Playgroud)
最重要的是,您可以实现更专业的读写操作层,例如用户相关数据。我们将引入一个IUserDatabaseReader和一个IUserDatabaseWriter接口。该接口不知道如何连接到数据库或使用什么类型的数据库。该接口只知道读取或写入用户详细信息需要哪些信息(例如,使用由Query低级IDatabaseReader或转换为真实查询的对象IDatabaseWriter):
// Knows only about structure of the database (e.g. there is a table called 'user')
// Implementation will internally use IDatabaseWriter to access the database
interface IUserDatabaseWriter {
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 internally use IDatabaseReader to access the database
interface IUserDatabaseReader {
User readUser(long userKey);
User readUser(string userName);
Email readUserEmail(string userName);
Credential readUserCredentials(long userKey);
...
}
Run Code Online (Sandbox Code Playgroud)
我们还没有完成持久层。我们可以引入另一个接口IUserProvider。这个想法是将数据库访问与应用程序的其余部分分离。换句话说,我们将用户相关的数据查询操作合并到这个类中。因此,IUserProvider它将是唯一可以直接访问数据层的类型。它形成了应用程序持久层的接口:
interface IUserProvider {
User getUser(string userName);
void saveUser(User user);
User createUser(string userName, Email email);
Email getUserEmail(string userName);
}
Run Code Online (Sandbox Code Playgroud)
实施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);
}
}
Run Code Online (Sandbox Code Playgroud)
UserService现在我们已经解决了数据库操作,我们可以专注于身份验证过程,并通过添加新接口继续从中提取身份验证逻辑IAuthentication:
interface IAuthentication {
void logIn(User user)
void logOut(User);
void registerUser(UserRegistrationRequest registrationData);
}
Run Code Online (Sandbox Code Playgroud)
的实现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());
}
}
Run Code Online (Sandbox Code Playgroud)
EmailService为了与类解耦,我们可以通过使用 Email` 参数对象EmailAuthentication来删除对的依赖:UserRegistrationRequestsendVerificationEmail()
class EmailService {
void sendVerificationEmail(Email userEmail) {
email.setToAddress(userEmail.getEmailId());
email.setContent("Hey User, this is your OTP + Random.newRandom(100000));
email.send();
}
Run Code Online (Sandbox Code Playgroud)
由于身份验证是由接口定义的IAuthentication,因此当您决定使用不同的过程(例如)时,您可以随时创建新的实现WindowsAuthentication,而无需修改现有代码。IDatabaseReader一旦IDatabaseWriter您决定切换到不同的数据库(例如 Sqlite),这也将适用。和实现仍然可以工作,无需任何修改IUserDatabaseReader。IUserDatabaseWriter
通过这种类设计,您现在有一个理由修改每个现有类型:
EmailService当您需要更改实现时(例如使用不同的电子邮件 API)IUserDatabaseReader或者IUserDatabaseWriter当您想要添加其他与用户相关的读取或写入操作时(例如处理用户角色)IDatabaseReader或的新实现IDatabaseWriterIAuthentication程序更改时的实现(例如使用内置操作系统身份验证)现在一切都干净地分开了。身份验证不与 CRUD 操作混合。我们在应用程序和持久层之间有一个附加层,以增加底层持久系统的灵活性。因此 CRUD 操作不会与实际的持久性操作混合在一起。
提示:将来您最好首先从思考(设计)部分开始:我的应用程序必须做什么?
正如您所看到的,您可以开始单独实施每个步骤或要求。但这并不意味着每个需求都由一个类来实现。您还记得,我们将数据库访问分为四个职责或类:读取和写入真实数据库(低级)、读取和写入数据库抽象层,以反映具体用例(高级)。使用接口可以增加应用程序的灵活性和可测试性。