程序设计原则之SOLID原则

设计模式中的SOLID原则,分别是单一原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则。前辈们总结出来的,遵循五大原则可以使程序解决紧耦合,更加健壮。

SOLID原则是由5个设计原则组成,SOLID对应每个原则英文字母的开头:

单一职责原则(Single Responsiblity Principle)
开闭原则(Open Close Principle)
里式替换原则(Liskov Substitution Principle)
接口隔离原则(Interface Segregation Principle)
依赖反转原则(Dependency Inversion Principle)
 

SRP单一责任原则
OCP开放封闭原则
LSP里氏替换原则
ISP接口隔离原则
DIP依赖倒置原则




单一责任原则

指的是一个类或者一个方法只做一件事。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化就可能抑制或者削弱这个类完成其他职责的能力。例如餐厅服务员负责把订单给厨师去做,而不是服务员又要订单又要炒菜。

这里写图片描述

一个协议翻译类,在协议翻译的过程中如果出现了异常,则把异常写入文件日志中。粗略看来这个类没有问题,但是如果我们需要把日志写入数据库,那么我么就需要改变代码。按照单一职责原则,这个类的设计就没有达到要求,因为日志规范的修改,确需要修改协议翻译类。为此我们可以引入专门的日志类来解决这个问题。如果再遇到日志相关的需求变更,我们只需要修改日志类就好了。
 




开放封闭原则

对扩展开放,对修改关闭。意为一个类独立之后就不应该去修改它,而是以扩展的方式适应新需求。例如一开始做了普通计算器程序,突然添加新需求,要再做一个程序员计算器,这时不应该修改普通计算器内部,应该使用面向接口编程,组合实现扩展。

这里写图片描述

程序 AnimalCounter 负责统计动物的腿数,如果我们要增加一种新动物比如 Sheep,我们就需要给 AnimalCounter 的 countFeet 函数增加一个判断,判断数组中是不是有 Sheep 实例,这就是说当有新的需求来的时候,我们得修改 AnimalCounter 代码,而不是扩展它。

下面的代码可以解决这个问题。我们使用了一个接口,鸡类和狗类都继承了这个接口,在 AnimalCounter 中我们只要调用这个接口就可以知道动物有多少只脚了。无论是再有绵羊类或者是昆虫类,它们只要继承了这个接口,AnimalCounter 都可以计算出动物的总脚数。也就是我们通过扩展 IAnimal 接口就可以满足需求,而不用修改 AnimalCounter 类。

里氏替换原则

所有基类出现的地方都可以用派生类替换而不会程序产生错误。子类可以扩展父类的功能,但不能改变父类原有的功能。例如机动车必须有轮胎和发动机,子类宝马和奔驰不应该改写没轮胎或者没发动机。

这里写图片描述




 

我们定义了一个类叫 Bird,这个接口有四个方法,然后我们有一个天鹅类,一个鸡类。可以看到,Bird 的四个方法用 Swan 类来代替是没有问题的,但是用 Chicken 类来代替当调用到 fly 方法的时候就会抛出异常。这个就不符合 Liskov 原则,因为作为 Bird 类的子类的 Chicken 类没有做到替换父类 Bird 类而不影响程序运行。解决方法是拆分Bird类的功能,因为家禽是不会飞的。
 

接口隔离原则

类不应该依赖不需要的接口,知道越少越好。例如电话接口只约束接电话和挂电话,不需要让依赖者知道还有通讯录。

这里写图片描述

类不应该被强迫去依赖它用不到的方法。大的原则是很多小而精的接口,要好于一个大一统的接口。比如下面的 ICar 接口,我们不应该为了大一统把加油 (fuel) 和 (Charge) 充电都放在里面,因为烧汽油的汽车才需要加油,使用电池驱动的电动车才需要充电。如果子类继承了父类中用不到的方法,子类也会打破上面的 Liskov 原则,也不利于将来的重构和优化。
 

依赖倒置原则

依赖反转说了两点:

