1.开闭原则
对扩展开放,对修改关闭。在程序需要进行扩展的时候,不能去修改原有的代码,要去实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
下面是输入法设置皮肤的例子:
// 抽象皮肤接口 public interface Skin { // 显示的方法 void display(); } // 默认皮肤类 public class DefaultSkin implements Skin { @Override public void display() { System.out.println("默认皮肤"); } } // 下班了皮肤 public class OffWorkSkin implements Skin{ @Override public void display() { System.out.println("下班了皮肤"); } } // 输入法类 @Data public class Input { // 设置皮肤 private Skin skin; public void display() { skin.display(); } } // 测试 public static void main(String[] args){ // 创建输入法对象 Input input = new Input(); // 创建皮肤对象 Skin defaultSkin = new DefaultSkin(); // 设置默认皮肤 input.setSkin(defaultSkin); // 显示皮肤 input.display(); // 默认皮肤 // 换成下班了皮肤 Skin offWorkSkin = new OffWorkSkin(); input.setSkin(offWorkSkin); input.display();// 下班了皮肤 }
2.单一职责原则
就一个类而言,应该仅有一个引起变化的原因。应该只有一个职责。
说白了就是,一个类或者一个方法就应该干一件事情。
下面是修改用户的例子:
public class UpdateUser { // 不符合单一职责原则 public void updateUser(String type, String oldPassword, String newPassword, String oldUserName, String newUserName) { if ("修改密码".equals(type)) { System.out.println(oldPassword + "修改密码为:" + newPassword); } else if ("修改账号".equals(type)) { System.out.println(oldUserName + "修改账号" + newUserName); } } // 符合单一职责原则 public void updatePassword(String oldPassword, String newPassword) { System.out.println(oldPassword + "修改密码为:" + newPassword); } public void updateUserName(String oldUserName, String newUserName) { System.out.println(oldUserName + "修改账号" + newUserName); } }
可以看到上者是根据操作类型进行区分, 不同类型执行不同的逻辑,把修改账号和修改密码这两件事耦合在一起了,如果客户端在操作的时候传错了类型, 那么就会发生错误;
下者则把修改账号和修改密码逻辑分开,各自执行各自的职责,互不干扰,功能清晰明了,符合单一职责原则。
3.依赖倒置原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单来说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
下面是一个组装电脑的例子(简单举例电脑只保留CPU吧,嘻嘻嘻):
// Intel处理器类 public class IntelCpu { public void getCpu() { System.out.println("使用Intel处理器"); } } // 电脑 @Data public class Computer { private IntelCpu intelCpu; // CPU public void run() { intelCpu.getCpu(); } } // 测试 public static void main(String[] args){ // 创建电脑对象 Computer computer = new Computer(); // 设置CPU computer.setIntelCpu(new IntelCpu()); computer.run(); // 使用Intel处理器 }
可以看到上面的组装的电脑只能使用Intel的CPU,假如我想换成AMD的CPU不行。这就是违反了依赖倒置原则。修改后的代码如下:
// CPU类 public interface Cpu { void getCpu(); } // Intel处理器类 public class IntelCpu implements Cpu { @Override public void getCpu() { System.out.println("使用Intel处理器"); } } // Amd处理器类 public class AmdCpu implements Cpu { @Override public void getCpu() { System.out.println("使用Amd处理器"); } } // 电脑 @Data public class Computer { private Cpu cpu; public void run() { cpu.getCpu(); } } // 测试 public static void main(String[] args){ // 创建电脑对象 Computer computer = new Computer(); // 设置CPU为Intel处理器 computer.setCpu(new IntelCpu()); computer.run(); // 使用Intel处理器 // 设置CPU为Amd处理器 computer.setCpu(new AmdCpu()); computer.run(); // 使用Amd处理器 }
此时,当用户需要更换CPU时,只需要创建实现类去实现CPU接口而不需要修改原本的接口代码,这就符合了开闭原则。
4.接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
下面用一个安全门的例子:
现在有A品牌的安全门具有防火防盗功能,于是把防火防盗的功能提取出来一个接口,即:
// 安全门接口 public interface Door { //防火 void fireproof(); //防水 void waterproof(); } // 安全门接口 public interface Door { //防火 void fireproof(); //防水 void waterproof(); }
那么假如我们现在又有一个B品牌的安全门,只有防水功能的呢?那么显然是不能直接实现安全门的接口的,那么应该如何改进呢?改进如下:
// 防火功能 public interface FireProof { void fireProof(); } // 防水功能 public interface WaterProof { void waterProof(); } // A类门,具有防火防水功能 public class ADoor implements WaterProof, FireProof { @Override public void fireProof() { System.out.println("A品牌安全门防火功能"); } @Override public void waterProof() { System.out.println("A品牌安全门防水功能"); } }// B类安全门 public class BDoor implements WaterProof { @Override public void waterProof() { System.out.println("B品牌安全门防水功能"); } }
这样改进以后可以看到,当不同的品牌的安全门具有不同的功能的时,有什么功能就实现什么功能的接口,假如以后还有一个C品牌类的安全门具有防火防水防盗的功能,那么可以加一个防盗的接口,然后C类门去实现防火防水防盗的接口即可。这就是接口隔离原则。
5.迪米特法则
迪米特法则又叫最少知识原则。如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
下面是一个明星,粉丝,经纪公司的例子:
// 明星类 @Data @AllArgsConstructor @NoArgsConstructor public class Star { private String name; } // 粉丝类 @Data @AllArgsConstructor @NoArgsConstructor public class Fans { private String name; } // 经纪公司类 @Data @AllArgsConstructor @NoArgsConstructor public class Company { private String name; } @Data @AllArgsConstructor @NoArgsConstructor public class Agent { private Star star; private Fans fans; private Company company; //和粉丝见面的方法 public void meeting(){ System.out.println(star.getName() + "和粉丝"+fans.getName() + "见面"); } //和媒体公司洽谈的方法 public void business(){ System.out.println(star.getName() + "和" + company.getName() + "洽谈"); } } // 测试 public static void main(String[] args) { //创建明星对象 Star star = new Star("严晓波"); //创建粉丝对象 Fans fans = new Fans("彭晓锋"); //创建公司对象 Company company = new Company("杨永信电疗娱乐有限公司"); //创建经纪人对象 Agent agent = new Agent(star, fans, company); agent.meeting();// 严晓波和粉丝彭晓锋见面 agent.business();// 严晓波和杨永信电疗娱乐有限公司洽谈 }
这里明星的日常事务有经纪人负责处理,比如和粉丝见面,和媒体公司洽谈,这里明星的朋友是经纪人,而和粉丝和公司是陌生人,所以适合使用迪米特法则。本例子可知迪米特法则主要是为了降低明星与粉丝和公司之间的耦合度。
6.里氏替换原则
任何基类可以出现的地方,子类一定可以出现。子类可以扩展父类的功能,但不能改变父类原有的能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
下面正方形不是长方形的例子:
// 长方形 @Data @AllArgsConstructor @NoArgsConstructor public class Rectangle { private Double length; private Double width; } // 正方形类(错误继承长方形) public class Square extends Rectangle { @Override public void setLength(Double length) { super.setLength(length); super.setWidth(length); } public void setWidth(Double width) { super.setLength(width); super.setWidth(width); } } // 测试类 public class Test { public static void main(String[] args){ // 创建长方形对象 Rectangle rectangle = new Rectangle(15.0, 10.0); // 扩宽 resize(rectangle); // 15.0 // 16.0 print(rectangle); System.out.println("===================="); Square square = new Square(); square.setLength(10.0); // 扩宽 resize(square); // 死循环 直到oom print(square); } // 扩宽修正方法 public static void resize(Rectangle rectangle) { while (rectangle.getWidth() <= rectangle.getLength()) { rectangle.setWidth(rectangle.getWidth() + 1); } } // 打印长宽 public static void print(Rectangle rectangle) { System.out.println(rectangle.getLength()); System.out.println(rectangle.getWidth()); } }
可以从运行上面的代码看到,当我们调用resize()方法时候,普通长方形可以正常运行;但是当调用resize()方法的对象是正方形时,会死循环,这是因为正方形的长宽相等,永远都不会满足扩宽的条件。所以可以得出结论:在resize()方法中,长方形的参数是不能被正方形的参数所替代的,如果进行了替换就得不到预期的效果,所以Square和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立。修改后的代码如下:
// 四边形接口,长方形和正方形同属于四边形 public interface Quadrilateral { Double getLength(); // 获取长 Double getWidth(); // 获取宽 } // 长方形 @Data @AllArgsConstructor @NoArgsConstructor public class Rectangle implements Quadrilateral { private Double length; private Double width; } @Data public class Square implements Quadrilateral { private Double side; @Override public Double getLength() { return side; } @Override public Double getWidth() { return side; } } // 测试类 public class Test { public static void main(String[] args){ // 创建长方形对象 Rectangle rectangle = new Rectangle(15.0, 10.0); // 扩宽 resize(rectangle); // 15.0 // 16.0 print(rectangle); } // 扩宽修正方法 public static void resize(Rectangle rectangle) { while (rectangle.getWidth() <= rectangle.getLength()) { rectangle.setWidth(rectangle.getWidth() + 1); } } // 打印长宽 public static void print(Rectangle rectangle) { System.out.println(rectangle.getLength()); System.out.println(rectangle.getWidth()); } }
这样Square类的对象就不能使用resize()方法。
7.合成复用原则
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也有以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展和维护。
- 它限制了复用的灵活性。从父类继承而来的实现时静态的,在编译时已经定义,所以运行时不可能发生变化。
采用组合或聚合复用,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
- 它维持了类的封装性。因为成员对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 对象间的耦合度低。可以在类的成员位置声明抽象。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的对象。
下面是汽车种类的例子: