开闭原则
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级
想要达到这样的效果,我们需要使用接口和抽象类。
示例:计算面积
假设有一个图形类(Shape)和两个具体的图形子类:矩形(Rectangle)和圆形(Circle)。现在,我们要为这些图形添加一个计算面积的功能,并且希望在未来能够扩展更多的图形
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| abstract class Shape { public abstract double calculateArea(); }
class Rectangle extends Shape { private double width; private double height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override public double calculateArea() { return width * height; } }
class Circle extends Shape { private double radius;
public Circle(double radius) { this.radius = radius; }
@Override public double calculateArea() { return Math.PI * radius * radius; } }
class AreaCalculator { public double calculateTotalArea(Shape[] shapes) { double totalArea = 0; for (Shape shape : shapes) { totalArea += shape.calculateArea(); } return totalArea; } }
public class OCPDemo { public static void main(String[] args) { Shape[] shapes = {new Rectangle(4, 5), new Circle(3)}; AreaCalculator calculator = new AreaCalculator(); double totalArea = calculator.calculateTotalArea(shapes); System.out.println("Total area: " + totalArea); } }
|
里氏代换原则
任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
里氏代换原则可以扩展代码的功能而不破坏原有结构,同时提高类型检查的准确性
依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
示例:电脑组装
现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。
电脑(Computer):
1 2 3 4 5 6 7 8 9 10 11
| @Data public class Computer {
private HardDisk hardDisk; private Cpu cpu; private Memory memory;
public void run() { System.out.println("计算机工作"); } }
|
硬盘接口(HardDisk):
1 2 3 4 5 6
| public interface HardDisk {
void save(String data);
String get(); }
|
希捷硬盘类(XiJieHardDisk):
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class XiJieHardDisk implements HardDisk {
@Override public void save(String data) { System.out.println("使用希捷硬盘存储数据" + data); }
@Override public String get() { System.out.println("使用希捷希捷硬盘取数据"); return "数据"; } }
|
西数硬盘类(XiShuHardDisk):
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class XiShuHardDisk implements HardDisk {
@Override public void save(String data) { System.out.println("使用西数硬盘存储数据" + data); }
@Override public String get() { System.out.println("使用西数希捷硬盘取数据"); return "数据"; } }
|
CPU、内存各自的接口和实现类同理
测试类(TestComputer):
测试类用来组装电脑。
1 2 3 4 5 6 7 8 9 10
| public class TestComputer { public static void main(String[] args) { Computer computer = new Computer(); computer.setHardDisk(new XiJieHardDisk()); computer.setCpu(new IntelCpu()); computer.setMemory(new KingstonMemory());
computer.run(); } }
|
接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
示例:安全门
我们需要创建一个安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。
现在如果我们还需要再创建一个只具有防盗、防水功能,那创建起来就相当方便了
防盗接口(AntiTheft):
1 2 3
| public interface AntiTheft { void antiTheft(); }
|
防火接口(Fireproof):
1 2 3
| public interface Fireproof { void fireproof(); }
|
防水接口(Waterproof):
1 2 3
| public interface Waterproof { void waterproof(); }
|
SafetyDoor(类):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class SafetyDoor implements AntiTheft,Fireproof,Waterproof { public void antiTheft() { System.out.println("防盗"); }
public void fireproof() { System.out.println("防火"); }
public void waterproof() { System.out.println("防水"); } }
|
LiteSafetyDoor(类):
1 2 3 4 5 6 7 8 9
| public class LiteSafetyDoor implements AntiTheft,Fireproof { public void antiTheft() { System.out.println("防盗"); }
public void fireproof() { System.out.println("防火"); } }
|
迪米特法则
迪米特法则又叫最少知识原则。
只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
示例:明星与经纪人
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
明星类(Star)
1 2 3 4 5
| @Data @RequiredArgsConstructor public class Star { private final String name; }
|
粉丝类(Fans)
1 2 3 4 5
| @Data @RequiredArgsConstructor public class Fans { private final String name; }
|
媒体公司类(Company)
1 2 3 4 5
| @Data @RequiredArgsConstructor public class Company { private final String name; }
|
经纪人类(Agent)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data public class Agent { private Star star; private Fans fans; private Company company;
public void meeting() { System.out.println(fans.getName() + "与明星" + star.getName() + "见面了。"); }
public void business() { System.out.println(company.getName() + "与明星" + star.getName() + "洽谈业务。"); } }
|
合成复用原则
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 对象间的耦合度低。可以在类的成员位置声明抽象。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
示例:汽车颜色和动力源
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。
当使用继承复用时,我们可能会先让电车和油车继承抽象父类Car,然后电车油车各自再细分子类红电车、蓝电车、黑油车……继承复用会产生很多子类
但如果我们将继承复用改为聚合复用就会简单很多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import lombok.Getter; import lombok.Setter;
interface PowerSource { }
interface Color { }
@Getter @Setter class Car { private PowerSource powerSource; private Color color;
public Car(PowerSource powerSource, Color color) { this.powerSource = powerSource; this.color = color; } }
public class Main { public static void main(String[] args) { PowerSource gasolinePower = new GasolinePower(); Color whiteColor = new WhiteColor();
Car car1 = new Car(gasolinePower, whiteColor);
PowerSource electricPower = new ElectricPower(); Color redColor = new RedColor();
Car car2 = new Car(electricPower, redColor);
} }
|