软件架构设计的七大原则

开闭原则

开闭原则指的是一个软件实体如类、模块、和函数应该对扩展开放、对修改关闭。所谓的开闭,指的是对扩展和修改两个行为的一个原则。强调的是用抽象构建框架,用实现扩展细节。可以提高软件系统的可复用性和可维护性。它指导我们如何建立灵活稳定的系统。
实现开闭原则的核心思想就是面向抽象编程。
举例:弹性工作时间就是一个例子,它对每天工作8小时是关闭的、对几点来几点走是开放的。

下面以课程为例,首先建立一个课程接口ICourse:

public interface ICourse {
	Integer getId();
	String getName();
	Double getPrice();
}

现在来建立一个Java类:

public class JavaCourse implements ICourse{
	private Integer id;
	private String name;
	private Double price;
	public JavaCourse(Integer id, String name, Double price) {
		this.id = id;
		this.name = name;
		this.price = price;
	}
	@Override
	public Integer getId() {
		return this.id;
	}
	@Override
	public String getName() {
		return this.name;
	}
	@Override
	public Double getPrice() {
		return this.price;
	}
}

这时,我们要给Java课程做一个价格优惠的活动,此时,如果我们修改JavaCourse 里的getPrice()则有可能会影响其他地方的调用。那么要如何在不修改源代码的同时实现价格优惠的功能呢?现在,再写一个处理优惠的类,JavaDiscountCourse类。

public class JavaDiscountCourse extends JavaCourse{

	public JavaDiscountCourse(Integer id, String name, Double price) {
		super(id, name, price);
	}
	public Double getOrigenPrice() {
		return super.getPrice();
	}
	public Double getPrice() {
		return super.getPrice() * 0.16;
	}
}

依赖倒置原则

是指在设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。
通过依赖倒置,可以减少类与类之间的耦合性,提高系统稳定性,提高代码的可读性和可维护性,降低修改程序做造成的风险。

以课程为例,建立一个Tom类:

public class Tom {
	public void studyJava() {
		System.out.println("Tom正在学习Java");
	}
	public void studyAI() {
		System.out.println("Tom正在学习AI");
	}
}

新建一个调用类:

public class DependenceInversionTest {
	public static void main(String[] args) {
		Tom tom = new Tom();
		tom.studyAI();
		tom.studyJava();
	}
}

倘若现在Tom还想学习大数据,这个时候,我们就需要从底层到高层(调用层)依次进行修改。如此一来,系统发布以后是非常不稳定的,在修改代码的同时也会带来未知的风险。接下来,我们优化代码,创建一个课程的抽象接口:

public interface ICourse {
	void study();
}

新建一个JavaCourse类:

public class JavaCourse implements ICourse{
	@Override
	public void study() {
		System.out.println("Tom 正在学习Java");
	}
}

新建一个AICourse类:

public class AICourse implements ICourse{
	@Override
	public void study() {
		System.out.println("Tom正在学习AI");
	}
}

修改Tom类:

public class Tom {
	public void study(ICourse course) {
		course.study();
	}
}

来看调用:

public static void main(String[] args) {
		Tom tom = new Tom();
		tom.study(new AICourse());
		tom.study(new JavaCourse());
}

现在,不论Tom想要学习任何新课程,我们只需要新建一个类,通过传参的方式告诉Tom,不需要修改底层代码,就可以实现。这也叫依赖注入。注入的方式还有构造器方式和Setter方式。

  1. 构造器方式注入:

修改Tom类:

public class Tom {
	private ICourse iCourse;
	public Tom(ICourse iCourse) {
		this.iCourse = iCourse;
	}
	public void study() {
		iCourse.study();
	}
}

修改调用类:

public static void main(String[] args) {
		Tom tom = new Tom(new JavaCourse());
		tom.study();
		Tom tom2 = new Tom(new AICourse());
		tom2.study();
}

