咱们首先了解一个单词
solid

这个单词和咱们今天要讲述的七大原则有什么关系?

咱们首先要了解七大原则都有什么?

  1. 单一职责原则 (Single Responsibility Principle)
  2. 开放-关闭原则 (Open-Closed Principle)
  3. 里氏替换原则 (Liskov Substitution Principle)
  4. 接口隔离原则 (Interface Segregation Principle)
  5. 依赖倒转原则 (Dependence Inversion Principle)
  6. 迪米特法则(Law Of Demeter)
  7. 组合/聚合复用原则 (Composite/Aggregate Reuse Principle)

咱们来进行详细的讲解。

1、单一职责原则
定义:
单一职责原则(Single Responsibility Principle,SRP)又称单一功能原则,由罗伯特·C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。
优点:

单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。
1、降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
2、提高类的可读性。复杂性降低,自然其可读性会提高。
3、提高系统的可维护性。可读性提高,那自然更容易维护了。
4、变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

类层面:
错误示范:

public class SingleResponsibility1 {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.run("飞机");
vehicle.run("轮船");
}
}

class Vehicle {
public void run(String vehicle) {
System.out.println(vehicle + " 在公路运行...");
}
}
汽车 在公路运行...
飞机 在公路运行...
轮船 在公路运行...

正确示范:

public class SingleResponsibility2 {
public static void main(String[] args) {
RoadVehicle roadVehicle = new RoadVehicle();
roadVehicle.run("汽车");

AirVehicle airVehicle = new AirVehicle();
airVehicle.run("飞机");

WaterVehicle waterVehicle = new WaterVehicle();
waterVehicle.run("轮船");
}
}

class RoadVehicle {
public void run(String vehicle) {
System.out.println(vehicle + " 在公路运行...");
}
}

class AirVehicle {
public void run(String vehicle) {
System.out.println(vehicle + " 在天空运行...");
}
}

class WaterVehicle {
public void run(String vehicle) {
System.out.println(vehicle + " 在水上运行...");
}
}
汽车 在公路运行...
飞机 在天空运行...
轮船 在水上运行...

接口层面:

我们假设一个场景, 大家一起做家务, 张三扫地, 李四买菜. 李四买完菜回来还得做饭. 这个逻辑怎么实现呢?

错误示范:

/**
* 做家务
*/
public interface HouseWork {
// 扫地
void sweepFloor();

// 购物
void shopping();
}

public class Zhangsan implements HouseWork{
@Override
public void sweepFloor() {
// 扫地
}

@Override
public void shopping() {

}
}

public class Lisi implements HouseWork{
@Override
public void sweepFloor() {

}

@Override
public void shopping() {
// 购物
}
}

首先定义了一个做家务的接口, 定义两个方法扫地和买菜. 张三扫地, 就实现扫地接口. 李四买菜, 就实现买菜接口. 然后李四买完菜回来还要做饭, 于是就要在接口类中增加一个方法cooking. 张三和李四都重写这个方法, 但只有李四有具体实现.
这样设计本身就是不合理的. 首先: 张三只扫地, 但是他需要重写买菜方法, 李四不需要扫地, 但是李四也要重写扫地方法. 第二: 这也不符合开闭原则. 增加一种类型做饭, 要修改3个类. 这样当逻辑很复杂的时候, 很容易引起意外错误.
上面这种设计不符合单一职责原则, 修改一个地方, 影响了其他不需要修改的地方.

正确示范:

/**
* 做家务
*/
public interface Hoursework {
}

public interface Shopping extends Hoursework{
// 购物
void shopping();
}

public interface SweepFloor extends Hoursework{
// 扫地
void sweepFlooring();
}

public class Zhangsan implements SweepFloor{

@Override
public void sweepFlooring() {
// 张三扫地
}
}

public class Lisi implements Shopping{
@Override
public void shopping() {
// 李四购物
}
}

