设计模式之美笔记4

记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步

经典设计原则续

1. 依赖反转原则

思考题:

  1. “依赖反转”这个概念指的是“谁跟谁”的“什么依赖”被反转了?“反转”两个字该如何理解?
  2. 经常听到另外两个概念:“控制反转”和“依赖注入”,它们和“依赖反转”有什么区别和联系?是同一个事情吗?
  3. java语言的spring框架中的IOC跟这些概念有什么关系?

1. 控制反转IOC

英文Inversion Of Control,暂时不要将它和spring框架的IOC联系到一起。

举例看什么是控制反转

public class UserServiceTest{
	public static boolean doTest(){
		//...
	}

	public static void main(String[] args){
		//这部分逻辑可放到框架中
		if(doTest()){
			System.out.println("Test succeed.");
		}else{
			System.out.println("Test failed.");
		}
	}
}

上述代码中,所有流程由程序员控制。如果抽象出下面这个框架,再看如何利用框架实现同样的功能:

public abstract class TestCase{
	public void run(){
		if(doTest()){
			System.out.println("Test succeed.");
		}else{
			System.out.println("Test failed.");
		}
	}

	public abstract void doTest();
}

public class JunitApplication{
	private static final List<TestCase> testCases = new ArrayList<>();

	public static void register(TestCase testCase){
		testCases.add(testCase);
	}

	public static final void main(String[] args){
		for(TestCase case:testCases){
			case.run();
		}
	}
}

将这个简化版的测试框架引入工程中,只需在框架预留的扩展点,也就是TestCase类的doTest()抽象方法中,填充具体的测试代码即可实现之前的功能。完全不需要写负责执行流程的main()方法

public class UserServiceTest extends TestCase{
	@Override
	public boolean doTest(){
		//...
	}
}

//注册操作还可通过配置的方式实现,不需要程序员显式调用register
JunitApplication.register(new UserServiceTest());

上述案例就是通过框架实现“控制反转”的例子。框架提供一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员用框架开发时,只需要往预留的扩展点,添加自己业务相关的代码,就能利用框架驱动整个程序流程的执行。

这里的“控制”指的是对程序执行流程的控制,而“反转”是指在没用框架之前,程序员自己控制整个程序的执行,而用框架后,程序的执行流程通过框架控制。流程的控制权从程序员“反转”到框架。

实际上,实现控制反转的方法很多,除了上述例子所示类似模板设计模式的方法之外,还有依赖注入等方法。所以,控制反转不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。

2. 依赖注入DI

依赖注入Dependency Injection,跟控制反转相反,是一种具体的编码技巧。有个形象的说法:依赖注入是一个标价25美元,而实际只值5美分的概念。理解应用很简单。概括就是:不通过new()方法的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好后,通过构造函数、方法参数等方式传递(或注入)给类使用。

举例说明。Notification类负责消息推送,依赖MessageSender类实现推送商品促销、验证码等消息给用户。分别用依赖注入和非依赖注入两种方式实现。

//=======非依赖注入实现方式========
public class Notification{
	private MessageSender messageSender;

	public Notification(){
		this.messageSender = new MessageSender();//有点像硬编码
	}

	public void sendMessage(String cellphone,String message){
		//...省略校验逻辑等...
		this.messageSender.send(cellphone,message);
	}
}

public class MessageSender{
	public void send(String cellphone,String message){
		//...
	}
}

//使用Notification
Notification notification = new Notification();


//============依赖注入方式实现==========
public class Notification{
	private MessageSender messageSender;

	//通过构造函数将messageSender传递进来
	public Notification(MessageSender messageSender){
		this.messageSender = messageSender;
	}

	public void sendMessage(String cellphone,String message){
		//...省略校验逻辑等...
		this.messageSender.send(cellphone,message);
	}
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);

通过依赖注入的方式将依赖的类对象传递进来,提高了代码的扩展性,可以灵活的替换依赖的类。当然,还能继续优化,将MessageSender定义为接口,基于接口而非实现编程。

public class Notification{
	private MessageSender messageSender;

