SRP(单一责任原则)是否正确应用?

时间:2019-06-13 12:48:09

标签: java design-patterns single-responsibility-principle clean-architecture

我有一个Java类:

class User {
 private String name;
 private String address;
 private int age;
 private BigDecimal salary;
 // other fields
 //getters setters
}

我可以在这些字段中接收新值的映射并进行更新。看起来像这样:ChangeItem changeItem,其中 changeItem.key是字段的名称,而 changeItem.value是字段的值

我创建了用于更新每个字段的策略。例如通用界面:

public interface UpdateStrategy<T> {
    T updateField(T t, ChangeItem changeItem) throws ValidationExceptions;
}

和一些实现:

public class UpdateNameStrategy implements UpdateStrategy<User> {

    private static final Pattern USER_NAME = Pattern.compile(...);

    @Override
    public User updateField(User user, ChangeItem changeItem) throws ValidationExceptions {

        String fieldValue = changeItem.value;

        if (!validate(fieldValue))
            throw new ValidationExceptions(changeItem);

        user.setName(fieldValue);

        return user;
    }

    private boolean validate(String value){
        return USER_NAME.matcher(value).matches();
     }
}

在实际项目中,我有40个字段和每个字段40个策略(具有不同的验证和逻辑)。

我认为此类违反了SRP(单一责任原则)。我将验证逻辑移到单独的类中。我将验证方法更改为:

public class UpdateNameStrategy implements UpdateStrategy<User> {

    @Override
    public User updateField(User user, ChangeItem changeItem) throws ValidationExceptions {

        String fieldValue = changeItem.value;

        ValidateFieldStrategy fieldValidator = new UserNameValidate(fieldValue);

        if (!fieldValidator.validate())
            throw new ValidationExceptions(changeItem);

        return user;
    }
}

public class UserNameValidate implements ValidateFieldStrategy {

    private static final Pattern USER_NAME = Pattern.compile(...);

    private String value;

    public UserNameValidate(String value) {
        this.value = value;
    }

    @Override
    public boolean validate() {
        return USER_NAME.matcher(value).matches();
    }
}

现在我有40个用于更新字段的策略和40个验证器。这是正确的方法吗?或者,也许我可以更清楚地更改此代码?

1 个答案:

答案 0 :(得分:2)

对不起,直视我的眼睛流血。您采用了一个不必要的复杂验证模型,然后将其分为两部分,以使其更加复杂。它与单一责任原则没有多大关系。

在不知道任何特定于您的域问题的情况下,这看起来像是对策略模式的多余使用。

我从来没有见过一个合法的领域问题,它要求对每个字段进行这样的验证策略拆分。

域中的对象不仅是字段的集合。行为也是控制字段(即对象状态)的行为,也是控制该状态的可变性的规则。

通常,我们希望具有行为的丰富对象。而且该行为通常包括验证。

我真诚地怀疑模型中的每个字段都需要对此粒度级别进行验证。将验证放入对象的setter方法中,然后进行验证。

您正在做所有这些精心的设置。我们都想要结构,但是在某些时候,所有这些只是建造非常高的沙堡的仪式。

验证通常是对象的一部分。一个对象是负责任的,它有责任管理其状态,其拥有和控制的字段和值的集合。

单一责任原则并不意味着从对象中提取验证字段的责任。这种责任是客体固有的。

“单一责任原则”涉及“外部”责任,即对象向使用该对象的人提供单个连贯功能(或一组连贯功能)的责任。

考虑一个Printer对象。该对象负责打印。例如,它不负责管理打印机和用户之间的网络连接。

SRP不仅限于类,还包括程序包和模块。数学模块显然应该为您提供数学例程。它不应该提供用于文件系统操作的例程,对吧?

这就是SRP的目的。您正在做的是从对象中提取验证行为,而该行为与SRP几乎没有关系。

有时候,您可能希望提取出通用的验证例程(检查字符串是黑色还是null,或者数字是否为自然数。)