上面做家务不是定义成一个接口, 而是将扫地和做家务分开了. 张三扫地, 那么张三就实现扫地的接口. 李四购物, 李四就实现购物的接口. 后面李四要增加一个功能做饭. 那么就新增一个做饭接口, 这次只需要李四实现做饭接口就可以了.

public interface Cooking extends Hoursework{ 
void cooking();
}

public class Lisi implements Shopping, Cooking{
@Override
public void shopping() {
// 李四购物
}

@Override
public void cooking() {
// 李四做饭
}
}

如上, 我们看到张三没有实现多余的接口, 李四也没有. 而且当新增功能的时候, 只影响了李四, 并没有影响张三. 这就是符合单一职责原则. 一个类只做一件事. 并且他的修改不会带来其他的变化.

2、开放-关闭原则 (Open-Closed Principle)

定义
开放封闭原则(OCP,Open Closed Principle)是所有面向对象原则的核心。软件设计本身所追求的目标就是封装变化、降低耦合,而开放封闭原则正是对这一目标的最直接体现。其他的设计原则,很多时候是为实现这一目标服务的.

具体描述

● 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
● 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对已有代码进行任何修改

为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键。

示例

错误实例:

public class OpenClosed1 {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());//+新增绘制三角形
}
}
//这是一个用于绘图的类
class GraphicEditor {
//接收 Shape 对象,然后根据 type 来绘制不同的图形
public void drawShape(Shape s) {
if (s.m_type == 1)
drawRectangle(s);
else if (s.m_type == 2)
drawCircle(s);
else if (s.m_type == 3)//+新增绘制三角形
drawTriangle(s);
}

//绘制矩形
public void drawRectangle(Shape r) {
System.out.println("绘制矩形");
}

//绘制圆形
public void drawCircle(Shape r) {
System.out.println("绘制圆形");
}

//+新增绘制三角形
public void drawTriangle(Shape r) {
System.out.println("绘制三角形");
}
}

class Shape {
int m_type;
}

//以前就写好的
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}

//以前就写好的
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}

//+新增绘制三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
}
绘制矩形
绘制圆形
绘制三角形

正确示范

public class OpenClosed2 {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());//+新增绘制三角形
}
}

//这是一个用于绘图的类
class GraphicEditor {
//接收 Shape 对象,调用 draw 方法
public void drawShape(Shape s) {
s.draw();
}
}

abstract class Shape {
int m_type;

public abstract void draw();
}

//以前就写好的
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}

@Override
public void draw() {
System.out.println("绘制矩形");
}
}

//以前就写好的
class Circle extends Shape {
Circle() {
super.m_type = 2;
}

@Override
public void draw() {
System.out.println("绘制圆形");
}
}

//+新增绘制三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}

@Override
public void draw() {
System.out.println("绘制三角形");
}
}
绘制矩形
绘制圆形
绘制三角形

3、里氏替换原则

定义
子类可以扩展父类的功能,但不能改变原有父类的功能。

详细描述

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

根据上述理解,对里氏替换原则的定义可以总结如下:
● 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
● 子类中可以增加自己特有的方法
● 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
● 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等

子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法

枪支抽象类:
public abstract class AbstractGun {
public abstract void shoot();
}
手枪实现类:
public class HandGun extends AbstractGun {
public void shoot() {
System.out.println("手机射击");
}
}
public class Rifle extends AbstractGun {
public void shoot() {
System.out.println("步枪射击");
}
}
士兵实现类:
public class Soldier {
private AbstractGun gun;
public void setGun(AbstractGun gun) {
this.gun = gun;
}
public void killEnemy() {
System.out.println("士兵杀敌人");
gun.shoot();
}
}

场景类:

public class Client {
public static void main(String[] args) {
Soldier sanMao = new Soldier();
sanMao.setGun(new Rifle());
sanMao.killEnemy();
}
}

