设计原则硬核干货_不满足单一职责原则-程序员宅基地

技术标签: 算法  架构  java  设计模式  软件框架  

文章有点长,推荐先进行收藏!
精心编写,在迷茫的时候可以反复观看!

一篇文章帮你拿下设计模式的核心:设计原则,万字长文

以下所有的原则,都不能脱离应用场景!!

不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。

一、SOLID五大原则

1、单一职责原则(SRP - Single Resposibility Princple)

简单清晰的定义:一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。

如何判断一个类的职责是否足够单一?

举个例子:

public class UserInfo {
    
    private long userId;
    private String username;
    private String email;
    private String telephone;
    private long createTime;
    private long lastLoginTime;
    private String avatarUrl;
    private String provinceOfAddress; // 省
    private String cityOfAddress; // 市
    private String regionOfAddress; // 区
    private String detailedAddress; // 详细地址
    // ... 省略其他属性和方法...
}

对于这个类的设计,有两种观点:

  • 第一种:如果我们从“用户”这个业务层面来看,UserInfo 包含 的信息都属于用户,满足职责单一原则。

  • 第二种:如果我们从更加细分的“用户展示信息”“地址信 息”“登录认证信息”等等这些更细粒度的业务层面来看,那 UserInfo 就应该继续拆分。

综上所述,评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软件开发中, 我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需 求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度类。这就是所谓的持续重构(文章后面部分将会具体提及)

对于单一职责原则的定义,我们不好判断是否满足条件。

所以我们可以从以下几条具体的指导原则来进行设计:

  • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
  • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
  • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
  • 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。

总结 + 提问

1. 如何理解单一职责原则(SRP)?

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

2. 如何判断类的职责是否足够单一?

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

  • 类中的代码行数、函数或者属性过多;
  • 类依赖的其他类过多,或者依赖类的其他类过多;
  • 私有方法过多;
  • 比较难给类起一个合适的名字;
  • 类中大量的方法都是集中操作类中的某几个属性。

3. 类的职责是否设计得越单一越好?

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

2、开闭原则(OCP - Open Closed Principle)

简单清晰的定义:对扩展开放,对修改关闭

稍微详细一点的解读:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

修改代码就意味着违背开闭原则吗?

我们再一块回忆一下开闭原则的定义:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。从定义中,我们可以看出,开闭原则可以应用在不同粒度的代码中,可以是模块,也可以类,还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,被认定为“修改”,在细代码粒度下,又可以被认定为“扩展”。比如,在一个类中添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,所以在方法(及其属性)这一层面,它又可以被认定为“扩展”。

如何做到对扩展开放、修改关闭?

实际上,开闭原则讲的就是代码的扩展性问题,是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化的时候,能够做到“对扩展开放、对修改关闭”,那就说明这段代码的扩展性比较好。所以,问如何才能做到“对扩展开放、对修改关闭”,也就粗略地等同于在问,如何才能写出扩展性好的代码。

这里要先明确一个指导思想:

  • 在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。
  • 其次,在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。

在众多的设计原则、思想、模式中,最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)。

如何在项目中灵活应用开闭原则?

前面我们提到,写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点。那问题是如何才能识别出所有可能的扩展点呢?

  • 如果你开发的是一个业务导向的系统,比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。
  • 如果你开发的是跟业务无关的、通用的、偏底层的系统,比如,框架、组件、类库,你需要了解“它们会被如何使用?今后你打算添加哪些功能?使用者未来会有哪些更多的功能需求?”等问题。

有一句话说得好,“唯一不变的只有变化本身”。即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点,即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。我们没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计。

针对这个情况我们要在合理的控制范围能进行一定的设计,并根据上面提到过的观点,针对后续的业务需求变更之后,再对代码的设计进行持续重构

总结 + 问题

1. 如何理解“对扩展开放、对修改关闭”?

添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

2. 如何做到“对扩展开放、修改关闭”?

我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。

很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为 指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编 程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。

3、里氏替换原则 (LSP - Liskov Substitution Principle)

简单清晰的定义:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

核心思维:子类完美继承父类的设计初衷,并做了增强

举个简单的例子来说明一下:

public class LiskovSubstitutionPrinciple {
    
    public static void main(String[] args) throws Exception {
    
        // Human student = new Student();
        Human human = new Human();
        Student student = new Student();
        human.eat("bread");
        human.eat("d");
        student.eat("d");
    }
}

class Human {
    
