3. 软件设计原则

软件设计原则

开闭原则

对扩展开放,对修改关闭。也就是说在程序需要进行扩展的时候,不能修改原有代码,要实现热插拔的效果。简而言之,是为了程序的更好的扩张和升级。

要想要达到这种的效果,我们需要使用到接口和抽象类。

因为抽象类灵活性好,适应性广,只要抽象地比较合理,基本可以保持软件架构的稳定性。而软件易变的细节可以通过抽象类的派生类来实现,就相当于是定义规范。

例如:搜狗输入法的皮肤,我们就可以看做是一个抽象类,皮肤可以随意更换,其实就是基于皮肤这种规则来进行的代码实现。

/**
 * 抽象皮肤类,只要继承抽象皮肤类就可以无限扩展
 */
public abstract class AbstractSkin {
    // 显示的方法
    public abstract void display();
}
/**
 * 默认皮肤类
 */
public class DefaultSkin extends AbstractSkin{
    @Override
    public void display() {
        System.out.println("默认皮肤");
    }
}
/**
 * 黑马皮肤类
 */
public class HeimaSkin extends AbstractSkin {
    @Override
    public void display() {
        System.out.println("黑马程序员皮肤");
    }
}
/**
 * 搜狗输入法
 */
public class SougouInput {
    private AbstractSkin skin;
    public void setSkin(AbstractSkin skin) {
        this.skin = skin;
    }
    // 搜狗输入法展示
    public void display(){
        skin.display();
    }
}
public class Client {
    public static void main(String[] args) {
        SougouInput input = new SougouInput();
        // 创建皮肤对象,想要什么皮肤就 new 什么皮肤
        DefaultSkin skin = new DefaultSkin();
        input.setSkin(skin);
        input.display();
    }
}

里氏替换原则

任何基类可以出现的地方,子类一定可以出现。通俗的说,子类可以扩展父类的功能,但是不能更改父类原来的功能。所以说,子类尽量不要重写父类的方法,这样会让重用性变差,新加功能会更好。

正方形是长方形,而长方形不是正方形,所以针对于这种情况,正方形继承长方形不是个好选择,更好的方法是抽象出一个四边形类,两者去继承四边形。

/**
 * 四边形接口
 */
public interface Quadrilateral {
    double getHeight();
    double getWidth();
}
/**
 * 长方形类
 */
@AllArgsConstructor
public class Rectangle implements Quadrilateral{
    private double width;
    private double height;
    @Override
    public double getHeight() {
        return height;
    }
    @Override
    public double getWidth() {
        return width;
    }
}
/**
 * 正方形类
 */
@AllArgsConstructor
public class Square implements Quadrilateral {
    private double side;
    @Override
    public double getHeight() {
        return side;
    }
    @Override
    public double getWidth() {
        return side;
    }
}

依赖倒转原则

高层模块不应该依赖于低层模块,两者都应该依赖于低层模块的抽象。简单来说就是对抽象编程,具体实现是细节问题。

现在有 A 类、B 类,其中 A 类用到了 B 类中的内容,这个时候 A 类叫做高层模块,B 类叫做低层模块。那么 A 类不应该依赖于 B 类,而应该依赖于 B 类的抽象。

举个例子:

现在我们有一台电脑(高层模块),有各个配件(低层模块):主板、CPU、散热器、内存条、显卡、电源……。

组装电脑的精髓就是在于挑选各个配件进行组合,挑选出出电脑的最高性价比,也就是说你的各个配件不能是固定的品牌。以 CPU 举例子,我只知道我需要一个 CPU,而具体是 Intel 的还是 AMD 的,具体是什么型号的,这些不是在一开始要去操心的,这是细节问题。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Computer {
  private CPU cpu;
  private Disk disk;
  private Memory memory;
}
public interface CPU {
}
public class AMD implements CPU{
}
public class Intel implements CPU{
}
public interface Disk {
}
public class Xishu implements Disk{
}
public interface Memory {
}
public class WeiGang implements Memory{
}
/**
 * 依赖倒转
 */
public class RelyOnReverse {
  public static void main(String[] args) {
    Computer computer = new Computer();
    computer.setCpu(new AMD());
    computer.setDisk(new Xishu());
    computer.setMemory(new WeiGang());
  }
}

接口隔离原则

简单来讲就是实现最小的接口。

比如接口 A 有方法 1 和 方法 2,但是类 B 只需要实现方法 1 的功能,那么它去实现接口 A 就多余了方法 2,这样就违背了接口隔离原则。

迪米特法则

如果两个实体之间不需要直接的通信,那么就不需要直接的调用,而是可以通过第三方的转发来进行调用。目的就是为了降低耦合,提高模块之间的独立性。

比如说,如果要租房,找的其实是中介而不是房东。如果要做软件,找的应该是软件公司而不是具体的工程师。

合成复用

类的复用通常来说分为:继承复用、合成复用。

合成复用的意思是指:尽量优先使用组合或者聚合的关联关系来实现操作,其次才考虑使用继承关系来实现。

我们首先要考虑合成复用而不是继承复用,因为继承复用虽然实现起来简单,但是存在以下缺点:

  • 继承复用破坏了类的封装性,因为继承会将实现细节暴露给子类。父类对子类是透明的,所以继承复用又被称为白箱复用。
  • 子类和父类的耦合度高,父类的任何实现改变都会改变子类,这不利于类的扩展和维护。
  • 限制了复用的灵活性,因为从父类继承来的实现是静态的,在编译时就已经定义了,所以在运行时不可能发生变化。

采用组合或者聚合复用时,可以将已有对象纳入到新的对象中,成为新对象的一部分,新对象可以调用原有对象,这有以下好处:

  • 维持了类的封装性,因为类的内部实现细节不会对新对象开放。
  • 对象之间的耦合度低。
  • 这样可以在类的成员位置声明为抽象,复用的灵活性更高,这样的复用可以在运行时动态进行,新对象可以动态引用类型相同的对象。