可以看出:构造器的注入方式需要每次都创建一个实例,如果Tom是全局单例的话,我们就只能使用Setter的方式注入了
2. Setter方式注入:
修改Tom类:

public class Tom {
	private ICourse iCourse;
	public void setICourse(ICourse iCourse) {
		this.iCourse = iCourse;
	}
	public void study() {
		iCourse.study();
	}
}

修改调用类:

public static void main(String[] args) {
		Tom tom = new Tom();
		tom.setICourse(new JavaCourse());
		tom.study();
		
		tom.setICourse(new AICourse());
		tom.study();
}

以抽象为基准要比以细节为基准搭建起来的架构要稳定得多,因此拿到需求后,要面向接口编程,先顶层再细节来设计代码结构。

单一职责原则

指的是不要存在多于一个导致类变更的原因。
假设我们有一个Class负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能会导致另一个职责的功能发生故障。这样一来,这个Class存在2个导致类变更的原因。如此,我们就要给两个职责分别用两个Class来实现,进行解耦。后期需求变更维护互不影响。
这样的设计,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。
总的来说就是,一个Class\Interface\Method只负责一项职责。

  • Class层面的单一职责设计

还是以课程为例:

public class ICourse {
	public void study(String courseName) {
		if("直播".equals(courseName)) {
			System.out.println(courseName+"不能快进");
		}else {
			System.out.println(courseName+"随意");
		}
	}
}

代码调用:

public static void main(String[] args) {
		ICourse iCourse = new ICourse();
		iCourse.study("直播");
		iCourse.study("重播");
}

从上面来看,ICourse 承担了两种处理逻辑。如果我们现在要对课程进行加密,两种课程的加密逻辑是不一样的,这样的话就肯定要改代码,而修改代码就会相互影响造成不可控的风险。因此,对职责进行分离,分别创建两个类LiveCourse和ReplayCourse。

public class LiveCourse {
	public void study(String courseName) {
		System.out.println(courseName + "不能快进");
	}
}
public class ReplayCourse {
	public void study(String courseName) {
		System.out.println(courseName + "随意");
	}
}

修改调用类:

public static void main(String[] args) {
		LiveCourse liveCourse = new LiveCourse();
		liveCourse.study("直播");
		ReplayCourse replayCourse = new ReplayCourse ();
		replayCourse .study("重播");
}
  • Interface 层面的单一职责设计
    业务继续发展,课程需要加权限,未交费的学员只可以获取课程基本信息,已经交费的学员可以获取视频信息。那么对于控制课程层面上至少有两个职责。我们可以把展示职责和管理职责分离开来,都实现同一个抽象依赖。我们可以设计一个顶层接口,创建ICourse接口。
public interface ICourse {
	//获得基本信息 
	String getCourseName();
	//获得视频流 
	byte[] getCourseVideo();
	//学习课程 
	void studyCourse();
	//退款 
	void refundCourse();
} 

我们可以把这个接口拆成两个接口,创建一个接口ICourseInfo和ICourseManager:
ICourseInfo接口:

public interface ICourseInfo { 
	String getCourseName();
	byte[] getCourseVideo();
 } 

ICourseManager接口:

public interface ICourseManager {
	void studyCourse(); 
	void refundCourse();
} 
  • 方法层面的单一职责设计
    通常我们会将一个方法写成如下所示:
private void modifyUserInfo(String userName,String address) { 
	userName = "Tom"; 
	address = "Changsha";
} 

显然,上面的modifyUserInfo方法承担了多个职责,既可以修改userName,又可以修改address,甚至更多,明显不符合单一职责原则。因此,我们将该方法拆分成两个:

private void modifyUserName(String userName){ 
	userName = "Tom"; 
} 
private void modifyAddress(String address){ 
	address = "Changsha"; 
} 

我们在编写代码的过程中,不可控因素太多,很多都不符合职责单一原则。但是在编写的过程中,还是要尽量让接口和方法保持单一职责,这样对项目后期的维护是会有很大的帮助的。

接口隔离原则

是指使用多个专门的接口,而不是单一的总接口。客户端不应该依赖它不需要的接口。该原则指导我们在设计接口时应该注意以下几点:

  1. 一个类对类的依赖应该建立在最小的接口之上。
  2. 建立单一接口,不要建立庞大臃肿的接口。
  3. 尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度)。

接口隔离原则符合我们所说的高内聚低耦合的设计思想,使得类具有很好的可读性、可扩展性、可维护性。在设计接口的时候,要多花时间思考业务模型,以及能对将来可能出现变更的地方做出预判。所以,对于抽象,对业务模型的理解是非常重要的。

下面以动物行为的抽象为例:

public interface Animal {
	void eat();
	void fly();
	void swim();
}

Bird类实现:

public class Bird implements Animal{

	@Override
	public void eat() {
	}

	@Override
	public void fly() {
	}

	@Override
	public void swim() {
	}

}

Dog类实现:

public class Dog implements Animal{

	@Override
	public void eat() {
	}

	@Override
	public void fly() {
	}

	@Override
	public void swim() {
	}

}

在上述的实现中,对于Bird 来说,swim()方法是永远也用不上的,同样,对于Dog来说,fly()方法也是永远用不上的。这时候,我们就需要为不同的动物设计不同的接口。分别设计 IEatAnimal,IFlyAnimal 和ISwimAnimal接口,来看代码:
IEatAnimal接口:

public interface IEatAnimal { 
	void eat();
}

IFlyAnimal 接口:

public interface IFlyAnimal { 
	void fly();
}

ISwimAnimal接口:

public interface ISwimAnimal{ 
	void swim();
}

Dog只实现IEatAnimal和ISwimAnimal接口:

public class Dog implements IEatAnimal,ISwimAnimal{

	@Override
	public void swim() {
		// TODO Auto-generated method stub
	}

	@Override
	public void eat() {
		// TODO Auto-generated method stub
	}
}

迪米特法则

是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则。尽量降低类与类之间的耦合。迪米特原则主要强调只和朋友交流,不与陌生人说话。出现在成员变量、方法的输入参数、输出参数中的类都可以称之为朋友,而出现在方法体内部的类不属于朋友类。

业务场景:
现在,Boss需要统计全部已上线的课程数量,这时候,Boss只需要找到TeamLeader去统计,TeamLeader将统计好的结果返回给Boss即可。
以代码为例:
Course类:

public class Course { 
} 

TeamLeader类:

public class TeamLeader { 
	public void checkNumberOfCourses(List<Course> courseList) {
		System.out.println("目前已发布的课程数量是:"+courseList.size());
	}
 } 

Boss类:

public class Boss {
	public void commandCheckNumber(TeamLeader teamLeader) {
		List<Course> courseList = new ArrayList<>();
		//模拟 Boss 一页一页往下翻页,TeamLeader 实时统计
		for(int i=0;i<20;i++) {
			courseList.add(new Course());
		}
		teamLeader.checkNumberOfCourses(courseList);
	}
}

测试代码:

public static void main(String[] args) {
	Boss boss = new Boss();
	TeamLeader teamLeader = new TeamLeader();
	boss.commandCheckNumber(teamLeader);
}

根据迪米特原则,Boss只需要和TeamLeader交流。它和Course 并不是朋友,不需要和它直接交流,TeamLeader统计需要用到Course 。
现在将其改造成符合迪米特法则的代码:
TeamLeader类:

public class TeamLeader {
	public void checkNumberOfCourses() {
		List<Course> courseList = new ArrayList<>();
		//模拟 Boss 一页一页往下翻页,TeamLeader 实时统计
		for(int i=0;i<20;i++) {
			courseList.add(new Course());
		}
		System.out.println("目前已发布的课程数量是:"+courseList.size());
	}
}

Boss类:

public class Boss {
	public void commandCheckNumber(TeamLeader teamLeader) {
		teamLeader.checkNumberOfCourses();
	}
}

测试代码:

public class LawOfDemeterTest {
	public static void main(String[] args) {
		Boss boss = new Boss();
		TeamLeader teamLeader = new TeamLeader();
		boss.commandCheckNumber(teamLeader);
	}
}

现在再看,Boss和Course 就没有关联了,符合迪米特法则。

合成复用原则

是指尽量使用对象组合\聚合,而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
继承我们叫白箱复用,相当于把所有的实现细节暴露给子类。组合\聚合也叫黑箱复用,对类以外的对象是无法获取到细节的。

以数据库操作为例:

public class DBConnection {
	public String getDBConnection() {
		return "Mysql数据库连接";
	}
}

新建一个ProductDao类:

public class ProductDao {
	private DBConnection dBConnection;
	public void setDBConnection(DBConnection dBConnection) {
		this.dBConnection = dBConnection;
	}
	public void addProduct() {
		String cocc = dBConnection.getDBConnection();
		System.out.println("使用"+cocc+"生成产品");
	}
}

测试代码:

public static void main(String[] args) {
	ProductDao productDao = new ProductDao();
	productDao.setDBConnection(new DBConnection());
	productDao.addProduct();
}

这是一种非常典型的合成复用原则应用场景。但是,目前的设计来说,DBConnection 不是抽象,不便于系统扩展。目前的系统仅支持Mysql,但是如果我们也想要让其支持Oracle怎么办呢?可以在DBConnection 类中增加对Oracle支持的方法,但是如此一来,就违背了开闭原则。其实,无需修改ProductDao ,我们只需将DBConnection修改为abstract即可。

修改后的DBConnection:

public abstract class DBConnection {
	public abstract String getDBConnection();
}

然后,将MySQL的逻辑抽离:

public class MysqlDBConnection extends DBConnection{
	@Override
	public String getDBConnection() {
		return "MySQL 数据库连接";
	}
}

再创建Oracle支持的逻辑:

public class OracleDBConnection extends DBConnection{
	@Override
	public String getDBConnection() {
		return "Oracle数据库连接";
	}
}

具体选择交给应用层来选择
测试代码:

public static void main(String[] args) {
	ProductDao productDao = new ProductDao();
	productDao.setDBConnection(new OracleDBConnection());
	productDao.addProduct();
}

里氏替换原则

是指如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都被替换成O2时,程序P的行为没有发生变化,那么类型T2是T1的子类型。
即:一个软件实体如果适用于父类的话,那一定适用于子类,所有使用父类的地方必须能透明地使用其子类的对象,子类对象能够替换其父类对象,而程序逻辑不变。

引申含义:子类可以扩展父类的功能,但不能改变父类的原有功能。

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  2. 子类中可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等。

回过头来看上面的开闭原则,我们在获取折后的时候重写了父类的getPrice()方法,增加了一个获取原来价格的方法getOriginPrice(),这样就违背了里氏替换原则。在此,修改一下,不应该覆盖getPrice()方法,增getDiscountPrice
()方法:

public class JavaDiscountCourse extends JavaCourse{

	public JavaDiscountCourse(Integer id, String name, Double price) {
		super(id, name, price);
	}
	public Double getDiscountPrice() {
		return super.getPrice() * 0.16;
	}
}

使用里氏替换原则有以下优点:
1、约束继承泛滥,开闭原则的一种体现。
2、加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩
展性。降低需求变更时引入的风险。

现在来描述一个经典的业务场景,用正方形、矩形和四边形的关系说明里氏替换原则。
正方形是一个特殊的长方形,因此创建一个长方形父类Rectangle类:

public class Rectangle {
	private long height; 
	private long width;
	public long getHeight() {
		return height;
	}
	public void setHeight(long height) {
		this.height = height;
	}
	public long getWidth() {
		return width;
	}
	public void setWidth(long width) {
		this.width = width;
	} 
	
}

然后创建一个正方形继承长方形:

public class Square extends Rectangle{
	private long length;

	public long getLength() {
		return length;
	}