    public void eat(String things) throws Exception {
    
        System.out.println("I am eating " + things);
    }
}

class Student extends Human{
    
    @Override
    public void eat(String things) throws Exception {
    
        if (things.length() < 2) {
    
            throw new Exception("You are eating shit ?");
        }
        System.out.println("I am eating " + things);
    }
}

运行结果十分清晰明了:

I am eating bread
I am eating d
Exception in thread "main" java.lang.Exception: You are eating shit ?
	at designpattern.solid.Student.eat(LiskovSubstitutionPrinciple.java:23)
	at designpattern.solid.LiskovSubstitutionPrinciple.main(LiskovSubstitutionPrinciple.java:9)

上述现象是完全不满足里氏替换原则的,同一个函数在父类和子类声明的对象中调用获得了我们不想得到的结果。

更细致的解释一下:子类在设计的时候,应当遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。**这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。**实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。

总结

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数 的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至 包括注释中所罗列的任何特殊说明。

理解这个原则,我们还要弄明白里式替换原则跟多态的区别。虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

4、接口隔离原则(ISP - Interface Segregation Principle)

接口隔离中对接口的三种理解:一组 API 接口集合、单个 API 接口、函数 OOP 中的接口概念。下面对这三种理解分别进行详细的阐述。

  • 一组 API 接口集合

举个例子:

class User {
    
    private int id;
    private String username;
    private String password;
    private String phone;
}

interface UserService {
    
    void login(String username, String password);
    void register(User user);
    User getUserById(int id);
    User deleteUserById(int id);
    User deleteUserByPhone(String phone);
}

根据代码,我们可以看出来,UserService接口中罗列了几个看起来没什么问题的方法。不过在实际生产需求之中,接口中的deleteUserById方法和deleteUserByPhone方法如果暴露给所有实现类,可能会因为接口全部暴露而导致误操作,而产生不必要的麻烦,这个时候我们根据接口隔离原则可以设计成如下的方式:

interface UserService {
    
    void login(String username, String password);
    void register(User user);
    User getUserById(int id);
}

interface RestrictUserService {
    
    User deleteUserById(int id);
    User deleteUserByPhone(String phone);
}

这样,就很明朗了。讲不同级别的接口进行隔离开来,在需要实现的时候,再进行分别的引入。这样就可以使调用者可以根据需要进行引入,而不必强迫的实现不需要使用到的接口。

  • 单个 API 接口或函数
class DataInfo {
    
    private long sum;
    private long avg;
    private long max;
    private long min;
		// 省略 getter,setter
}

class DataHandler {
    
    
    public DataInfo DataCalc() {
    
        DataInfo dataInfo = new DataInfo();
        // 省略庞大的计算逻辑
        dataInfo.setMax(...);
        dataInfo.setMin(...);
        dataInfo.setSum(...);
        dataInfo.setAvg(...);
        return dataInfo;
    }
}

此时我们如果发现如果这个数据计算类这么设计,会使得整个方法十分的臃肿;如果我们讲各参数的计算分开设计,如下

interface DataCalc {
    
    public long calcSum();
    public long calcAvg();
    public long calcMax();
    public long calcMin();
}

class DataHandler implements DataCalc{
    
    
    public DataInfo DataCalc() {
    
        DataInfo dataInfo = new DataInfo();
	      dataInfo.setSum(calcSum());
      	// 省略其他参数的计算
        return dataInfo;
    }
		// 分别重写实现方法,将具体业务逻辑分离开
    @Override
    public long calcSum() {
    
      	// 计算 calcSum 的逻辑代码
        return 0;
    }
		// 这里省略其他的重写实现
}

不难发现,我们的代码优美了不少,而且今后在遇到业务逻辑需要变更的时候,可以更加清晰的对代码进行重构

  • OOP 中的接口概念

假设我们的项目中用到了三个外部系统:Redis、MySQL、Kafka。每个系统都对应一系列 配置信息,比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,供项目中 的其他模块来使用,我们分别设计实现了三个 Configuration 类:RedisConfig、MysqlConfig、KafkaConfig。每个类都有一些共有的一些功能比如update等,不过他们的逻辑实现分别不同。这个时候我们可以将他们共有的方法进行向上抽取出来,因为是OOP中的接口,我们之前也有提及,这里就不再进行过多的阐述了。

问题

接口隔离原则与单一职责原则的区别

单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

5、依赖倒置原则(DIP - Dependence Inversion Principle)

