当我们将职责分为不同的类别时,尝试了解SRP

时间:2019-06-29 11:07:07

标签: java oop design-patterns solid-principles single-responsibility-principle

我正在尝试了解SRP原理,并且大多数sof线程都无法回答我遇到的这个特定查询,

用例

每当他尝试在网站中注册/创建用户帐户时,我都会尝试向用户的电子邮件地址发送电子邮件以进行验证。

没有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();
    }
}

上述“ UserService”类违反了SRP规则,因为我们将“ UserService” CRUD操作组合在一起,并将验证电子邮件代码触发为一个单一类。

因此,

使用SRP

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()合并为一个类吗?

2 个答案:

答案 0 :(得分:3)

您的感觉绝对正确。我同意你的看法。

我认为您的问题始于您的命名风格,因为您似乎很了解 SRP 的含义。诸如“ ...服务”或“ ...管理器”之类的名称具有非常模糊的含义或语义。他们描述了一个更笼统的上下文或概念。换句话说,“ ... Manager”类邀请您将所有内容放进去,但感觉仍然不错,因为它是 manager

当您通过专注于类的真实概念或其职责而变得更加具体时,您会自动找到具有更强含义或语义的较大名称。这确实可以帮助您划分班级并确定职责。

SRP:

  

更改某个模块永远不会有一个以上的原因。

您可以先将UserService重命名为UserDatabaseContext。现在,这将自动迫使您只将与数据库相关的操作放入此类(例如CRUD操作)。

您甚至可以在这里获得更具体的信息。您正在使用数据库做什么?您从读取并写入。显然有两个职责,这意味着两个类:一个负责读操作,另一个负责写操作。这可能是非常通用的类,它们只能读取或写入任何内容。我们称它们为DatabaseReaderDatabaseWriter,由于我们试图使所有事物脱钩,因此我们将在各处使用接口。这样,我们得到了两个IDatabaseReaderIDatabaseWriter接口。这种类型的级别很低,因为它们知道数据库(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对象,该对象由低级IDatabaseReaderIDatabaseWriter转换为真实查询):

// 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的实现。整个应用程序中唯一可通过引用IUserDatabaseReaderIUserDatabaseWriter直接访问数据层的类。它包装了数据的读写,使数据处理更加方便。这种类型的责任是向应用程序提供用户数据:

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);
  }
}

现在我们解决了数据库操作,我们可以专注于身份验证过程,并通过添加新接口UserServiceIAuthentication继续提取身份验证逻辑:

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());    
  }
}

要将EmailServiceEmailAuthentication类中分离出来,我们可以通过让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),此方法也将与IDatabaseReaderIDatabaseWriter一起使用。 IUserDatabaseReaderIUserDatabaseWriter实现仍然可以正常工作,无需进行任何修改。

使用这种类设计,您现在就有一个理由来修改每个现有类型:

  • EmailService,当您需要更改实现(例如,使用 不同的电子邮件API)
  • IUserDatabaseReaderIUserDatabaseWriter,当您想添加其他与用户相关的读写操作(例如,处理用户角色)时
  • 当您要切换基础数据库或需要修改数据库访问权限时,提供IDatabaseReaderIDatabaseWriter的新实现
  • 过程更改时IAuthentication的实现(例如,使用内置OS身份验证)

现在,一切都清晰地分开了。身份验证不与CRUD操作混在一起。我们在应用程序和持久性层之间有一个附加层,以增加有关基础持久性系统的灵活性。因此,CRUD操作不会与实际的持久性操作混合在一起。

提示:将来,您最好首先考虑一下(设计)部分:我的应用程序必须做什么?

  • 处理身份验证
  • 处理用户
  • 处理数据库
  • 处理电子邮件
  • 创建用户回复
  • 向用户显示视图页面

如您所见,您可以开始分别实施每个步骤或要求。但这并不意味着每个需求都只能由一个类来实现。您还记得,我们将数据库访问分为四个职责或类:对真实数据库的读写(底层),对数据库抽象层的读写,以反映具体的用例(高层)。使用界面可为应用程序增加灵活性和可测试性。

答案 1 :(得分:1)

@BionicCode已经为这个问题提供了一个很好的答案。我只是不想添加简短的摘要和对此事的一些想法。

SRP可能很棘手。

根据我的经验,您放置在系统中的 职责粒度 摘要数量 会影响它的易用性和大小。

您可以添加大量抽象并将所有内容分解为非常小的组件。这确实是我们应该争取的东西。

现在的问题是: 何时停止?

这取决于:

  • 您的应用程序大小
  • 其中哪些部分将比其他部分更频繁地
  • 您是否需要将对象组合在一起,或者大多数情况下您的模块彼此独立,并且没有引起太多对象。
  • 你几点钟了
  • 您的团队有多大
  • 很多其他东西...

让我们从团队的规模开始。

我们将代码分解为单独的模块,将类分解为单独的文件的原因之一是,我们可以组成团队,并避免在我们喜欢的源代码控制系统中进行过多的合并。如果您需要更改包含系统组件的文件,而其他人也需要对其进行更改,那么这可能很快就会变得很丑陋。现在,如果您使用SRP进行单独的模块处理,则会得到更多但更小的模块,大多数情况下,它们将彼此独立地更改。

如果团队没有那么大,我们的模块也没有那么大怎么办?您是否需要生成更多这些内容?

这是一个例子。

假设您有一个具有设置的移动应用程序。我们可以说将这些settigns包含在一项职责中,然后将其添加到一个接口IApplicationSettings中以容纳所有这些settigns。

在我们有30种设置的情况下,该界面将非常庞大,这很糟糕。这也意味着我们可能再次违反了SRP,因为此界面可能包含多个不同类别的设置。

因此,我们决定应用接口隔离原则 SRP ,并将设置分为多个接口ISomeCategorySettingsIAnotherCategorySettings等。

现在让我们说我们的应用程序还不太大(但是),我们有5种设置。即使它们来自不同的类别,将这些设置保留在一个界面中也很不好吗?

我想说,将所有settigns放在一个界面中是可以的,只要它不会开始减慢我们的速度或开始变得丑陋(30个或更多settigns!)。

构造电子邮件并从您的service对象发送电子邮件是否很糟糕?确实,这很快就会变得很丑陋,因此您最好将此责任从service对象转移到EmailSender对象上。

如果您有一个包含5个方法的service对象,您是否真的需要针对每个操作将其分解为5个不同的对象?如果这些方法很大,可以。如果它们很小,将它们放在一个物体中,那就是个大问题。

SRP很好,但是要考虑粒度,并根据代码大小,团队大小等来明智地选择它。