软件设计原则
开闭原则
对扩展开放,对修改关闭。也就是说在程序需要进行扩展的时候,不能修改原有代码,要实现热插拔的效果。简而言之,是为了程序的更好的扩张和升级。
要想要达到这种的效果,我们需要使用到接口和抽象类。
因为抽象类灵活性好,适应性广,只要抽象地比较合理,基本可以保持软件架构的稳定性。而软件易变的细节可以通过抽象类的派生类来实现,就相当于是定义规范。
例如:搜狗输入法的皮肤,我们就可以看做是一个抽象类,皮肤可以随意更换,其实就是基于皮肤这种规则来进行的代码实现。
/**
* 抽象皮肤类,只要继承抽象皮肤类就可以无限扩展
*/
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,这样就违背了接口隔离原则。
迪米特法则
如果两个实体之间不需要直接的通信,那么就不需要直接的调用,而是可以通过第三方的转发来进行调用。目的就是为了降低耦合,提高模块之间的独立性。
比如说,如果要租房,找的其实是中介而不是房东。如果要做软件,找的应该是软件公司而不是具体的工程师。
合成复用
类的复用通常来说分为:继承复用、合成复用。
合成复用的意思是指:尽量优先使用组合或者聚合的关联关系来实现操作,其次才考虑使用继承关系来实现。
我们首先要考虑合成复用而不是继承复用,因为继承复用虽然实现起来简单,但是存在以下缺点:
- 继承复用破坏了类的封装性,因为继承会将实现细节暴露给子类。父类对子类是透明的,所以继承复用又被称为白箱复用。
- 子类和父类的耦合度高,父类的任何实现改变都会改变子类,这不利于类的扩展和维护。
- 限制了复用的灵活性,因为从父类继承来的实现是静态的,在编译时就已经定义了,所以在运行时不可能发生变化。
采用组合或者聚合复用时,可以将已有对象纳入到新的对象中,成为新对象的一部分,新对象可以调用原有对象,这有以下好处:
- 维持了类的封装性,因为类的内部实现细节不会对新对象开放。
- 对象之间的耦合度低。
- 这样可以在类的成员位置声明为抽象,复用的灵活性更高,这样的复用可以在运行时动态进行,新对象可以动态引用类型相同的对象。