	//通过构造函数将messageSender传递进来
	public Notification(MessageSender messageSender){
		this.messageSender = messageSender;
	}

	public void sendMessage(String cellphone,String message){
		//...省略校验逻辑等...
		this.messageSender.send(cellphone,message);
	}
}
public interface MessageSender{
	void send(String cellphone,String message);
}

//短信发送类
public class SmsSender implements MessageSender{
	@Override
	public void send(String cellphone,String message){
		//...
	}
}
//站内信发送类
//短信发送类
public class InboxSender implements MessageSender{
	@Override
	public void send(String cellphone,String message){
		//...
	}
}

//使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);

依赖注入是编写可测试性代码最有效的手段。

3. 依赖注入框架DI Framework

采用依赖注入实现的Notification类中,虽然不需要硬编码,在类内部通过new来创建MessageSender对象,但创建对象、组装(或注入)对象的工作仅仅是被移动到更上层的代码而已,还需要我们自己实现。

public class Demo{
	public static final void main(String[] args){
		MessageSender sender = new SmsSender();//创建对象
		Notification notification = new Notification(sender);//依赖注入
		notification.sendMessage("13510733521","短信验证码:2346");
	}
}

实际的软件开发,一些项目可能会涉及非常多的类,类对象的创建和依赖注入非常复杂,而对象创建和依赖注入的工作,本身和具体业务无关,抽象为框架自动完成,也就是“依赖注入框架”。通过框架提供的扩展点,简单配置所有要创建的类对象、类和类之间的依赖关系,就能实现由框架自动创建对象、管理对象的生命周期、依赖注入等原本要程序员做的事情。

现成的依赖注入框架如Google Guice,java spring等。spring框架自称是控制反转容器(Inversion Of Control Container)。spring框架的控制反转主要是通过依赖注入实现。

4. 依赖反转原则DIP

依赖反转原则Dependency Inversion Principle,也叫依赖倒置原则。英文描述:High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions。也就是:高层模块不要依赖低层模块,应该通过抽象来互相依赖。此外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

高层和低层模块的划分:在调用链上,调用者属于高层,被调用者属于低层。这个原则主要是指导框架层面的设计。以tomcat这个servlet容器为例。tomcat是运行java web应用程序的容器。web应用程序代码只需部署到tomcat容器下,就能被tomcat容器调用执行。因此,tomcat是高层模块,web应用程序代码是低层模块。tomcat和程序代码之间没有直接的依赖关系,都依赖同一个“抽象”,也就是servlet规范。servlet规范不依赖具体的tomcat容器和应用程序的实现细节,而tomcat容器和应用程序依赖servlet规范。

spring开发的项目在tomcat中,控制权在tomcat手中,微服务兴起,tomcat内嵌到springboot项目中,控制权反转到springboot手中。

2. KISS和YAGNI原则

问题:

  • 怎么理解KISS原则的“简单”两个字?什么样的代码才算“简单”?怎样的代码才算“复杂”?
  • 如何才能写出“简单”的代码?YAGNI原则和KISS原则说的是一回事吗?

1. 如何理解KISS原则

英文好几个版本:Keep It Simple and Stupid。尽量保持简单。是个万金油类型的设计原则,可用于很多场景,如iPhone的设计。那如何在代码开发中应用这条原则呢?如何落地呢?

2. 代码行数越少越简单吗

先看案例,下面三段代码可实现同样一个功能:检查输入的字符串ipAddress是否是合法的IP字段。

一个合法的IP地址由4个数字组成,并通过"."进行分割。每组数字取值范围是0~255.第一组较特殊,不允许为0。

//第一种实现方式:使用正则表达式
public boolean isValidIpAddressV1(String ipAddress){
	if(StringUtils.isBlank(ipAddress)) return false;
	String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
		+"(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
		+"(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
		+"(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
	return ipAddress.matches(regex);
}

//第二种实现方式:使用现成的工具类
public boolean isValidIpAddressV2(String ipAddress){
	if(StringUtils.isBlank(ipAddress)) return false;
	String[] ipUnits = StringUtils.split(ipAddress,'_');
	if(ipUnits.length != 4){
		return false;
	}
	for(int i=0; i<4; ++i){
		int ipUnitIntValue;
		try{
			ipUnitIntValue = Integer.parseInt(inUnits[i]);
		}catch(NumberFormatException e){
			return false;
		}

		if(ipUnitIntValue < 0 || ipUnitIntValue>255){
			return false;
		}
		if(i==0 && ipUnitIntValue==0){
			return false;
		}
	}
	return true;
}

//第三种,不用任何工具类
public boolean isValidIpAddressV3(String ipAddress){
	char[] ipChars = ipAddress.toCharArray();
	int length = ipChars.length;
	int ipUnitIntValue = -1;
	boolean isFirstUnit = true;
	int unitsCount = 0;
	for(int i=0; i<length;++i){
		char c = ipChars[i];
		if(c=='.'){
			if(ipUnitIntValue<0 || ipUnitIntValue>255) return false;
			if(isFirstUnit && ipUnitIntValue==0) return false;
			if(isFirstUnit) isFirstUnit=false;
			ipUnitIntValue = -1;
			unitsCount++;
			continue;
		}
		if(c<'0' || c>'9'){
			return false;
		}
		if(ipUnitIntValue == -1) ipUnitIntValue=0;
		ipUnitIntValue = ipUnitIntValue*10+(c-'0');
	}
	if(ipUnitIntValue<0 || ipUnitIntValue>255) return false;
	if(unitsCount != 3) return false;
	return true;
}

第一种正则,写出来就很难,也难保证不出bug,可读性和可维护性也差;第二种使用一些现成的工具类,处理IP地址字符串;第三种,不用任何工具类,逐一处理IP地址中的字符,判断是否合法。第三种更有难度,更易出现bug,当然性能更高些。

如何选择,如果一般的业务代码,首选第二种,第三种其实是过度优化,牺牲代码的可读性,性能提升并不明显,投入产出比较低。

3. 代码逻辑复杂就违反KISS原则吗

如果代码的逻辑复杂、实现难度大、可读性也不好,是否就一定违反KISS原则?

以KMP字符串匹配算法的代码实现为例,该算法以快速高效著称。当需要处理长文本字符串匹配问题(几百MB大小文本内容的匹配),或者字符串匹配是某个产品的核心功能(如Vim、Word等文本编辑器),又或者字符串匹配算法是系统性能瓶颈时,应该选择该算法。本身就复杂的问题,用复杂的算法解决,并不违反KISS原则。

当然,平时项目开发涉及到的字符串匹配问题,大多都是较小的文本,直接调用现成的字符串匹配方法即可。要看应用场景。

4. 如何写出满足KISS原则的代码

  • 不要用同事可能不懂的技术来实现,如前面的正则表达式,还有些过于高级的语法
  • 不要重复造轮子,善于使用已有的工具类库
  • 不要过度优化,牺牲代码的可读性

5. YAGNI和KISS是一回事吗

YAGNI原则的英文:You Ain’t Gonna Need It。你不会需要它。意为:不要去设计当前用不到的功能;不要编写当前用不到的代码。也就是不要做过度设计。

如用redis存储配置信息,可能以后会用zookeeper,没必要提前编写这块代码,只需要预留好扩展点即可。再比如,不要在项目中提前引入不需要依赖的开发包。如maven等配置文件,提前引入大量常用的library包,就违反YAGNI原则。

YAGNI原则讲的是“要不要做”的问题(当前不需要的就不要做),KISS原则讲的是“如何做”的问题(尽量保持简单)。

3. DRY原则

1. 概念

英文描述:Don’t Repeat Yourself。应用于编程,可理解为:不要写重复的代码。

三种场景:

2. 实现逻辑重复

看下面这段代码是否违反DRY原则:

public class UserAuthenticator{
	public void authenticate(String username,String password){
		if(!isValidUsername(username)){
			//...throw InvalidUsernameException
		}
		if(!isValidPassword(password)){
			//...throw InvalidPasswordException
		}
		//...省略其他代码...
	}

	private boolean isValidUsername(String username){
		// check not null, not empty
		if(StringUtils.isBlank(username)){
			return false;
		}
		// check length:4~64
		int length = username.length();
		if(length < 4 || length > 64){
			return false;
		}
		// contains only lowcase characters
		if(!StringUtils.isAllLowerCase(username)){
			return false;
		}
		// contains only a~z,0~9,dot
		for(int i=0;i<length;i++){
			char c = username.charAt(i);
			if(!((c>='a' && c<='z') || (c>='0' && c<='9') || c=='.')){
				return false;
			}
		}
		return true;
	}

	private boolean isValidPassword(String password){
		// check not null, not empty
		if(StringUtils.isBlank(password)){
			return false;
		}
		// check length:4~64
		int length = password.length();
		if(length < 4 || length > 64){
			return false;
		}
		// contains only lowcase characters
		if(!StringUtils.isAllLowerCase(password)){
			return false;
		}
		// contains only a~z,0~9,dot
		for(int i=0;i<length;i++){
			char c = password.charAt(i);
			if(!((c>='a' && c<='z') || (c>='0' && c<='9') || c=='.')){
				return false;
			}
		}
		return true;
	}
}

有两处非常明显的重复的代码片段:isValidUsername()和isValidPassword()方法。看起来违反DRY原则,但如果重构为isValidUsernameOrPassword(),代码行数减少,但并非更好。合并后的方法,负责两个事情,违反了单一职责原则和接口隔离原则。

其实这两个方法,虽然代码实现逻辑看起来重复,但语义上并不重复。语义不重复的意思是:从功能上看,这两个方法干的是完全不重复的两件事情,一个校验用户名,一个校验密码。如果某天需要修改校验密码的逻辑,那还需要重新拆分。

如果代码实现逻辑相同,但语义不同,判定不违反DRY原则。对于包含重复代码的问题,可以抽象成更细的粒度的方法来解决。如将校验只包含az、09、dot的逻辑封装为boolean onlyContains(String str,String charlist);方法。

3. 功能语义重复

再看另一个例子,同一个项目代码,有两个方法:isValidIp()和checkIfIpValid()。尽管命名不同,实现逻辑不同,但功能相同,都是判断IP地址是否合法。

原因可能是两个方法由两个不同的同事开发,其中一个不知道已经有了isValidIp()方法,自己又定义并实现另一个方法。这种就违反了DRY原则,因为功能重复。需要统一调一个方法。因为假如不统一实现思路,结果有些地方用isValidIp(),另一些地方调用checkIfIpValid(),不熟悉的同事增加阅读难度,而且很疑惑。此外,如果哪天IP地址判定合法的规则改变,结果只改了其中一个,忘了改另一个,导致出现莫名其妙的bug。

4. 代码执行重复

下面例子,UserService中login()方法校验用户登录是否成功。

public class UserSerive{
	private UserDao userDao;//IOC框架注入
	public User login(String email,String password){
		boolean existed = userDao.checkIfUserExisted(email,password);
		if(!existed){
			//...throw AuthenticationFailureException
		}
		User user = userDao.getUserByEmail(email);
		return user;
	}
}

public class UserDao{
	public boolean checkIfUserExisted(String email,String password){
		if(!EmailValidation.validate(email)){
			//...throw InvalidEmailException...
		}

		if(!PasswordValidation.validate(password)){
			//...throw InvalidPasswordException...
		}

		//...query db to check if email&password exists...
	}

	public User getUserByEmail(String email){
		if(!EmailValidation.validate(email)){
			//...throw InvalidEmailException...
		}
		//...query db to get user by email...
	}
}

上面这段代码,没有逻辑重复和语义重复,但仍违反了DRY原则。因为代码存在执行重复。

最明显的就是在login()方法中,email校验逻辑执行两次。修改的话,只需要将校验逻辑从UserDao中移除,统一放到UserService中即可。

此外,还有一处较为隐蔽的执行重复,login()方法并不需要调checkIfUserExisted()方法,只需调一次getUserByEmail()方法,从数据库获取用户的emai、password,跟用户输入的信息对比,判断是否登录成功。

