1. 单一原则(SRP)
一个类只负责一个功能领域的相应职责,将一个类拆分成多个小类,每个小类都只关注一个职责,提高代码的复用性和可维护性
2. 开闭原则(OCP)
对扩展开放,对修改关闭:一个软件实体应以对扩展开放的方式设计,以支持系统的 扩展而不是重构,这个原则应用在模块设计、接口设计、抽象类设计、类的继承等方面。
3. 里氏替换原则(LSP)
所有引用基类对象的地方都能够透明地使用其子类对象。既子类可以扩展父类的方法,但不能改变父类原有的方法特性,保证父类与子类之间的基本锲约关系不受破坏,用这个原则定义出的子类,才能保证在集成场景下对系统的正常运行不产生任何影响。
里氏替换原则内在含义:
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法【核心概念】
- 子类中可以增加自己特有的方法。
- 前置条件放大,子类在重载父类的已实现方法时,方法的前置条件(形参)范围应该比父类更加宽松.
- 后置条件缩小,子类在实现父类的抽象方法时,方法的后置条件(返回值)范围应该比父类更加严格或相同。
如下图,父类parent的function方法形参是collection,son子类的继承parent类,并且重载了父类的function方法,但是son子类function的形参范围比父类的要小,因为List继承Collection接口。
当我们执行下面的测试用例时,代码及输出结果如下:
但是当我们用son子类使用同一对象调用同一方法却产生了不同的结果。此时子类便有可能不能完全透明的替换掉父类,即子类替换掉父类后可能会影响到原程序的运行状况。
4. 依赖倒置原则(DIP)
高层模块不应该依赖底层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象,即依赖于抽象,而不依赖于具体实现。抽象对代码来说即接口或者抽象类,细节对代码来说即实现类
对比代码如下:
工作人员接收微信老板发来的加班消息
方案一:
/**
* 定义工作人员Worker.java
*/
public class Worker {
public void getMessage(WeChat weChat){
weChat.sendMessage();
}
}
/**
* 定义微信消息类WeChat.java
*/
public class WeChat {
public void sendMessage() {
System.out.println("微信上,老板让你加班了");
}
}
/**
* 方案一测试代码,违反依赖倒置原则
*/
@Test
public void test03(){
new Worker().getMessage(new WeChat());
}
方案一中,违反了依赖倒置原则,如果功能需求扩展,比如说需要扩展一种飞书发送消息,我们需要新增一个飞书类,并实现发送消息的功能,工作人员的类也需要修改接收消息的方法,客户端也需要进行相应的修改,改动大,风险大
方案二:
/**
* 定义消息接口IMessage.java
*/
public interface IMessage {
void sendMessage();
}
/**
* 定义微信消息类WeChatNew.java
*/
public class WeChatNew implements IMessage{
@Override
public void sendMessage() {
System.out.println("微信上,老板让你加班了");
}
}
/**
* 定义飞书类
*/
public class Feishu implements IMessage{
@Override
public void sendMessage() {
System.out.println("飞书上,老板让你加班了");
}
}
/**
* 定义工作人员Worker.java
*/
public class WorkerNew {
public void getMessage(IMessage iMessage){
iMessage.sendMessage();
}
}
@Test
public void test04(){
new WorkerNew().getMessage(new WeChatNew());
new WorkerNew().getMessage(new Feishu());
}
方案二遵守了依赖倒置原则,同样的需求扩展,抽象一个公共的消息接口,所有的微信,飞书等发送消息的类只要实现该接口,重写发送消息的方法,工作人员的接收消息方法以消息接口为入参,客户端只需要传入相应的消息实体类,扩展方便,耦合低。
5. 接口隔离原则(ISP)
不应该强迫一个类实现它不需要的接口,或者使用一个类需要大量的调用其他接口的方法。将接口进行拆分,提供多个功能小而专一的接口。
结合案例:
假设有这样一个案例场景,现在有一个接口Repair,给定他有三个能力,可以修汽车,修火车,修飞机, 两个实现类张师傅,李师傅分别具有这些能力,有一个4S店类,假设是需要张师傅来修汽车和修飞机,而另一个4s店类需要李师傅来修汽车和修火车
方案一:
/**
* 定义维修接口
*/
public interface Repair {
/**
* 维修汽车
*/
void car();
/**
* 维修火车
*/
void train();
/**
* 维修飞机
*/
void air();
}
/**
* 维修王师傅
*/
public class RepairLi implements Repair{
@Override
public void car() {
System.out.println("李师傅修汽车");
}
@Override
public void train() {
System.out.println("李师傅修火车");
}
@Override
public void air() {
System.out.println("李师傅修飞机");
}
}
/**
* 维修王师傅
*/
public class RepairWang implements Repair{
@Override
public void car() {
System.out.println("王师傅修汽车");
}
@Override
public void train() {
System.out.println("王师傅修火车");
}
@Override
public void air() {
System.out.println("王师傅修飞机");
}
}
/**
* 4s店1
*/
public class Shop1 {
/**
* 修汽车
* @param repair
*/
public void repairCar(Repair repair){
repair.car();;
}
/**
* 修火车
* @param repair
*/
public void repairTrain(Repair repair){
repair.train();
}
}
/**
* 4s店2
*/
public class Shop2 {
/**
* 修汽车
* @param repair
*/
public void repairCar(Repair repair){
repair.car();;
}
/**
* 修飞机
* @param repair
*/
public void repairAir(Repair repair){
repair.air();
}
}
方案一中违反了接口隔离原则,因为李师傅和王师傅都实现了维修接口的三个方法:修汽车,修火车,修飞机。但需求中并不需要这么多,只需要李师傅修汽车和火车,王师傅修汽车和飞机,依赖关系不是建立在最小接口上。
方案二:
/**
* 修飞机
*/
public interface RepairAir {
void repairAir();
}
/**
* 修汽车
*/
public interface RepairCar {
void repairCar();
}
/**
* 修火车
*/
public interface RepairTrain {
void repairTrain();
}
/**
* 维修李师傅
*/
public class RepairLiNew implements RepairCar,RepairTrain{
@Override
public void repairCar() {
System.out.println("李师傅修汽车");
}
@Override
public void repairTrain() {
System.out.println("李师傅修火车");
}
}
/**
* 维修王师傅
*/
public class RepairWangNew implements RepairCar,RepairAir {
@Override
public void repairAir() {
System.out.println("王师傅修飞机");
}
@Override
public void repairCar() {
System.out.println("王师傅修汽车");
}
}
/**
* 4s店1修汽车和火车
*/
public class Shop1New {
public void car(RepairCar repairCar){
repairCar.repairCar();
}
public void Train(RepairTrain repairTrain){
repairTrain.repairTrain();
}
}
/**
* 4s店2修汽车和飞机
*/
public class Shop2New {
public void car(RepairCar repairCar){
repairCar.repairCar();
}
public void air(RepairAir repairAir){
repairAir.repairAir();
}
}
方案二遵守了接口隔离原则,对李师傅和王师傅类都进行了接口隔离,实现了各自的两个方法,避免了耦合
6. 迪米特法则(LOD)
一个对象应该对其他对象保持最少的了解,即一个类应该对自己需要耦合或调用的类知道得最少。通俗的讲,就是一个类不应该知道太对其他类得细节,只需要了解自己的职责即可。
迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
举个案例:
有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。
/**
* 总公司员工
*/
public class Employee {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
/**
* 分公司员工
*/
public class SubEmployee {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
public class SubCompanyManager {
public List<SubEmployee> getAllEmployee() {
List<SubEmployee> list = new ArrayList<SubEmployee>();
for(int i=0;i<100;i++){
SubEmployee subEmployee = new SubEmployee();
subEmployee.setId("分公司"+i);
list.add(subEmployee);
}
return list;
}
}
public class CompanyManager {
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for(int i=0;i<100;i++){
Employee employee = new Employee();
employee.setId("总公司"+i);
list.add(employee);
}
return list;
}
public void printAllEmployee(SubCompanyManager subCompanyManager){
List<SubEmployee> allSubEmployee = subCompanyManager.getAllEmployee();
for(SubEmployee subEmployee: allSubEmployee){
System.out.println(subEmployee.getId());
}
List<Employee> allEmployee = this.getAllEmployee();
for(Employee employee: allEmployee){
System.out.println(employee.getId());
}
}
}
方案一中,主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下:
public class SubCompanyManagerNew {
public List<SubEmployee> getAllEmployee() {
List<SubEmployee> list = new ArrayList<SubEmployee>();
for(int i=0;i<100;i++){
SubEmployee subEmployee = new SubEmployee();
subEmployee.setId("分公司"+i);
list.add(subEmployee);
}
return list;
}
public void printAllEmployee(){
List<SubEmployee> allSubEmployee = this.getAllEmployee();
for(SubEmployee subEmployee: allSubEmployee){
System.out.println(subEmployee.getId());
}
}
}
public class CompanyManager {
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for(int i=0;i<100;i++){
Employee employee = new Employee();
employee.setId("总公司"+i);
list.add(employee);
}
return list;
}
public void printAllEmployee(SubCompanyManagerNew subCompanyManagerNew){
subCompanyManagerNew.printAllEmployee();
List<Employee> allEmployee = this.getAllEmployee();
for(Employee employee: allEmployee){
System.out.println(employee.getId());
}
}
}
@Test
public void test08(){
CompanyManager companyManager = new CompanyManager();
companyManager.printAllEmployee(new SubCompanyManagerNew());
}
修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。
迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
7. 组合/聚合复用原则(CARP)
要尽量使用组合/聚合。而不是使用继承来达到复用得目的。这样可以在运行时动态的改变对象的行为,并且改变的行为不会影响到其他拥有相同基类的对象。