高层模块不应该依赖低层模块,双方应该依赖抽象。
抽象不应该依赖细节,而细节应该依赖抽象。
听起来很绕口,不过这个确实是面向对象编程里解决紧耦合问题最重要的原则之一。通常的解决方案就是大名鼎鼎的依赖注入!

指的是高级模块不应该依赖低级模块,而是依赖抽象。抽象不能依赖细节,细节要依赖抽象。比如类A内有类B对象,称为类A依赖类B,但是不应该这样做,而是选择类A去依赖抽象。例如垃圾收集器不管垃圾是什么类型,要是垃圾就行。

例子图

比如 物品交易,我有牛,你有羊, 以物换物,相互依赖, 如果改为统一用货币来交易,则彼此不依赖,而是都依赖于各自的物品抽象--货币。 

下面的代码的任务是打印一个指定路径的文件,打印完成后发出 email。这个代码就违反了依赖反转原则,所有的 new 语句处都表示高层模块需要知道低层模块的细节,比如 Program 类就需要如何生成 PrinterService 和 EMailService,PrinterService 和 EMailService 的功能也没有被抽象出来。这样程序的功能在需要重构、扩展或者替换时高层模块和低层模块都需要知道对方的细节。

public Program {
	public static void main(String[] args) {
		var filePath = args[0];
		var printerService = new PrinterService();
		printerService.print(filePath);
		var emailService = new EMailService();
		emailService.send("surfirst@example.com", "File printed", filePath);
	}
}

public class PrinterService {
	final Logger logger;
	PrinterService()
	{
		this.logger = new Logger();
	}
	void print(String filePath) {
		// process print task
		...
		this.logger.log(filePath + " printed");
	}
}

public class Logger {
	public log(String content)
	{
		System.out.println(content);
	}
}

public class EmailService {
	public void Send(String email, String subject, String content)
	{
		System.out.println("Email %s has been sent to %s. ", subject, email);
	}
}

解决上面的方法可以是依赖注入,也可以通过类工厂的方法来解决。由于篇幅有限,我们在这里使用类工厂来展示解决方案。首先我们抽象出我们用到的组件的接口,然后我们通过类工厂来实现这些接口,最后通过类工厂来解决依赖问题。

下面是我们抽象出来的接口:

public inteface IPrinterService {
	void print(String filePath);
}

public interface ILogger {
	void log(String content);
}

public interface IEmailService {
	void send(String email, String subject, String content);
}

下面是类工厂的代码,虽然看上去很简单,但是通过类工厂,我们就解决了抽象到实现细节的问题,这使我们的业务逻辑独立于我们的依赖项。依赖项可以来自外部文件,对 Java 来说就是不同的 JAR 文件,对 .Net 来说可以是不同的 DLL。 

public class Factory {
	public static IPrinterService CreatePrinterService()
	{
		return new PrinterService();
	}
	public static ILogger CreateLogger() {
		return new Logger();
	}
	public static IEmailService CreateEmailService() {
		return new EmailService();
	}
}

下面是使用了类工厂以后的业务逻辑代码:

public Program {
	public static void main(String[] args) {
		var filePath = args[0];
		IPrinterService printerService = Factory.CreatePrinterService();
		printerService.print(filePath);
		IEmailService emailService = Factory.CreateEmailService();
		emailService.send("surfirst@example.com", "File printed", filePath);
	}
}

public class PrinterService implements IPrinterService {
	final Logger logger;
	PrinterService()
	{
		this.logger = Factory.CreateLogger();
	}
	void print(String filePath) {
		// process print task
		...
		this.logger.log(filePath + " printed");
	}
}

public class Logger implements ILogger {
	public log(String content)
	{
		System.out.println(content);
	}
}

public class EmailService implements EmailService {
	public void Send(String email, String subject, String content)
	{
		System.out.println("Email %s has been sent to %s. ", subject, email);
	}
}

总述

没人写一款程序能完全遵守SOLID原则,甚至有些设计模式是违反SOLID原则。如何权衡就要看利是否大于弊。不足之处望指教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值