软件架构设计原则

本文详细阐述了面向对象设计的七大原则:开闭原则、依赖倒置原则、单一职责原则、接口隔离原则、迪米特法则、里氏替换原则和合成复用原则。通过实例分析,展示了如何在代码中应用这些原则,以提高软件的可扩展性和可维护性。
摘要由CSDN通过智能技术生成

开闭原则

定义:指一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
核心思想:用抽象构建框架,面向抽象编程,用实现扩展细节,也是面向对象中最基础的设计原则

样例
tx课堂有很多课程可以学习,例如:java,golang等等,首先创建一个课程体系的接口ITXCourse

// TX课堂抽象接口
// 不打广告,是确实在用的平台
public interface ITXCourse {
    // 课程id
    Integer getId();

    // 课程名称
    String getName();

    // 课程价格
    Double getPrice();
}

接下来,我们看下java的课程,创建一个java的课程类


// java教程
public class JavaCourse implements ITXCourse{

    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;
    }
}

还有golang的课程类


public class GoLangCourse implements ITXCourse{
    private Integer id;

    private String name;

    private Double price;

    public GoLangCourse(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,防止有未知影响,这是可以重新创建一个java课程的促销类,继承JavaCourse 实现促销的扩展


public class JavaCoursePromotion extends JavaCourse {

    public JavaCoursePromotion(Integer id, String name, Double price) {
        super(id, name, price);
    }

    // 促销价格,打了八折
    public Double getPrice(){
        return super.getPrice() * 0.8;
    }
}

总结:开闭原则通俗的说是指,可以自由扩展,并且不会影响到原有的代码,如果扩展功能需要改动之前代码就已经违背了对扩展开放,对修改关闭

依赖倒置原则

定义:设计代码结构时,高层模块不应该依赖低层模块,而是都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象
核心思想:减少类与类之间的耦合性,将依赖抽象化,要面向接口编程,先顶层再细节来设计代码结构
样例
在前面提到的TX课堂举例了java课程和golang课程,但是只有课程基本信息,还缺少课程的学习方法,现在给课程接口添加学习的方法:

// 课程类接口
public interface ITXCourse {
    // 课程id
    Integer getId();

    // 课程名称
    String getName();

    // 课程价格
    Double getPrice();

    void study();
}

学习golnag


// java教程
public class JavaCourse implements ITXCourse{

    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;
    }

    @Override
    public void study(){
        System.out.println("学习java");
    }
}

我现在想学习java,也想学习golang

public class GuoHuPerson {
    // 学员实体类,学习多种课程
    public void study(ITXCourse txCourse){
        // 通过课程的抽象,调用学习的方法,学习课程
        txCourse.study();
    }
}

测试:

public static void main(String[] args) {
        GuoHuPerson guohuPerson = new GuoHuPerson();
        guohuPerson.study(new JavaCourse(1,"java",new Double(1000)));
        guohuPerson.study(new GoLangCourse(2,"golang",new Double(1000)));
    }

out:

学习java
学习golang

通过传入ITXCourse 的抽象完成课程的学习,把具体依赖抽离成抽象依赖,这种传入方式也叫依赖注入,在这个场景下,还可以选择Setter方式注入,代码修改如下:

public class GuoHuPerson {

    private ITXCourse txCourse;

    public void setCourse(ITXCourse course) {
        this.txCourse = course;
    }

    // 学员实体类,学习多种课程
    public void study(){
        // 通过课程的抽象,调用学习的方法,学习课程
        txCourse.study();
    }
}

 public static void main(String[] args) {
        GuoHuPerson guohuPerson = new GuoHuPerson();
        guohuPerson.setCourse(new JavaCourse(1,"java",new Double(1000)));
        guohuPerson.study();

        guohuPerson.setCourse(new GoLangCourse(1,"java",new Double(1000)));
        guohuPerson.study();
    }

out

学习java
学习golang

单一职责原则

定义:不要存在多于一个导致类变更的原因
核心思想:一个类/一个接口/一个方法 只做一件事情,不要过多冗余复杂逻辑
样例
假设java的课程部分时免费的,部分时收费的,那么对于学习同学就要做权限拦截,可以修改person的学习方法:

// 学员实体类,学习多种课程
    public void study(boolean isVip){
        if(isVip){
            // 简写,实际情况可能很复杂
            // 会涉及到vip课程各种权限和加密
            System.out.println("架构升级课");
        }else{
            System.out.println("基础免费课");
        }
    }

study方法现在非常多的逻辑,可以考虑拆分下

// 学习基础课程
    public void studyBasic(){
        System.out.println("基础免费课");
    }

    // 学习架构课程
    public void studyFramework(){
        // 简写,实际情况可能很复杂
        // 会涉及到vip课程各种权限和加密
        System.out.println("架构升级课");
    }
    public static void main(String[] args) {
        GuoHuPerson guohuPerson = new GuoHuPerson();
        guohuPerson.setCourse(new JavaCourse(1,"java",new Double(1000)));
        guohuPerson.studyBasic();
        guohuPerson.studyFramework();
    }

这个demo可能不是特别恰当,主题思想时方法或者类或者接口做拆分,一个方法只完成一件事,不要过多的冗余太多逻辑,方法的拆分之后,可以用组合的方式完成更多的事情

接口隔离原则

定义:多个专门的接口,而不使用单一的总接口
核心思想:高内聚低耦合的设计思想