	public void setLength(long length) {
		this.length = length;
	} 
	@Override 
	public long getWidth() { 
		return getLength(); 
	} 
	@Override 
	public long getHeight() { 
		return getLength(); 
	} 
	@Override 
	public void setHeight(long height) { 
		setLength(height); 
	} 
	@Override 
	public void setWidth(long width) { 
		setLength(width); 
	}

}

测试代码:

public class LiskovSubstitutionTest {
	
	public static void main(String[] args) {
		Rectangle rectangle = new Rectangle();
		rectangle.setWidth(20);
		rectangle.setHeight(10);
		resize(rectangle);
	}
	public static void resize(Rectangle rectangle){ 
		while (rectangle.getWidth() >= rectangle.getHeight()){ 
			rectangle.setHeight(rectangle.getHeight() + 1); 
			System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight()); 
		} 
		System.out.println("resize 方法结束" + "\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
	}
}

运行结果:

width:20,height:11
width:20,height:12
width:20,height:13
width:20,height:14
width:20,height:15
width:20,height:16
width:20,height:17
width:20,height:18
width:20,height:19
width:20,height:20
width:20,height:21
resize 方法结束
width:20,height:21

发现高比宽还大了,在长方形中是一种非常正常的情况。现在我们再来看下面的代码,
把长方形Rectangle替换成它的子类正方形Square,修改测试代码:

public class LiskovSubstitutionTest {
	
	public static void main(String[] args) {
		Square square = new Square();
		square.setLength(10);
		resize(square);
	}
	public static void resize(Rectangle rectangle){ 
		while (rectangle.getWidth() >= rectangle.getHeight()){ 
			rectangle.setHeight(rectangle.getHeight() + 1); 
			System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight()); 
		} 
		System.out.println("resize 方法结束" + "\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
	}
}

这时候我们运行的时候就出现了死循环,违背了里氏替换原则,将父类替换为子类后,程序运行结果没有达到预期。因此,我们的代码设计是存在一定风险的。里氏替换原则只存在父类与子类之间,约束继承泛滥。我们再来创建一个基于长方形与正方形共同的抽象四边形Quadrangle接口:

问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

在这里插入图片描述
对于这样不符合里氏替换原则原则的关系,我们在代码重构的时候一般采用下面的方法。
在这里插入图片描述
创建一个基于长方形与正方形共同的抽象四边形Quadrangle接口:

public interface Quadrangle {
	long getWidth(); 
	long getHeight();
}

修改长方形Rectangle类:

public class Rectangle implements Quadrangle{
	private long height; 
	private long width; 
	@Override
	public long getWidth() {
		return width;
	}
	@Override
	public long getHeight() {
		return height;
	}
	public void setHeight(long height) {
		this.height = height;
	}
	public void setWidth(long width) {
		this.width = width;
	}
	
}

修改正方形Square类:

public class Square implements Quadrangle{
	private long length; 
	public long getLength() { 
		return length; 
	} 
	@Override
	public long getWidth() {
		return length;
	}

	@Override
	public long getHeight() {
		return length;
	}
	
}

此时,如果我们把resize()方法的参数换成四边形Quadrangle类,方法内部就会报错。因为正方形Square已经没有了setWidth()和setHeight()方法了。因此,为了约束继承泛滥,resize()的方法参数只能用Rectangle长方形。

但是我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?来看一个例子。

public class A{
    public int func1(int a, int b){
        return a-b;
    }
}
public class B extends A{  
    public int func1(int a, int b){  
        return a+b;  
    }  

    public int func2(int a, int b){  
        return func1(a,b)+100;  
    }  
} 
public class Client{  
    public static void main(String[] args){  
        B b = new B();  
        System.out.println("100-50="+b.func1(100, 50));  
        System.out.println("100-80="+b.func1(100, 80));  
        System.out.println("100+20+100="+b.func2(100, 20));  
    }  
}  

输入结果:

100-50=150
100-80=180
100+20+100=220
我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值