所以您可能会有这样的课程:

public class User {
    // some fields, blah blah

    public void setName(final String aName){
        if( aName == null || a.aName.trim().length() < 1){
            throw new SomeException("empty string blah blah");
        }
        this.name=aName.trim(); // model requires this to be trimmed.
    }

    public void setRawField(final String aValue){
        if( aName == null || a.aName.trim().length() < 1){
            throw new SomeException("empty string blah blah");
        }
        this.rawField=aValue;  // model requires this to not be trimmed.
    }

public void setRawField2(final String aValue){
    // model requires this field to be non-null,
    // can be blank, and if not blank, must be all lower case.
    if(aValue == null) {
        throw new NullPointerException("null string blah blah");
    }
    this.rawField2=aValue.toLowerCase();

}

已更改为将细节委托给外部验证实用程序类或模块的类。

public class User {
     // some fields, blah blah

     public void setName(final String aName){
         // model requires this to be trimmed
         this.name=Validator.notEmptyOrDie(aName).trim();
     }

     public void setRawField(final String  aValue){
         // model requires this to *not* be trimmed
         this.rawField=Validator.notEmptyOrDie(aValue);
     }

public void setRawField2(final String aValue){
    // model requires this field to be non-null,
    // can be blank, and if not blank, must be all lower case.
    // too contrive to refactor, leave it here.
    if(aValue == null) {
        throw new NullPointerException("null string blah blah");
    }
    this.rawField2=aValue.toLowerCase();

}

 public class Validator {
     static public String notEmptyOrDie(final String aString){
         if( aString == null || aString.trim().length() < 1){
             throw new SomeException("empty string blah blah");
         }
         return aString;
 }

这是我实际上遵循的一种方法,用于重构常见验证的部分。我排除了细节。

但是核心验证逻辑(如果有的话)仍然保留在对象中。请注意,验证仍然是User类的一部分。提取的全部是细节。

声明验证意图的逻辑(检查黑色还是黑色)仍然保留在User类中。这是班级行为的本质。

在某些模型中,User类可能根本不需要验证。它可能只是一个数据穿梭机,一个POJO。

OTH,在需要它验证其状态的模型中,该状态通常应放在类内部,并且开发人员必须有一个很好的论据来以您在示例代码中所做的方式来解构该逻辑。

SRP没有说明您如何构成对象内部的责任,而只是如何承担该对象使用者的责任。

根据经验,对象字段的验证属于对象内部逻辑。它是对象行为,不变性,先决条件和后继条件的内在条件。

很少会从对象中提取整个验证(除非我们谈论的是由外部程序包序列化和反序列化的POJO,并且通过注释或某种控制配置描述符以声明方式添加验证)。

如果您还有任何疑问,请帮我。不知道我能回答多快,但是我不介意回答问题。

****编辑***

用户@qujck提到了此提议方法中的有效关注点,即不可能区分所有验证异常(因为它们对所有验证异常都使用通用的异常。)

一种可能性(我已经使用过)是具有重载和/或多态验证器:

public class Validator {
    static public String notEmptyOrDie(final String aString){
        return Validator.notEmptyOrDie(aString, null);
    }

    static public String notEmptyOrDie(final String aString, 
                                       final String aFieldName){
        if( aString == null || aString.trim().length() < 1){
            throw new SomeException( 
                (aFieldName==null? "" : aFieldName + " ") 
                + "empty string blah blah");
        }
        return aString;
    }
}

如果使用具有通用构造函数的验证异常层次结构,则可以通过传递所需的异常类,并使用反射来创建要抛出的实例,从而进一步实现这一目标。

我也做到了。实际上,我现在正在针对本身通过网络协议到达另一个系统的EJB层中的常见错误抛出机制进行操作。

但这是我为应对现有系统而要做的事情,而不是如果我有设计选择的情况下要做的事情。而且,它仍然局限于将验证或错误处理重构为其核心元素。

实际的,特定于对象的验证仍然在对象本身的内部/内部。