原文定义:高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

百度定义;依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

依赖倒置的核心思想是面向接口编程

依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类。
使用接口或抽象类的目的是:制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

举一个生动形象的例子:

// 上层模块依赖下层实现
class UploadFile {
       
    public void uploadFileToBaiduyun() {
    
        // TODO 上传至百度云实现逻辑
    }
}

大家应该都写过这样的案例,但是这样的实现方式对吗?这样的实现方式不是错误的,但是如果现在业务需求变更了,我们将上传至百度云更换为上传至阿里云了,这样子我们将要修改uploadfile类内的方法,如下:

// 上层模块依赖下层实现
class UploadFile {
       
//    public void uploadFileToBaiduyun() {
    
        // TODO 上传至百度云实现逻辑
//    }
  
    public void uploadFileToAliYun() {
    
        // TODO 上传至阿里云实现逻辑
    }
}
   

根据我们上面描述道的开闭原则:对扩展开放,对修改关闭。显然是有一些冲突的。因此我们根据依赖倒置原则,可以将代码进行一定的优化,使其变得更有扩展性和维护性,如下:

interface UploadFile {
    
    public void uploadFile();
}

class UploadFileToBaiduYun implements UploadFile {
    
    @Override
    public void uploadFile() {
    
        // TODO 上传至百度云实现逻辑
    }
}

class UploadFileToAliYun implements UploadFile {
    
    @Override
    public void uploadFile() {
    
        // TODO 上传至阿里云实现逻辑
    }
}

这样后来需求增加上传什么云服务器的时候,我们都可以以扩展的方式加进来,而不用去修改以前的上传方式,这样随着产品的迭代,面对业务需求的变更时,我们就可以游刃有余的对项目进行扩展,而不是改来改去。

二、KISS、YAGNI、DRY

在除了设计模式的SOLID五大设计原则还有这么三个经典的设计原则,比较偏向具体编码细节

KISS:Keep It Stupid and Simple

保持简单,曾经读到的一本书有看过这么一句话: 在编写代码的时候大家往往喜欢追求“高大上”的代码,以为写出让人们很难看懂的代码,才是厉害的代码。恰恰相反,高手都是将最复杂的思想用最简单的代码清晰直观的表现出来。

上面这句话深深点醒了我,不知道大家编写代码的时候有没有这样的经历:

  • 这个地方,我可以用位运算操作一手~ 效率杠杠的!
  • 这个地方,我可以手写一个轮子,让项目更高效!
  • 这个地方,我可以用正则进行匹配,代码简练清晰!
  • 这个地方,我可以 … …

不是说这是错误的想法,恰恰相反在有些业务效率达到瓶颈的时候,这样细节部分的优化是有必要的。不过这样的代码,可读性就有一点令人堪忧了。所以如何去平衡效率与可读性呢。 在业务开发的时候,应该遵循这么几个思想:

  • 这个地方可以用提供的工具类进行操作。
  • 这个地方可以拆分成几个简单的逻辑类接口,还可以进行复用。
  • 不要使用大家不懂的技术来实现代码
  • 不要过度优化,不要使用奇淫技巧

其实这是一个十分主观的思想,因为不同的人对代码的理解能力是有差异的,很多人对位运算,正则,lambda等觉得十分清晰简单,而有的人却没有过深刻的了解,就导致不同人对代码可读性的评判是有区别的。因此我们要尽量在不牺牲可读性的前提下,对代码进行优化,提高团队开发效率。

YAGNI:You Ain’t Gonna Need It

直译就是:你不会需要它。这个原则在提醒我们在设计开发的时候不要过度设计,不仅提高了时间成本,而且降低了开发效率。这样子是十分不必要的,我们可以在相应的地方留出一些扩展点即可,在业务需求迭代的时候,在进行加入适当的逻辑即可。

DRY:Don’t Repeat Yourself

简单粗暴的理解:不要重复!

可以分为三个方向:逻辑实现重复、语义功能重复、代码执行重复。

  • 逻辑实现重复:
// 简化版判断注册校验参数逻辑
    public boolean validateRegisterParameter(String username, String password) {
    
        if (validateUserName(username) && validatePassword(password)) return true;
        return false;
    }
    public boolean validateUserName (String username) {
    
        if (username == null) return false;
        // TODO 对username 格式进行判断
        // TODO 判断username是否满足6-18位
        // TODO 判断username是否包含字母大小写
        // TODO 判断username是否含有非法字符
        return true;
    }

    public boolean validatePassword (String password) {
    
        if (password == null) return false;
        // TODO 对username 格式进行判断
        // TODO 判断username是否满足6-18位
        // TODO 判断username是否包含字母大小写
        // TODO 判断username是否含有非法字符
        return true;
    }

