我正在尝试了解SRP原理,并且大多数sof线程都无法回答我遇到的这个特定查询,
每当他尝试在网站中注册/创建用户帐户时,我都会尝试向用户的电子邮件地址发送电子邮件以进行验证。
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();
}
}
上述“ UserService”类违反了SRP规则,因为我们将“ UserService” CRUD操作组合在一起,并将验证电子邮件代码触发为一个单一类。
因此,
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();
}
但是即使是'with SRP',UserService作为一个类也再次具有sendVerificationEmail()的行为,尽管这次它并不具有发送电子邮件的全部逻辑。
即使在应用了SRP之后,我们还是将原始操作合并为一个类,并将sendVerificationEmail()合并为一个类吗?
答案 0 :(得分:3)
您的感觉绝对正确。我同意你的看法。
我认为您的问题始于您的命名风格,因为您似乎很了解 SRP 的含义。诸如“ ...服务”或“ ...管理器”之类的名称具有非常模糊的含义或语义。他们描述了一个更笼统的上下文或概念。换句话说,“ ... 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
代替Email参数对象来删除对sendVerificationEmail()
的依赖:
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
)时随时创建新的实现,而无需修改现有代码。一旦您决定切换到其他数据库(例如Sqlite),此方法也将与IDatabaseReader
和IDatabaseWriter
一起使用。 IUserDatabaseReader
和IUserDatabaseWriter
实现仍然可以正常工作,无需进行任何修改。
使用这种类设计,您现在就有一个理由来修改每个现有类型:
EmailService
,当您需要更改实现(例如,使用
不同的电子邮件API)IUserDatabaseReader
或IUserDatabaseWriter
,当您想添加其他与用户相关的读写操作(例如,处理用户角色)时IDatabaseReader
或IDatabaseWriter
的新实现IAuthentication
的实现(例如,使用内置OS身份验证)现在,一切都清晰地分开了。身份验证不与CRUD操作混在一起。我们在应用程序和持久性层之间有一个附加层,以增加有关基础持久性系统的灵活性。因此,CRUD操作不会与实际的持久性操作混合在一起。
提示:将来,您最好首先考虑一下(设计)部分:我的应用程序必须做什么?
如您所见,您可以开始分别实施每个步骤或要求。但这并不意味着每个需求都只能由一个类来实现。您还记得,我们将数据库访问分为四个职责或类:对真实数据库的读写(底层),对数据库抽象层的读写,以反映具体的用例(高层)。使用界面可为应用程序增加灵活性和可测试性。
答案 1 :(得分:1)
@BionicCode已经为这个问题提供了一个很好的答案。我只是不想添加简短的摘要和对此事的一些想法。
SRP可能很棘手。
根据我的经验,您放置在系统中的 职责粒度 和 摘要数量 会影响它的易用性和大小。
您可以添加大量抽象并将所有内容分解为非常小的组件。这确实是我们应该争取的东西。
现在的问题是: 何时停止?
这取决于:
让我们从团队的规模开始。
我们将代码分解为单独的模块,将类分解为单独的文件的原因之一是,我们可以组成团队,并避免在我们喜欢的源代码控制系统中进行过多的合并。如果您需要更改包含系统组件的文件,而其他人也需要对其进行更改,那么这可能很快就会变得很丑陋。现在,如果您使用SRP进行单独的模块处理,则会得到更多但更小的模块,大多数情况下,它们将彼此独立地更改。
如果团队没有那么大,我们的模块也没有那么大怎么办?您是否需要生成更多这些内容?
这是一个例子。
假设您有一个具有设置的移动应用程序。我们可以说将这些settigns包含在一项职责中,然后将其添加到一个接口IApplicationSettings
中以容纳所有这些settigns。
在我们有30种设置的情况下,该界面将非常庞大,这很糟糕。这也意味着我们可能再次违反了SRP,因为此界面可能包含多个不同类别的设置。
因此,我们决定应用接口隔离原则和 SRP ,并将设置分为多个接口ISomeCategorySettings
,IAnotherCategorySettings
等。
现在让我们说我们的应用程序还不太大(但是),我们有5种设置。即使它们来自不同的类别,将这些设置保留在一个界面中也很不好吗?
我想说,将所有settigns放在一个界面中是可以的,只要它不会开始减慢我们的速度或开始变得丑陋(30个或更多settigns!)。
构造电子邮件并从您的service
对象发送电子邮件是否很糟糕?确实,这很快就会变得很丑陋,因此您最好将此责任从service
对象转移到EmailSender
对象上。
如果您有一个包含5个方法的service
对象,您是否真的需要针对每个操作将其分解为5个不同的对象?如果这些方法很大,可以。如果它们很小,将它们放在一个物体中,那就是个大问题。
SRP很好,但是要考虑粒度,并根据代码大小,团队大小等来明智地选择它。