《设计模式》系列-SOLID设计原则

时间:2022-07-22
本文章向大家介绍《设计模式》系列-SOLID设计原则,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

S、O、L、I、D设面对对象设计和编码的重要原则。当这些原则被使用时,它们会使得程序易于拓展和维护。

简写

全拼

中文翻译

SRP

Single Responsibility Principle

单一责任原则

OCP

Open Closed Principle

开闭原则

LSP

Liskov Substitution Principle

里氏替换原则

ISP

Interface Segregation Principle

接口隔离原则

DIP

Dependency Inversion Principle

依赖倒置原则

单一责任原则(SRP)

什么是单一责任原则?

A class or module should have a single reponsibility

从上面单一责任原则的英文描述,我们可以知道单一责任原则的原理比较简单。那就是:一个类或者模块应该只负责完成一个功能。

也就是说,我们在设计类或者模块的时候,避免设计大而全的类或者模块,要设计细粒度小,功能独立的类和模块。如果,一个类或者模块

包含了两个或者两个以上不相干的功能,我们要尽可能的对其拆分。

举个例子。在电商系统中有订单和用户两个模块,假如把这两个模块放在一个模块中。如果对订单和用户其中一个模块进行修改,那么修改和测试

阶段都要对另外一个没有被修改的模块进行研究和测试,浪费精力和时间。如果把他们单独放在两个模块里,只修改其中一个,我们只有在涉及另一个模块

的时候才去考虑是否被修改和测试。

责任是不是越单一越好?

我们在追求单一责任原则的时候,是不是把类或者模块划分的越细越好呢?答案是否定的。单一责任原则,将相同的功能放在一个类或者模块里,避免不同功能之间的耦合,提高代码的内聚性。但是如果拆分的过细,会适得其反。下面举个例子,说明这种情况。

@Slf4j
public class Token {


    private  String secret="token-secret";
    private Long expiration=604800L;


    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 根据负责生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }


    /**
     * 从token中获取登录用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }


    /**
     * 从token中获取JWT中的负载
     */
    public Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            log.info("JWT格式验证失败:{}", token);
        }
        return claims;
    }

上面这段代码包含两个功能,一个是generateToken生成token,一个是getClaimsFromTokengetUserNameFromToken从token中解析信息。如果按照单一责任原则,我们要将两个功能,分别放在GenerateTokenAnalysisToken这两个类中。虽然这么做将不同的功能职责分离,但是变量secret要被修改,则需要同时修改两个类中的变量,一旦有一个未被修改,那么解析的信息就会不正确。这么做只会降低了代码的内聚性。

开闭原则(OCP)

software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification

上面这段是开闭原则的英文定义,翻译成中文就是:软件实体(模块、类、方法等)应该对拓展开放,对修改关闭。

简单的说,就是在添加一个功能的时候,应该在已有代码的基础上拓展代码(模块、类、方法等),而不是修改已有的代码(模块、类、方法等)。

下面是一段配置解析代码,配置可能来自于Annotation、Xml,其中configMap是配置缓存,annotationParse是Annotation解析器,xmlParse是xml解析器,ConfigType是解析的类型枚举

public class Configuration {

    private Map<String,Object> configMap;
    private AnnotationParse annotationParse;
    private XmlParse xmlParse;

    public Configuration(Map<String, Object> configMap, AnnotationParse annotationParse, XmlParse xmlParse) {
        this.configMap = configMap;
        this.annotationParse = annotationParse;
        this.xmlParse = xmlParse;
    }

    public void parse(String type){

         if (ConfigType.Annotation.toString().equals(type)){
            annotationParse.parse(configMap);
        }else {
            xmlParse.parse(configMap);
        }

    }


    public static void main(String[] args) {
        Configuration configuration=new Configuration(new HashMap<String, Object>(),new AnnotationParse(),new XmlParse());
        configuration.parse("Xml");
    }

}
public enum ConfigType {
    Annotation,Xml
}

上面这段代码比较简单,就是根据不同的类型解析不同的配置信息,然后放到configMap的缓存里。现在我们要在原来基础上增加一个JSON格式的配置功能。主要修改和增加有三个地方,一:在ConfigType枚举中增加Json类型,二:对Configuration的parse方法进行修改,三:怎么一个JsonParse去解析这个格式的数据

public class Configuration {

    private Map<String,Object> configMap;
    private AnnotationParse annotationParse;
    private XmlParse xmlParse;
    private JsonParse jsonParse;//新增