看着是不是十分头大,一样的逻辑 分了两个不同的方法,这个时候,我们可以将相同部分以抽象的方式抽取出一个新的方法:

public boolean validateUserName (String username) {
    
  	if (!validateStringPattern(username)) return false;
  	// ...
  	return true;
}

public boolean validatePassword (String password) {
    
  	if (!validateStringPattern(password)) return false;
  	return true;
}
public boolean validateStringPattern(String s) {
    
    if (s == null) return false;
    // TODO 对s 格式进行判断
    // TODO 判断s是否满足6-18位
    // TODO 判断s是否包含字母大小写
    // TODO 判断s是否含有非法字符
    return true;
}
  • 语义功能重复:
public void isValidData(Data data) {
    
  // TODO 对data进行验证
}

public void checkIfDataValid(Data data) {
    
  // TODO 对data进行验证
}

上面代码中,同一语义对方法名,会引起歧义,如果在其他业务中分别调用了不同的方法,在业务进行更迭重构的时候,会使不清楚的编程人员不明白这两个函数有什么区别,付出不必要的额外时间成本。其次,若今后对其中一个验证方法进行了重构,而忘记了对另一个方法进行重构,则会引起比较难以发现的bug。

  • 代码执行重复:
class UserService {
    
    private UserRepo userRepo = new UserRepo();
    boolean login(User user) {
    
        String phone = user.getPhone();
      	// 这里执行了一次 checkUserByPhone()
        if(userRepo.checkUserByPhone(phone)) {
    
            userRepo.register(user);
            return true;
        }
        return false;
    }
}

class UserRepo {
    
    private UserDao userDao;
    
    boolean checkUserByPhone(String phone) {
    
        if (userdao.getUserByPhone(phone) == null) return true;
        return false;
    }
    
    void register(User user) {
    
      	// 这里又执行了一个 checkUserByPhone()
        if (checkUserByPhone(user.phone)) {
    
            // TODO 注册逻辑
        }
        return;
    }
}

上述代码就很明显的反应了代码执行重复这么一种情况,重复执行一段代码是很没有必要的,而且例子中的代码还是对数据库进行操作,更是极大的影响了程序执行的效率。

三、迪米特法则(LOD - Law of Demeter)

在介绍完经典的SOLID五大原则和KISS、YAGNI、DRY几个实用的原则后,我们来聊一聊这个超级优美的一个法则:迪米特法则。

至于为什么说迪米特法则十分的优美。我们来看一看迪米特法则要实现什么。

定义:迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

核心思想:实现高内聚,低耦合

"高内聚、低耦合"是一个非常重要的设计思想,能够有效地提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。实际上,在前面的章节中,我们已经多次提到过这个设计思想。很多设计原则都以实现代码的“高内聚、松耦合”为目的,比如单一职责原则、基于接口而非实现编程等。

我们通过前面讲述的所有原则,基本上都可以直接或间接的通向这么一个道路 -> 实现高内聚,低耦合。

什么是“高内聚”?

所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。 相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。实际 上,我们前面讲过的单一职责原则是实现代码高内聚非常有效的设计原则。

什么是“低耦合”?

所谓低耦合,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。实际上,我们前面讲的依赖注入、接口隔离、基于接口而非实现编程,以及今天讲的迪米特法则,都是为了实现代码的松耦合。

我们结合具体的代码进行分析:

class Serialization {
    
    public String serialize(Object object) {
    
        String serializeResult = "";
        // TODO 计算值操作...
        return serializeResult;
    }

    public Object deserialize(String str) {
    
        Object deserializeResult = null;
        // TODO 计算值操作...
        return deserializeResult;
    }
}

上述代码实现了序列化与反序列化的操作,乍一看,这个代码是没有任何问题的,不过站在高内聚和低耦合的角度上分析,还是有一定的优化空间的,根据最小知识原则:

在调用序列化的时候是完全不需要知道反序列化的操作是什么样子的,因此我们可能会做出如下设计:

class Serialize {
    
    public String serialize(Object object) {
    
        String serializeResult = "";
        // TODO 计算值操作...
        return serializeResult;
    }
}