这样的优化很有必要,因为两个方法都需要查询数据库,这种IO操作较为耗时,尽量减少这类IO操作。

重构之后的代码:

public class UserSerive{
	private UserDao userDao;//IOC框架注入
	public User login(String email,String password){
		if(!EmailValidation.validate(email)){
			//...throw InvalidEmailException...
		}

		if(!PasswordValidation.validate(password)){
			//...throw InvalidPasswordException...
		}
		User user = userDao.getUserByEmail(email);
		if(user==null || !password.equals(user.getPassword)){
			//...throw AuthenticationFailureException...
		}
		return user;
	}
}

public class UserDao{
	public boolean checkIfUserExisted(String email,String password){
		//...query db to check if email&password exists...
	}

	public User getUserByEmail(String email){
		//...query db to get user by email...
	}
}

5. 代码复用性code reusability

什么是代码的复用性?

首先区分三个概念:代码复用性(Code Reusability)、代码复用(Code Reuse)和DRY原则。

代码复用表示一种行为:开发新功能时,尽量复用已存在的代码。代码的可复用性表示代码可被复用的特性或能力。DRY原则是一条原则:不要写重复的代码。

区别:

  • 不重复并不代表可复用。这个角度,DRY原则和代码的可复用性是两回事
  • 复用和可复用性关注角度不同。可复用性是从开发者角度讲;复用是从代码使用者角度讲。如A同事开发的UrlUtils类可复用性很好,B同事开发新功能时复用了A的这个类

如何提高可复用性?

  • 减少代码耦合
  • 满足单一职责原则
  • 模块化
  • 业务和非业务逻辑分离
  • 通用代码下沉 越底层的代码越通用,会被更多模块调用,因此,通用代码尽量下沉到更下层
  • 继承、多态、抽象、封装 利用继承将公共的代码抽取到父类,子类复用父类的属性和方法;利用多态,动态的替换代码的部分逻辑,可复用性增强;抽象和封装,从更广义的层面理解,越是抽象、不依赖具体的实现,越容易复用;代码封装为模块,隐藏可变细节,暴露不变的接口,越容易复用
  • 应用模板等设计模式 一些设计模式也提高代码的复用性。如模板模式利用多态实现。

此外,复用意识也很重要,写代码时,多思考,这部分代码是否可抽取出来,作为独立的模块、类或者方法供多处使用。

另外还有个著名的原则,“Rule Of Three”。第一次开发时,没有复用的需求,可暂时不考虑复用性。之后开发新功能,发现可复用之前代码,就重构这段代码,让其可复用。

4. 迪米特法则LOD

问题:

  • 什么是“高内聚、低耦合”
  • 如何利用迪米特法则实现“高内聚、低耦合”
  • 有哪些代码设计明显违反迪米特法则,该如何重构

1. 概念

高内聚、低耦合是一个非常重要的设计思想,能有效的提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。很多设计原则都以实现代码的高内聚、低耦合为目的,如单一职责原则、基于接口而非实现编程等。

高内聚用来指导类本身的设计,低耦合指导类与类之间的依赖关系的设计。并非完全独立不相关,高内聚有助于低耦合,低耦合又需要高内聚的支持。

  • 什么是高内聚

指的是相近的功能应放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到一个类,修改集中,代码易维护。

  • 什么是低耦合

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

  • 内聚和耦合之间的关系

代码耦合度高,会牵一发而动全身,改动影响多个类。高内聚、低耦合的代码结构更简单清晰,可维护性和可读性更好。

  • 迪米特法则的描述

英文Law Of Demeter,另一个名字,最小知识原则,英文:The Least Knowledge Principle。英文定义:Ench unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers。

也就是:不该有直接依赖的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。

2. 理论解读和代码实战1

前半部分:不该有直接依赖的类之间,不要有依赖。

案例实现简化版的搜索引擎爬取网页的功能。代码包含三个主要的类。NetworkTransporter类负责底层网络通信,根据请求获取数据;HtmlDownloader类来处理URL获取网页;Document表示网页文档,后续网页内容的抽取、分词、索引都是以此为处理对象。