    public Configuration(Map<String, Object> configMap, AnnotationParse annotationParse, XmlParse xmlParse,JsonParse jsonParse) 	{
        this.configMap = configMap;
        this.annotationParse = annotationParse;
        this.xmlParse = xmlParse;
        this.jsonParse=jsonParse;
    }

    public void parse(String type){

        if (ConfigType.Annotation.toString().equals(type)){
            annotationParse.parse(configMap);
        }else if (ConfigType.Json.toString().equals(type)){//修改二
            jsonParse.parse(configMap);
        } else {
            xmlParse.parse(configMap);
        }

    }


    public static void main(String[] args) {
        Configuration configuration=new Configuration(new HashMap<String, Object>(),new AnnotationParse(),new XmlParse(),new JsonParse());
        configuration.parse("Json");
    }

}
public enum ConfigType {
    Annotation,Xml,Json
}

增加了JSON格式的配置功能配置功能代码很简单,但是依然存在很多问题。一方面,Configuration被修改了,那么调用Configuration的方法都要修改,另一方面,Configuration中的parse被修改,那么整个parse相关的功能都要被测试,增加了测试量。

上面的代码基于修改的方式,新增了Json格式的配置功能。那么如何按照开闭原则去实现这个功能呢?

首先,我们要重构代码,增加一个接口去定义parse方法

public interface ParseHandler {
    void parse(Map<String,Object> configMap,String type);
}

然后,让其他解析类去实现这个接口

public class AnnotationParse implements ParseHandler {
    
    public void parse(Map<String, Object> configMap,String type) {
        if (ConfigType.Annotation.toString().equals(type)){
            
        }
    }
}

最后重构Configuration

public class Configuration {

    private Map<String,Object> configMap;

    private List<ParseHandler> parseHandlers=new ArrayList<ParseHandler>();

    public Configuration(Map<String, Object> configMap) {
        this.configMap = configMap;
    }

    public void addparseHandlers(ParseHandler parseHandler){//注册接口
       this.parseHandlers.add(parseHandler);
    }

    public void parse(String type){
        for (ParseHandler parseHandler : parseHandlers) {
            parseHandler.parse(configMap,type);
        }
    }

    public static void main(String[] args) {
        Configuration configuration=new Configuration(new HashMap<String, Object>());
        configuration.addparseHandlers(new AnnotationParse());
        configuration.addparseHandlers(new XmlParse());//注册具体的解析
        ConfigType[] values = ConfigType.values();
        for (ConfigType value : values) {
            configuration.parse(value.toString());
        }
    }

}

上面是Annotation和Xml配置解析功能重构后的代码,parseHandlers是一个ParseHandler接口的集合的列表,通过addparseHandlers方法注册,parse执行相应的解析方法。下面我们完成对于Json格式的配置功能。

首先,还是再配置枚举增加Json类型

public enum ConfigType {
    Annotation,Xml,Json
}

其次,增加一个JsonParse类实现ParseHandler接口

public class JsonParse implements ParseHandler{

    public void parse(Map<String,Object> configMap,String type){
        if (ConfigType.Json.toString().equals(type)){
          
        }
    }
}

最后,在Configuration里注册JsonParse

public class Configuration {
	//...这些代码没有改动
    public static void main(String[] args) {
        Configuration configuration=new Configuration(new HashMap<String, Object>());
        configuration.addparseHandlers(new AnnotationParse());
        configuration.addparseHandlers(new XmlParse());//注册具体的解析
        ConfigType[] values = ConfigType.values();
        for (ConfigType value : values) {
            configuration.parse(value.toString());
        }
    }
}

从上面重构后代码可以看出,我们新增加一个Json配置功能,只需要去实现Handler接口,然后再注册,并没有改动关于其他两种类型的任何代码,所以根本不会对其产生影响。

上面的代码也做了修改,那么也算是修改原来代码吗?答案是否定的,我们无论是实现接口还是注册方法,都是对代码进行拓展,并不会对原有的功能有任何的影响。如果后续还要怎加其他类型的配置解析,我只需要完成上面两步即可,不会有其他的额外影响。

里氏替换原则(LSP)

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。

上面两条是里氏替换原则的英文定义,第二条是对于第一条的修订,将这两条翻译成中文就是:子类对象(subtype or derived classes)可以将代码中出现任何父类对象替换,并且不会破坏原有的规则和逻辑行为。

从里氏替换原则的定义不看出,它是一种父类对象和子类对象之间的设计约束。里氏替换原则的核心是按照约定规则来设计。父类指定了函数或者方法的约定,那么子类可以各自实现函数或者方法的逻辑,但是不能改变父类指定的约定。这里的约定包括入参、返回值和异常处理。面对对象里的多态就是一种里氏替换原则。

接口隔离原则(ISP)

Clients should not be forced to depend upon interfaces that they do not use

上面是接口隔离原则的英文定义,翻译成中文就是:客户端不应该被强迫依赖它不需要的接口。客户端是接口的使用者。

从名称可以看出,接口隔离原则是一种接口的设计原则,这里的接口并不是只是单单指面对对象的接口,还包括了单个或者多个的函数方法、http请求接口等等。

我们在设计接口的时候尽量让接口只做本身的事件,不要把额外的逻辑事件附加给接口的调用者。这一点和单一责任原则很像,但是他们俩并不是完全相同,单一责任原则针对的是模块、类和接口的设计原则,而接口隔离原则更加侧重于接口的设计。如果接口使用者只是调用了接口的部分功能,那么就不符合单一责任原则了。

首先,创建一个HotUpdate接口,设置update热加载的约定

public interface HotUpdate {
    void update();
}

其次,让JsonParseXmlParse实现HotUpdate接口

public class XmlParse implements ParseHandler,HotUpdate{

    public void parse(Map<String,Object> configMap,String type){
        if (ConfigType.Xml.toString().equals(type)){
            System.out.println("xml");
        }
    }

    public void update() {
        //热加载
    }
}

最后使用定时任务,去执行热加载

public class Configuration {

    private Map<String,Object> configMap;

    private List<ParseHandler> parseHandlers=new ArrayList<ParseHandler>();
    private List<HotUpdate> hotUpdates=new ArrayList<HotUpdate>();

    public Configuration(Map<String, Object> configMap) {
        this.configMap = configMap;
    }

    public void addparseHandlers(ParseHandler parseHandler){//注册接口
       this.parseHandlers.add(parseHandler);
    }

    public void addhotUpdates(HotUpdate hotUpdate){
        this.hotUpdates.add(hotUpdate);
    }

    //定时任务热加载
    public void ScheduledUpdater(){
        for (HotUpdate hotUpdate : hotUpdates) {
            hotUpdate.update();
        }
    }


    public void parse(String type){
        for (ParseHandler parseHandler : parseHandlers) {
            parseHandler.parse(configMap,type);
        }
    }


    public static void main(String[] args) {
        Configuration configuration=new Configuration(new HashMap<String, Object>());
        configuration.addparseHandlers(new AnnotationParse());
        configuration.addparseHandlers(new XmlParse());
        configuration.addparseHandlers(new JsonParse());//注册JsonParse
        ConfigType[] values = ConfigType.values();
        for (ConfigType value : values) {
            configuration.parse(value.toString());
        }
    }

}

我们在上面解析配置文件功能的基础上增加一个对于xml和Json解析方式进行热加载的功能,那么这个功能如果去实现呢?首先,定义一个热加载接口Update,让xmlParse类和JsonParse类去实现接口,分别实现各自的热加载逻辑。其次,利用定时任务去执行update热加载方法。

从上面的例子可以看出,只有利用了xml和Json格式解析配置的时候才会去执行热加载方法,而利用了注解形式去解析配置文件的时候不会去执行热加载方法,减小了负担,完全符合接口隔离原则。

依赖倒置原则(DIP)

High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractiojavans. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.

上面是依赖倒置原则的英文原始定义,翻译成中文就是:高层模块不应该依赖于低层模块。两个模块之间应该通过抽象依赖。除此之外,抽象类不应该依赖具体的实现细节。细节依赖于抽象类。

高层模块和低层模块简单的将就是调用者和被调用者的关系。从这定义我们就可以看出依赖倒置原则是架构层面的设计原则。比如web应用的tomcat容器之间的关系,tomcat就是高层模块,而web应用则是低层模块。两者都依赖于一个Servlet规范(一种抽象的约定),servlet不依赖于tomcat和web应用的具体实现细节,但是它们又都要依照servlet的规范去实现细节。

结束语

这些设计原则的原理的并不复杂,我们在使用的时候尽可能的活学活用,不要教条主义。在代码开发和设计的初期阶段,要尽可能的将设计原则考虑在内,降低每次功能变动,都要重构代码的风险。

这是一篇设计模式的学习文章。如果有什么不对的地方,希望大家可以评论斧正,一起在编程之路上徐徐前行。