class Deserialize {
    
    public Object deserialize(String str) {
    
        Object deserializeResult = null;
        // TODO 计算值操作...
        return deserializeResult;
    }
}

虽然满足的迪米特法则的最小知识原则了,但是不符合高内聚的这个思想。高内聚要求相近的功能要放到同一个类中,这样可以方便功能修改的时候,修改的 地方不至于过于分散。对于刚刚这个例子来说,如果我们修改了序列化的实现方式,比如从 JSON 换成了 XML,那反序列化的实现逻辑也需要一并修改。在未拆分的情况下,我们只需要修改一个类即可。在拆分之后,我们需要修改两个类。显然,这种设计思路的代码改动范围变大了。那么我们该如何改进呢?

为了既能实现高内聚,又能实现迪米特的最小知识原则。我们只需引入两个接口:

interface Serialize {
    
    public String serialize(Object object);
}

interface Deserialize {
    
    public Object deserialize(String str);
}

class Serialization implements serialize, deserialize{
    

    @Override
    public String serialize(Object object) {
    
        // TODO 序列化
        return null;
    }

    @Override
    public Object deserialize(String str) {
    
        // TODO 反序列化
        return null;
    }
}

是不是豁然开朗!!!

一切都是那么的清晰明了,在调用的时候仅仅需要:

Serialize serializeObject = new Serialization();	// 多态声明
serializeObject.serialize();

既隔离了接口,又内聚了模块。简直完美,这种高内聚,低耦合的思想在其他原则中也都有隐约的体现。

如果觉得文章对你有帮助,点个赞支持一下噻~~

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/e935052319/article/details/117136182

智能推荐

使用JDBC连接数据库出现 The server time zone value ‘�й���׼ʱ��‘ is unrecognized or represents more than one解决方案_jdbc.properties timezone-程序员宅基地

文章浏览阅读553次。在 jdbc.properties 文件中的 url 后面加上 ?serverTimezone=UTC加入之前的jdbc.properties文件:user=rootpassword=12345678url=jdbc:mysql://localhost:3306/testdriverClass=com.mysql.cj.jdbc.Driver加入之后:user=rootpassword=12345678url=jdbc:mysql://localhost:3306/test?serv_jdbc.properties timezone

计算机图形学孔令德基础知识,计算机图形学基础教程孔令德答案-程序员宅基地

文章浏览阅读1.4k次。计算机图形学基础教程孔令德答案【篇一:大学计算机图形学课程设】息科学与工程学院课程设计任务书题目:小组成员:巴春华、焦国栋成员学号:专业班级:计算机科学与技术、2009级本2班课程:计算机图形学指导教师:燕孝飞职称:讲师完成时间: 2011年12 月----2011年 12 月枣庄学院信息科学与工程学院制2011年12 月20日课程设计任务书及成绩评定12【篇二:计算机动画】第一篇《计算机图形学》..._计算机图形学基础教程 孔令德 答案

python xlwings追加数据_大数据分析Python库xlwings提升Excel工作效率教程-程序员宅基地

文章浏览阅读1k次。原标题:大数据分析Python库xlwings提升Excel工作效率教程Excel在当今的企业中非常非常普遍。在AAA教育,我们通常建议出于很多原因使用代码,并且我们的许多数据科学课程旨在教授数据分析和数据科学的有效编码。但是,无论您偏爱使用大数据分析Python的程度如何,最终,有时都需要使用Excel来展示您的发现或共享数据。但这并不意味着仍然无法享受大数据分析Python的某些效率!实际上,..._xlwings通过索引添加数据

java8u211_jre864位u211-程序员宅基地

文章浏览阅读911次。iefans为用户提供的jre8 64位是针对64位windows平台而开发的java运行环境软件,全称为java se runtime environment 8,包括Java虚拟机、Java核心类库和支持文件,不包含开发工具--编译器、调试器和其它工具。jre需要辅助软件--JavaPlug-in--以便在浏览器中运行applet。本次小编带来的是jre8 64位官方版下载,版本小号u211版..._jre8是什么

kasp技术原理_KASP基因分型-程序员宅基地

文章浏览阅读5k次。KASP基因分型介绍KASP(Kompetitive Allele-Specific PCR),即竞争性等位基因特异性PCR,原理上与TaqMan检测法类似,都是基于终端荧光信号的读取判断,每孔反应都是采用双色荧光检测一个SNP位点的两种基因型,不同的SNP对应着不同的荧光信号。KASP技术与TaqMan法类似,它与TaqMan技术不同的是,它不需要每个SNP位点都合成特异的荧光引物,它基于独特的..._kasp是什么