注意
在类中调用其他类时务必要使用父类或者接口(例如Solider类的setGun(AbstractGun gun)方法),否则说明类的设计已经违背了LSP原则。
现在有个玩具枪该怎么定义?直接继承AbstractGun类吗?如下:

public class ToyGun extends AbstractGun {
@Override
public void shoot() {
//玩具枪不能像真枪杀敌,不实现
}
}

场景类:
public class Client {
public static void main(String[] args) {
Soldier sanMao = new Soldier();
sanMao.setGun(new ToyGun());
sanMao.killEnemy();
}
}

在这种情况下,士兵拿着玩具枪杀敌,发现业务调用类已经出现了问题,正常的业务逻辑运行结果是不正确的。(因为玩具枪并不能杀敌)ToyGun应该脱离继承,建立一个独立的类,可以与AbstractGun建立关联委托关系。类图如下:

注意
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中发生重写或者重载,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。

2、子类中可以增加自己特有的方法
说下面两层含义之前先要明白 重载 重写(覆盖) 的区别:
重写(覆盖)的规则:
1、重写方法的参数列表必须完全与被重写的方法的相同,否则不能称其为重写而是重载.
2、重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protected>default>private)。
3、重写的方法的返回值必须和被重写的方法的返回一致;
4、重写的方法所抛出的异常必须和被重写方法的所抛出的异常一致,或者是其子类;
5、被重写的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行重写。
6、静态方法不能被重写为非静态的方法(会编译出错)。
重载的规则:
1、在使用重载时只能通过相同的方法名、不同的参数形式实现。不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样);
2、不能通过访问权限、返回类型、抛出的异常进行重载;
3、方法的异常类型和数目不会对重载造成影响;
3、当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松

public class Father {
public Collection doSomething(HashMap map){
System.out.println("父类被执行了");
return map.values();
}
}


public class Son extends Father{
public Collection doSomething(Map map){
System.out.println("子类被执行了");
return map.values();
}
}


public class Client{
public static void main(String[] args) {
invoker();
}

public static void invoker(){
Son son = new Son();//子类对象
HashMap map=new HashMap<>();
son.doSomething(map);
}
}

运行是”父类被执行了”,这是正确的,父类方法的参数是HashMap类型,而子类的方法参数是Map类型,子类的参数类型范围比父类大,那么子类的方法永远也不会执行。
如果我们反过来让父类的参数类型范围大于子类,并在调用时用子类去调用,我们会发现打印时的结果是”子类被执行了”,这就违反了里氏替换原则,在开发中很容易引起业务逻辑的混乱,所以类的方法重载父类的方法时,方法的前置条件(形参)要比父类方法的输入参数更宽松(相同也可以)。

4、当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等

父类的一个方法的返回值是一个类型T,子类的相同方法(重载或者重写)的返回值为S,那么里氏替换原则就要求S必须小于等于T。

4、接口隔离
原则介绍
客户端不应该依赖它不需要的接口, 即一个类对另一个类的依赖应该建立在最小的接口上。

示范

//A、B总的接口
interface InterfaceAll {
void operation1();

void operation2();

void operation3();
}

//实现类A只用InterfaceAll中的operation1、operation2方法,所以实现两个方法
class A implements InterfaceAll {
@Override
public void operation1() {
System.out.println("A 实现了 operation1...");
}

@Override
public void operation2() {
System.out.println("A 实现了 operation2...");
}

@Override
public void operation3() {
//A用不到,但是还需要空实现
}
}
//实现类B只用InterfaceAll中的operation1、operation3方法,所以实现两个方法
class B implements InterfaceAll {
@Override
public void operation1() {
System.out.println("B 实现了 operation1...");
}
@Override
public void operation2() {
//B用不到,但是还需要空实现
}

@Override
public void operation3() {
System.out.println("B 实现了 operation3...");
}
}

正确示范

//接口Interface1
interface Interface1 {
void operation1();
}

//接口Interface2
interface Interface2 {
void operation2();
}

//接口Interface3
interface Interface3 {
void operation3();
}

//实现类A用Interface1中的operation1和Interface2中的operation2
class A implements Interface1, Interface2 {
@Override
public void operation1() {
System.out.println("A 实现了 operation1...");
}

@Override
public void operation2() {
System.out.println("A 实现了 operation2...");
}
}

//实现类B用Interface1中的operation1和Interface3中的operation3
class B implements Interface1, Interface3 {
@Override
public void operation1() {
System.out.println("B 实现了 operation1...");
}

@Override
public void operation3() {
System.out.println("B 实现了 operation3...");
}
}

5、依赖倒置原则

定义

依赖倒置原则(dependence inversion principle): 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

规则

  1. 每个类尽量提供接口或抽象类,或者两者都具备。
  2. 变量的声明类型尽量是接口或者是抽象类。
  3. 任何类都不应该从具体类派生。
  4. 使用继承时尽量遵循里氏替换原则。

示例

错误示例:

class Customer {
public void shopping(ShaoguanShop shop) {
//购物
System.out.println(shop.sell());
}
}

class Customer {
public void shopping(WuyuanShop shop) {
//购物
System.out.println(shop.sell());
}
}

添加功能的时候没有办法复用代码,顾客每更换一家商店,都要修改一次代码,这明显违背了开闭原则。存在以上缺点的原因是:顾客类设计时同具体的商店类绑定了,这违背了依赖倒置原则。解决方法是:定义“婺源网店”和“韶关网店”的共同接口 Shop,顾客类面向该接口编程,其代码修改如下:

class Customer {
public void shopping(Shop shop) {
//购物
System.out.println(shop.sell());
}
}



package principle;

public class DIPtest {
public static void main(String[] args) {
Customer wang = new Customer();
System.out.println("顾客购买以下商品:");
wang.shopping(new ShaoguanShop());
wang.shopping(new WuyuanShop());
}
}

//商店
interface Shop {
public String sell(); //卖
}

//韶关网店
class ShaoguanShop implements Shop {
public String sell() {
return "韶关土特产:香菇、木耳……";
}
}

//婺源网店
class WuyuanShop implements Shop {
public String sell() {
return "婺源土特产:绿茶、酒糟鱼……";
}
}

//顾客
class Customer {
public void shopping(Shop shop) {
//购物
System.out.println(shop.sell());
}
}

6、合成复用原则

定义

合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

继承关系实现:

组合关系实现

7、迪米特法则

定义
迪米特法则(Law of Demeter,LOD)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话 。

实现法则

  1. 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
  2. 在类的结构设计上,尽量降低类成员的访问权限。
  3. 在类的设计上,优先考虑将一个类设置成不变类。
  4. 在对其他类的引用上,将引用其他对象的次数降到最低。
  5. 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
  6. 谨慎使用序列化(Serializable)功能。

示例

package principle;

public class LoDtest {
public static void main(String[] args) {
Agent agent = new Agent();
agent.setStar(new Star("林心如"));
agent.setFans(new Fans("粉丝我"));
agent.setCompany(new Company("中国传媒有限公司"));
agent.meeting();
agent.business();
}
}

//经纪人
class Agent {
private Star myStar;
private Fans myFans;
private Company myCompany;

public void setStar(Star myStar) {
this.myStar = myStar;
}

public void setFans(Fans myFans) {
this.myFans = myFans;
}

public void setCompany(Company myCompany) {
this.myCompany = myCompany;
}

public void meeting() {
System.out.println(myFans.getName() + "与明星" + myStar.getName() + "见面了。");
}

public void business() {
System.out.println(myCompany.getName() + "与明星" + myStar.getName() + "洽淡业务。");
}
}

//明星
class Star {
private String name;

Star(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

//粉丝
class Fans {
private String name;

Fans(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

//媒体公司
class Company {
private String name;

Company(String name) {
this.name = name;
}

public String getName() {
return name;
}
}