软件设计的七大原则

软件设计的七大原则

内容定位

学习设计原则,是学习设计模式的基础。在实际开发过程中,并不是一定要求所以代码都遵循设计原则,我们要考虑人力、成本、时间、质量,不是刻意追求完美,要在适当的场景遵循设计原则,体现的是一种平衡取舍,帮助我们设计出更优雅的代码结构。

开闭原则

开闭原则(Open-Closed Principle,OCP)是指一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。所谓开闭,也正是对扩展个修改两个行为的一个原则。强调的是用抽象构建框架,用实现扩展细节。可以提高软件系统的可复用性及可维护性。开闭原则,是面向对象设计中最基础的设计原则。它指导我们如何建立稳定灵活的系统,例如我们版本更新,我尽可能不修改源代码,但是可以增加新功能。
实现开闭原则的核心思想就是面向抽象编程,接下来我们看一段代码:
超市中,贩卖的各种商品,首先创建一个商品的接口:

public interface IGoods {
	String getName( );
	Double getPrice();
}

接口提供两个方法,获取商品名称和商品售价。超市里有很多商品,我们以其中一个为例,如大米:

publiC Class Rice implements IGoods {
	private String name;
	private Double price ;
	public Rice(String name , Double price){
		this.name = name;
		this.price = price ;
	}
	public String getName() {
		return this . name;
	}
	public Double getPrice() {
		return this.price ;
	}
}	

它实现了IGoods商品的接口,并实现接口的两个方法,并提供一个带参的构造函数。具体应用:

public class Test
	public static void main(String[] args) {
		IGoods goods = new Rice( name:“大米",price: 100 .00) ;
		System. out . printIn("商品名称:“+goods . getName ()+" \n商品售价:+goods . getPrice()); 
	}
}

结果:

商品名称:大米
商品售价: 100.0

最近超市为大米搞促销,给大米打8折出售,如果我们直接修改Rice的getPrice()方法,则会存在一定风险,可能影响其他地方的调用结果。我们如何在不修改原有代码前提下,实现价格优惠这个功能呢?现在我们再写一个处理优惠逻辑的类, RiceDisciuntCourse类:

public class RiceDisciuntCourse extends Rice {
	public RiceDisciuntCourse(String name, Double price) {
		super(name, price);
	}
	public Double getOriginalPrice(){
		return super . getPrice();
	}
	public Double getPrice(){
		return super .getPrice() * 0.8;
	}
}

最后测试代码:

public class Test {
	public static void main(String[] args){
		IGoods goods = new Rice( name: “大米”,price: 100 .00);
		System.out . printIn("商品名称: +goods . getName()+"\n打折前 ,商 品售价 :"+goods . getPrice()):
		IGoods disGoods = new RiceDisciuntCourse( name: "大米",price: 100 .00) ;
		System.out . printIn("打折后,商品售价:+disGoods . getPrice()):
}

结果:

商品名称:大米
打折前,商品售价: 100.0
打折后,商品售价: 80.0

这样就在不修改原有代码的情况下,实现了价格优惠这个功能。
回顾一下,简单的类结构图:
在这里插入图片描述

依赖倒置原则

依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层模块不应依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。通过依赖倒置,可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性,并能够降低修改程序所造成的风险。接下来看一个案例:
Tom是一个热爱学习的人,目前他正在学习Java和Python两门课程:

public class Tom {
	public void studyJavaCourse(){
		System. out . println("Tom正在学习Java课程");
	}
	public void studyPythonCourse(){ 
		System . out . print1n("Tom正在学习Python课程");
	}
}

高层调用:

public class Test {
	public static void main(String[] args) {
		Tom tom = new Tom();
		tom. studyJavaCourse();
		tom . studyPythonCourse(); 
	}
}

随着Tom的学习热情高涨,现在他有想学习AI人工智能课程。这个时候,业务扩展,我们的代码也要从底层到高层统一一次修改。在Tom类中增加StudyAICourse()的方法,在高层也要追加调用。如此一来,系统发布以后,实际上是非常不稳定的,在修改代码的同时也会带来意想不到的风险。接下来我们优化代码,创建一个课程的抽象ICourse接口:

public interface ICourse{
	void study();
}

然后写一个StudyJavaCourse类:

public class Study JavaCourse implements ICourse {
	public void study() {
		System. out. print1n("Tom正在学习Java课程");
	}
}

再写一个StudyPythonCourse类:

public class StudyPythonCourse implements ICourse {
	public void study() {
		System. out . print1n("Tom正在学习Python课程");
	}
}

写一个StudyAICourse类:

public class StudyAICourse implements ICourse {
	public void study() {
		System. out . println("Tom正在学习AI课程" );
	}
}

Tom类改造:

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

最后高层调用:

publiC Class Test {
	public static void main(String[] args) {
		Tom tom = new Tom();
		tom. study(new StudyJavaCourse());
		tom. study(new StudyPythonCourse());
	tom. study(new StudyAICourse());
	}
}

结果:

Tom正在学习Java课程
Tom正在学习Python课程
Tom正在学习AI课程

这样,以后无论Tom的学习热情如何暴涨,学习多少新课程,我们只需要新增加类,通过传参的方式告诉Tom,而不需要修改底层代码。实际上这是一种大家非常熟悉的方式,叫依赖注入。
现在我们来看下最终的类图:
在这里插入图片描述
大家要切记:以抽象为基准比以细节为基准搭建起来的架构要稳定的多,因此大家在拿到需求之后,要面向接口编程,先顶层再细节来设计代码结构。

单一职责原则

单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。假设我们有一个Class负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能会导致另一个职责的功能发生故障。这样一来,这个Class存在两个导致类变更的原因。如何解决这个问题呢?我们就要给两个职责分别用两个Class类实现,进行解耦。后期需求变更维护互不影响。这样的设计,可以降低类的复杂度。提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说就是一个Class/Interface/Method只负责一项职责。
接下来,我们来看代码实例,还是用课程举例,我们的课程有直播课和录播课。直播课不能快进和快退,录播可以任意的反复观看,功能职责不一样。还是先创建一个Course类:

public class Course {
	public void study (String courseName){
		if("直播课”. equals( courseName)){
			System. out . println("不能快进" );
		}else{
			System . out . print1n("可以任意的来回播放" );
		}
	}
}	

看代码调用:

public class Test {
	public static void main(String[] args) {
		Course course = new Course();
		course. study( courseName: "直播课");
		course. study( courseName: "录播课");
	}
}

从上面的代码来看,Course类承担了两种处理逻辑。假如,现在要对课程加密,那么直播课和录播课的加密逻辑都不一样,必须要修改代码。而修改代码逻辑势必会相互影响造成不可控的风险。我们对职责进行分离解耦,来看代码,分别创建两个类ReplayCourse和LiveCourse:

public class L iveCourse {
	public void study(String courseName ){
		System. out . println( courseName+"不能快进观看");
	}
}
public class ReplayCourse {
	public void study(String courseName){
		System . out . println( courseName+可以任意来回播放");
	}
}

调用代码:

public class Test {
	public static void main(String[] args) {
		/*Course course = new Course();
		course. study("直播课");
		course . study("录播课");*/
		LiveCourse liveCourse = new LiveCourse();
		liveCourse. study( courseName: ” 直播课" );
		ReplayCourse replayCourse = new ReplayCourse();
		replayCourse. study( courseName: "录播课" );
	}
}

业务继续发展,课程要做权限。没有付费的学员可以获取课程基本信息,已经付费的学院可以获得视频流,即学习权限。那么对于控制课程层面上至少有两个职责。我们 可以把展示职责和管理职责分离开来,都实现同一个抽象依赖。设计一个顶层接口,创建ICourse接口:

public interface ICourse {
	//获取基本信息
	String getCourseName();
	//获取视频流
	byte[] getCourseVideo();
	//学习课程
	void studyCourse(); 
	//退款
	void refunCourse();
}

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

public interface ICourseInfo {
	String getCourseName(); 
	byte[] getCoursevideo();
}
public interface ICourseManager {
	void studyCourse();
	void refundCourse();
}

来看下类图:
在这里插入图片描述
下面我们来看下方法层的单一职责设计。有时候,我们为了偷懒,通常会把一个方法写成下面这样:

   private void modifyUserInfo(String userName,String address){
        userName = "Tom";
        address = "Changsha";
    }

还可能写成这样:

private void modifyUserInfo(String userName,String address,boolean bool){
        if(bool){

        }else{

        }
        userName = "Tom";
        address = "Changsha";
    }

显然,上面的modifyUserInfo()方法中都承担了多个职责,既可以修改userName,也可以修改address,甚至更多,明显不符合单一职责。那么我们做如下修改,把这个方法拆成两个:

    private void modifyUserName(String userName){

    }

    private void modifyAddress(String address){

    }

接口隔离原则

接口隔离原则(Interface Segregation Priciple,ISP)是指用多个专用的接口,而不适用单一的总接口,客户端不应该依赖它不需要的接口。这恶原则指导我们再设计接口时应当注意以下几点:

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

接口隔离原则符合我们常说的搞内聚低耦合的设计思想,从而使得类具有很好的可读性、
可扩展性和可维护性。我们再设计接口的时候,要多花时间去思考,要考虑业务模型,包括以后有可能发生的变更的地方还要做些预判。所以,对于抽象,业务模型的理解非常重要。下面我们来看一段代码,写一个动物的抽象:
Ianimal接口:

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

Bird类实现:

public class Bird implements IAnimal {
    @Override
    public void eat() {

    }
    @Override
    public void fly() {

    }
    @Override
    public void swim() {

    }
}

Dog类实现:

public class Dog implements IAnimal {
    @Override
    public void eat() {

    }
    @Override
    public void fly() {

    }
    @Override
    public void swim() {

    }
}

可以看出,Bird的swim()方法可能智能空着,Dog的fly()方法显然不可能的。这个时候,我们针对不同动物行为来设计不同接口,分别设计IeatAnimel,IflyAnimel和IswimAnimel接口,来看代码:
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 eat() {

    }

    @Override
    public void swim() {

    }
}

Brid类只实现IEatAnimal和IFlyAnimal接口:

public class Bird implements IFlyAnimal,IEatAnimal {
    @Override
    public void fly() {

    }

    @Override
    public void eat() {

    }
}

接下来看下两种类图的对比:
在这里插入图片描述
在这里插入图片描述

迪米特法则

迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合。迪米特原则主要强调只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类,出现在方法体内部的类不属于朋友类。
现在来看代码,每个班级都有个班主任和一个班长,老师通过班长来了解当月班上学生迟到人数:
迟到学生类,LateStudents:

public class LateStudents {
}

班长类SquadLeader:

public class SquadLeader {
    public void countLateStudents(List<LateStudents> list){
        System.out.println("迟到人数为:"+list.size());
    }
}

班主任类ClassTeacher:

public class ClassTeacher {
    public void getLateStudents(SquadLeader squadLeader){
        List<LateStudents> list = new ArrayList<LateStudents>();
        for(int i = 0 ; i < 20 ; i++){
            list.add(new LateStudents());
        }
        squadLeader.countLateStudents(list);
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        ClassTeacher classTeacher = new ClassTeacher();
        classTeacher.getLateStudents(new SquadLeader());
    }
}

写到这里,其实功能已经都实现了,代码看上去也没什么问题。根据迪米特原则,ClassTeacher只想要结果,不需要跟LateStudents产生交流。而SquadLeader统计需要引用LateStudents对象。ClassTeacher和LateStudents并不是朋友,从类图就可以看粗来:
在这里插入图片描述
下面来对代码进行改造,
班长类SquadLeader:

public class SquadLeader {
    public void countLateStudents(){
        List<LateStudents> list = new ArrayList<LateStudents>();
        for(int i = 0 ; i < 20 ; i++){
            list.add(new LateStudents());
        }
        System.out.println("迟到人数为:"+list.size());
    }
}

班主任类ClassTeacher:

public class ClassTeacher {
    public void getLateStudents(SquadLeader squadLeader){
        squadLeader.countLateStudents();
    }
}

再来看下类图,ClassTeacher和LateStudents已经没有关系了:
在这里插入图片描述

里氏替换原则

里氏替换原则(Liskov Substitution Principile,LSP)是指如果每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所以程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
定义看上去还是比较抽象,我们重新理解下,可以理解为一个软件实体如果适用一个父类的话,那一定是适用其子类,所有应用父类的地方必须能透明的使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,我们总结一下:
引申含义:子类可以扩展父类的功能,但不能改变父类原有的功能。
1、 子类可以实现父类的抽象功能,但不能覆盖父类的非抽象方法。
2、 子类中可以增加自己特有的方法。
3、 当子类的方法重载父类的方法时,方法前置条件(即方法的输入/入参)要比父类的输入参数更宽松。
4、 当子类方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等。
在前面讲开闭原则的时候埋下一个伏笔,我们记得在获取打折时重写覆盖了父类的getPrice()方法,增加了获取原价格的方法getOriginPrice(),显然就违背了里氏替换原则。我们修改一下代码,不应该覆盖getPrice()方法,增加getDiscountPrice()方法:

public class RiceDisciuntCourse extends Rice {
    public RiceDisciuntCourse(String name, Double price) {
        super(name, price);
    }

    public Double getOriginalPrice(){

        return super.getPrice();
    }

    /*public Double getPrice(){

        return super.getPrice() * 0.8;
    }*/

    public Double getDiscountPrice(){
        return  super.getPrice()*0.8;
    }
}

使用里氏替换有一下有点:
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;
	}
}

创建正方形Square类继承长方形:

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

在测试中创建resize()方法,根据逻辑,长方形的宽应该大于等于高,我们让高一直自增长,直到高等于宽变成正方形:

public static void resize(Rectangle rectangle){
	while (rectangle. getwidth() >= rectangle.getHeight()){
		rectangle . setHe ight(rectangle . getHeight()+1);
		System . out . print In( "Width:"+rectangle . getwidth()+" ,Height:" +rectangle . getHeight(O);
		System . out . print1n( "Resize End ,Width:"+rectangle . getHidth()+"Heigth: "+rectangle. getHeight());
}

测试代码:

public static void main(String[] args) {
	Rectangle rectangle = new Rectangle();
	rectangle . setHeight(10); 
	rectangle . setWidth(20); 
	resize(rectangle);| 
}

运行结果:

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 End ,Width: 20 ,Heigth:21

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

    public static void main(String[] args) {
        Square square = new Square();
        square.setHeight(10);
        square.setWidth(20);
        resize(square);
    }

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

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

修改长方形Rectangle类:

public class Rectangle implements QuadRangle {
    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;
    }
}

修改正方形类Square类:

public class Square implements QuadRangle {
    private long length;

    public long getLength(){
        return length;
    }

    public void setLength(long length){
        this.length = length;
    }

    public long getHeight(){
        return getLength();
    }

    public long getWidth(){
        return getHeight();
    }

}

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

合成复用原则

合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)/聚合(contanis-a),而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
继承我们叫白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合也称之为黑箱复用,对类以外的对象是无法获取到实现细节的。要根据具体业务场景来做代码设计,其实也都需要遵循OOP模型。还是以数据库操作为例,先来创建DBconnection类:

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

创建ProdctDao类:

public class ProductDao {
    private DBConnection dbConnection;
    public void setDbConnection(DBConnection dbConnection){
        this.dbConnection = dbConnection;
    }
    public void addProductDao(){
        String conn = dbConnection.getConnection();
        System.out.println("使用"+conn+"增加产品");
    }
}

这是一种非常典型的合成复用原创应用场景。但是,目前的设计来说,DBConnection还不是一种抽象,不便于系统扩展。目前系统支持MySql数据库连接,假设业务发生变化,数据库操作层还要支持Oracle数据库,当然,我们可以在DBConnection中增加对Oracle数据支持的方法。但是违背 了开闭原则,其实我们可以不必修改Dao的代码,将DBConnection修改为abstract,来看代码:

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

然后,将Mysql的逻辑抽离:

public class MySqlConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "MySql 数据库连接";
    }
}

再创建Oracle支持的逻辑:

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

具体选择交给应用层,来看下类图:
在这里插入图片描述

设计原则总结

学习设计原则,是学习设计模式的基础。再实际开发过程中,并不是一定要求所有代码都遵循设计原则,我们要考虑人力、时间、成本、质量,不是刻意追求完美,要再适度的场景遵循设计原则,体现的是一种平衡取舍,帮助我们设计出更加优雅的代码结构。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值