  • 一个类对一类的依赖应该建立在最小的接口之上
  • 建立单一接口,不要建立庞大臃肿的接口
  • 尽量细化接口,接口中的方法尽量少(注意分析实际情况)
    样例
    曾经我们在很多地方见到了IAnimal抽象接口,这个接口包含吃,飞,叫,游泳等等操作
public interface IAnimal {
    // 吃
    void eat();

    // 飞
    void fly();

    // 叫
    void call();

    // 游泳
    void swim();
}

从实际角度看,狗肯定不会飞,鸟也未必会游泳,所以可以针对操作类型设计针对性的接口,IEatAnimal,IFlyAnimal,ICallAnimal,ISwimAnimal

// 具有吃行为的动物抽象
public interface IEatAnimal {
    void eat();
}

// 具有叫行为的动物抽象
public interface ICallAnimal{
    void call();
}

// 具有游泳行为的动物抽象
public interface ISwimAnimal{
    void swim();
}

具体的dog实体类:


public class Dog implements IEatAnimal,ICallAnimal,ISwimAnimal{

    @Override
    public void call() {
        System.out.println("叫");
    }

    @Override
    public void eat() {
        System.out.println("吃");
    }

    @Override
    public void swim() {
        System.out.println("游泳");
    }
}

如果时鱼类,可以实现 游泳接口 和 吃接口
注意:并不是一个接口只有一个方法,在设计接口的过程中,是按照实际的公共的属性进行抽象,具体要看所看实际的应用场景,多用组合

迪米特法则

定义:指一个对象应该对其他对象保持最少的了解
核心思想:尽量降低类与类之间的耦合
样例
某天leader想知道tx课堂有多少学员报名了学习

public class Admin {
    public void SumStudentCount(List<Person> personList){
        System.out.println("有"+personList.size()+"个学员报名了");
    }
}

public class Learder {
    public void SumStudentCount(Admin admin){
        List<Person> personList= new ArrayList<Person>();
        for (int i= 0; i < 5 ;i ++){
            personList.add(new Person());
        }
        admin.SumStudentCount(personList);
    }
}

不过感觉这么写不太好,leader没必要关系具体的学员,只需要管理员告诉他下有多少学生count就好了,所以改造下

public class Learder {
    public void SumStudentCount(Admin admin){
        admin.SumStudentCount(); // admin告诉leader 有多少count	
    }
}
public class Admin {
    public void SumStudentCount(){
        List<Person> personList = new ArrayList<Person>();
        for (int i= 0; i < 5 ;i ++){
            personList.add(new Person());
        }
        System.out.println("有"+personList.size()+"个学员报名了");
    }
}

让leader与学员解耦,需要的信息从admin中获取即可

里氏替换原则

定义:子类对象能够替换父类对象,而程序逻辑不变
核心思想:子类可以扩展父类的功能,但不能改变父类原有的功能
注意点:

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

优缺点分析
优点

  • 约束继承泛滥,开闭原则的一种体现
  • 加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩展性,降低需求变更时引入的风险

缺点

  • 通过继承来实现,降低了子类的灵活性,同时继承也会将父类的实现细节暴露给子类
  • 增加了类与类之间的耦合性,父类如果发生变更需要考虑到子类,不利于代码的维护

样例
在开闭原则中的举例就违背了里氏替换原则,因为重写了getPrice方法,如果要符合开闭原则,则在子类中重新定义自己的打折促销方法

public class JavaCoursePromotion extends JavaCourse {

    public JavaCoursePromotion(Integer id, String name, Double price) {
        super(id, name, price);
    }

    // 原始价格
    public Double getOriginPrice(){
        return super.getPrice();
    }

    // 促销价格,打了八折
    public Double getPromotionPrice(){
        return super.getPrice() * 0.8;
    }
}

合成复用原则

定义:尽量使用对象组合/聚合少用继承,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
核心思想:多用组合/聚合,少用继承

  • 组合:组合(合成)是一种强的“拥有”关系
  • 聚合:聚合表示一种弱的“拥有”关系

样例
在依赖倒置的分析中其实已经用到了合成复用原则,当时使用的是GuoHuPerson 的Setter方式注入,让person内部调用了ITXCourse 的方法

public class GuoHuPerson {

    private ITXCourse txCourse;

    public void setCourse(ITXCourse course) {
        this.txCourse = course;
    }

    // 学员实体类,学习多种课程
    public void study(){
        // 通过课程的抽象,调用学习的方法,学习课程
        txCourse.study();
    }
}

 public static void main(String[] args) {
        GuoHuPerson guohuPerson = new GuoHuPerson();
        guohuPerson.setCourse(new JavaCourse(1,"java",new Double(1000)));
        guohuPerson.study();

        guohuPerson.setCourse(new GoLangCourse(1,"java",new Double(1000)));
        guohuPerson.study();
    }

out

学习java
学习golang

学员和课程并没有继承关系,但却使用了课程的功能

总结

设计原则是一种规范,但并不是所有代码都要符合设计原则,具体情况要具体分析,适合的场景用适合的原则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值