华为p50预装鸿蒙系统,华为p50会不会预装鸿蒙系统_华为p50会预装鸿蒙系统吗-程序员宅基地

文章浏览阅读154次。华为现在比较火的还真就是新开发的鸿蒙系统了,那么在即将上市的华为p50手机上会不会预装鸿蒙系统呢?接下来我们就来一起了解一下华为官方发布的最新消息吧。1.华为p50最新消息相信大家都知道,随着华为鸿蒙OS系统转正日期临近,似乎全网的花粉们都在关注华为鸿蒙OS系统优化、生态建设等等,直接忽略了不断延期发布的华为P50手机,如今华为P50系列手机终于传来了最新的好消息,在经过一系列方案修改以后,终于被..._华为手机p50直接预装鸿蒙系统

随便推点

python用什么软件编程好-初学python编程,有哪些不错的软件值得一用?-程序员宅基地

文章浏览阅读2.1k次。Python编程的软件其实许多,作为一门面向大众的编程言语,许多修正器都有对应的Python插件,当然,也有特地的PythonIDE软件,下面我简单引见几个不错的Python编程软件,既有修正器,也有IDE,感兴味的朋友可以本人下载查验一下:1.VSCode:这是一个轻量级的代码修正器,由微软规划研发,免费、开源、跨途径,轻盈活络,界面精练,支撑常见的自动补全、语法提示、代码高亮、Git等功用,插..._python入门学什么好

pytorch一步一步在VGG16上训练自己的数据集_torch vgg训练自己的数据集-程序员宅基地

文章浏览阅读3.2w次,点赞30次,收藏307次。准备数据集及加载,ImageFolder在很多机器学习或者深度学习的任务中,往往我们要提供自己的图片。也就是说我们的数据集不是预先处理好的,像mnist,cifar10等它已经给你处理好了,更多的是原始的图片。比如我们以猫狗分类为例。在data文件下,有两个分别为train和val的文件夹。然后train下是cat和dog两个文件夹,里面存的是自己的图片数据,val文件夹同train。这样我们的..._torch vgg训练自己的数据集

毕业论文管理系统设计与实现(论文+源码)_kaic_论文系统设计法-程序员宅基地

文章浏览阅读968次。论文+系统+远程调试+重复率低+二次开发+毕业设计_论文系统设计法

在python2与python3中转义字符_Python 炫技操作:五种 Python 转义表示法-程序员宅基地

文章浏览阅读134次。1. 为什么要有转义?ASCII 表中一共有 128 个字符。这里面有我们非常熟悉的字母、数字、标点符号,这些都可以从我们的键盘中输出。除此之外,还有一些非常特殊的字符,这些字符,我通常很难用键盘上的找到,比如制表符、响铃这种。为了能将那些特殊字符都能写入到字符串变量中,就规定了一个用于转义的字符 \ ,有了这个字符,你在字符串中看的字符,print 出来后就不一定你原来看到的了。举个例子>..._pytyhon2、python3对%转义吗

java jar 文件 路径问题_「问答」解决jar包运行时相对路径问题-程序员宅基地

文章浏览阅读1.3k次。我这几天需要做一个Java程序,需要通过jar的形式运行,还要生成文件。最终这个程序是要给被人用的,可能那个用的人还不懂代码。于是我面临一个问题:生成的文件一定不能存绝对路径。刚开始我想得很简单,打绝对路径改成相对路径不就行了吗?于是有了这样的代码:String path = "../test.txt";File file = new File(path);……这个写法本身并没有问题,直接运行代码..._jar启动文件路径中存在!

微信读书vscode插件_曾经我以为 VSCode 是程序员专属的工具,直到发现了这些……...-程序员宅基地

文章浏览阅读598次。如果你知道 VSCode,一说起它,你可能第一个想到的就是把它当做一个代码编辑器,而它的界面应该可能大概率是这样的——如果你恰好又是个程序员,那你可能经常会用到它,不管是 Python、JS 还是 C++ 等各种语言对应的文件,都可以用它来进行简单的编辑和整理,甚至是运行和 debug......但是今天要讲的显然不是这些,经过小美的多方研究,发现了即使是对于大多数并不了解 VSCode,也完全不..._vscode weixin read

推荐文章

热门文章

相关标签