public class NetworkTransporter{
	//省略属性和其他方法
	public Byte[] send(HtmlRequest htmlRequest){
		//...
	}
}

public class HtmlDownloader{
	private NetworkTransporter transporter;//IOC注入

	public Html downloadHtml(String url){
		Byte[] rawHtml = transporter.send(new HtmlRequest(url));
		return new Html(rawHtml);
	}
}

public class Document{
	private Html html;
	private String url;

	public Document(String url){
		this.url = url;
		HtmlDownloader downloader = new HtmlDownloader();
		this.html = downloader.downloadHtml(url);
	}
	//...
}

这段代码虽然能用,但有些缺陷。

首先,看NetworkTransporter类。作为底层网络通信类,希望尽可能通用,而不是只服务于下载HTML,不应该直接依赖太具体的发送对象HtmlRequest。应如何重构呢?假如现在去商店买东西,不会直接把钱包给收银员,让收银员自己从钱包里拿钱,而是你从钱包里把钱拿出来交给收银员。这里,HtmlRequest对象相当于钱包,里面的address和content对象相当于钱。应把address和content给NetworkTransporter。

public class NetworkTransporter{
	//省略属性和其他方法
	public Byte[] send(String address,Byte[] data){
		//...
	}
}

再看HtmlDownloader类。设计没问题,但刚修改了NetworkTransporter的send()方法,这个类也要相应的修改

public class HtmlDownloader{
	private NetworkTransporter transporter;//IOC注入

	public Html downloadHtml(String url){
		HtmlRequest htmlRequest = new HtmlRequest(url);
		Byte[] rawHtml = transporter.send(htmlRequest.getAddress(),htmlRequest.getContent().getBytes());
		return new Html(rawHtml);
	}
}

最后看Document类。它的问题较多,主要有三点。

  1. 构造函数的downloader.downloadHtml()逻辑复杂,耗时长,不应放到构造函数,影响性能。
  2. HtmlDownloader对象在构造函数中通过new创建
  3. 业务含义上,Document网页文档没必要依赖HtmlDownloader类,违反迪米特法则

改造:

public class Document{
	private Html html;
	private String url;

	public Document(String url){
		this.url = url;
		this.html = downloader.downloadHtml(url);
	}
	//...
}

//通过工厂方法创建Document
public class DocumentFactory{
	private HtmlDownloader downloader;

	public DocumentFactory(HtmlDownloader downloader){
		this.downloader = downloader;
	}

	public Document createDocument(String url){
		Html html = downloader.downloadHtml(url);
		return new Document(url,html);
	}
}

3. 理论解读和代码实战2

原则的后半部分:“有依赖关系的类之间,尽量只依赖必要的接口”。如之前讲的Serialization类负责序列化和反序列化。假设在项目中,有些类只用到序列化操作,而另一些类只用到反序列化。那只用到序列化操作的类不应该依赖反序列化接口,反之亦然。

因此,我们应该将Serialization类拆分为更小粒度的类,但这样违反了高内聚的设计思想。那如何解决这个问题?通过引入两个接口就能解决该问题。也就是接口隔离原则。

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

public interface Deserializable{
	Object deserialize(String text);
}

public class Serialization implements Serializable,Deserializable{
	@Override
	public String serialize(Object object){
		String serializedResult = ...;
		...
		return serializedResult;
	}

	@Override
	public Object serialize(String str){
		Object deserializedResult = ...;
		...
		return deserializedResult;
	}
}

public class DemoClass1{
	private Deserializable deserializer;

	public Demo(Deserializable deserializer){
		this.deserializer = deserializer;
	}
	//...
}
public class DemoClass2{
	private Serializable serializer;

	public Demo(Serializable serializer){
		this.serializer = serializer;
	}
	//...
}

上述的代码思路,体现了基于接口而非实现编程的设计原则,结合迪米特法则,可以总结出一条新的设计原则,基于最小接口而非最大实现编程。

猜你喜欢

转载自blog.csdn.net/wjl31802/article/details/107669472