最详细的设计模式的七大原则讲解

精心整理了设计模式的七大原则,包括代码解释方便理解,但是难免不了存在纰漏,感谢大家的指正与理解!觉的写的不错的小伙伴儿,一键三连支持一下,后期会有持续更新!!抱拳了罒ω罒

设计模式的七大原则

面向对象的设计模式有七大基本原则:

  • 开闭原则(Open Closed Principle,OCP)
  • 单一职责原则(Single Responsibility Principle, SRP)
  • 里氏代换原则(Liskov Substitution Principle,LSP)
  • 依赖倒转原则(Dependency Inversion Principle,DIP)
  • 接口隔离原则(Interface Segregation Principle,ISP)
  • 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
  • 最少知识原则(Least Knowledge Principle,LKP)或者迪米特法则(Law of Demeter,LOD)
设计模式原则名称简单定义
开闭原则对扩展开放,对修改关闭
单一职责原则一个类只负责一个功能领域中的相应职责
里氏代换原则所有引用基类的地方必须能透明地使用其子类的对象
依赖倒转原则依赖于抽象,不能依赖于具体实现
接口隔离原则类之间的依赖关系应该建立在最小的接口上
合成/聚合复用原则尽量使用合成/聚合,而不是通过继承达到复用的目的
迪米特法则一个软件实体应当尽可能少的与其他实体发生相互作用

1. 开闭原则

  定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原代码的情况下进行扩展。
  下面通过一个例子遵循开闭原则进行设计,场景是这样:某系统的后台需要监测业务数据展示图表,如柱状图、折线图等,在未来需要增加饼图展示操作。在开始设计的时候,代码可能是这样的:

public class BarChart {
	public void draw(){
		System.out.println("Draw bar chart...");
	}
}

public class LineChart {
	public void draw(){
		System.out.println("Draw line chart...");
	}
}

public class App {
	public void drawChart(String type){
		if (type.equalsIgnoreCase("line")){
			new LineChart().draw();
		}else if (type.equalsIgnoreCase("bar")){
			new BarChart().draw();
		}
	}
}

  这样做在初期是能满足业务需要的,开发效率也十分高,但是当后面需要新增一个饼状图的时候,既要添加一个饼状图的类,原来的客户端App类的drawChart方法也要新增一个if分支,这样做就是修改了原有客户端类库的方法,是十分不合理的。基于此,需要引入一个抽象Chart类AbstractChart,App类在画图的时候总是把相关的操作委托到具体的AbstractChart的派生类实例,这样的话App类的代码就不用修改:

public abstract class AbstractChart {
	public abstract void draw();
}

public class BarChart extends AbstractChart{
	@Override
	public void draw() {
		System.out.println("Draw bar chart...");
	}
}

public class LineChart extends AbstractChart {
	@Override
	public void draw() {
		System.out.println("Draw line chart...");
	}
}

public class App {
	public void drawChart(AbstractChart chart){
		chart.draw();
	}
}

  如果新加一种图,只需要新增一个AbstractChart的子类即可。客户端类App不需要改变原来的逻辑。修改后的设计符合开闭原则,因为整个系统在扩展时原有的代码没有做任何修改。

2. 单一职责原则

  定义:对类或者模块来说,一个类或者模块应该只负责一项职责。如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。
  下面通过一个很简单的实例说明一下单一职责原则:在一个项目系统代码编写的时候,由于历史原因和人为的不规范,导致项目没有分层,一个Service类的伪代码是这样的:

public class Service {
	public UserDTO findUser(String name){
		String sql = "SELECT * FROM t_user WHERE name = ?";
		Connection connection = getConnection();
		PreparedStatement preparedStatement = 
		connection.prepareStatement(sql);
		preparedStatement.setObject(1, name);
        User user = //处理结果
        UserDTO dto = new UserDTO();
        //entity值拷贝到dto
        return dto;
	}
}

  这里出现一个问题,Service做了太多东西,包括数据库连接的管理,Sql的执行等,这些业务层不应该接触到的逻辑,更可怕的是,例如到时候如果数据库换成了Oracle,这个方法将会大改。因此,拆分出新的DataBaseUtils类用于专门管理数据库资源,Dao类用于专门执行查询和查询结果封装,改造后Service类的伪代码如下:

public class Service {
    private Dao dao;
	public UserDTO findUser(String name){
       User user =  dao.findUserByName(name);
       UserDTO dto = new UserDTO();
        //entity值拷贝到dto
       return dto;
    }
}

public class Dao{
    public User findUserByName(String name){
       Connection connection = DataBaseUtils.getConnnection();
       String sql = "SELECT * FROM t_user WHERE name = ?";
       PreparedStatement preparedStatement = 
       connection.prepareStatement(sql);
	   preparedStatement.setObject(1, name);
       User user = //处理结果
       return user;
    }
}

  如果有查询封装的变动只需要修改Dao类,数据库相关变动只需要修改DataBaseUtils类,每个类的职责分明。

3. 里式替换原则

  定义:所有引用基类的地方必须能透明地使用其子类的对象,通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能,即尽量不要重写父类的方法。
  举个简单的例子,基类A是计算两个数相减,类B完成两个数相加,然后和9求和

public class LisKov {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("11 - 3 = " + a.func1(11,3));
        B b = new B();
        //由于B重写的func1方法,所以计算11和3的差会变成求和,出现计算错误
        System.out.println("11 - 3 = " + b.func1(11,3));
    }
}

class A{
    public int func1(int num1,int num2){
        return num1 - num2;
    }
}

class B extends A{
    @Override
    public int func1(int a, int b){
        return a + b;
    }
}

  我们发现原来运行正常的相减功能发生了错误。原因就是类 B 无意中重写了父类的func1方法,造成原有功能出现错误。通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替。

public class LisKov {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("11 - 3 = " + a.func1(11,3));
        B b = new B();
        // 使用组合调用A的func1的方法
        System.out.println("11 -3 = " + b.func3(11,3));
    }
}

// 创建一个更加基础的基类
class Base{
}

class A extends Base{
    public int func1(int num1,int num2){
        return num1 - num2;
    }
}

// 使用组合关系来代替继承
class B extends Base {
    private A a = new A();
    public int func1(int a, int b){
        return a + b;
    }
    // 我们仍然想使用 A 的方法
    public int func3(int a,int b){
        return this.a.func1(a,b);
    }
}

4. 依赖倒转原则

  定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。从Java角度看待依赖倒转原则的本质就是:面向接口(抽象)编程。
  遵循依赖倒转原则的一个例子,场景是接受邮件:

public class DependencyInversion {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
    }
}
class Email{
    public String getInfo(){
        return "电子邮件信息:Hello,world!";
    }
}

class Person{
    public void receive(Email email){
        System.out.println(email.getInfo());
    }
}

  如果我们获取的对象是微信,短信等,则要新增类,同时Person也要增加相应的接收。因此引入一个抽象的接口 IReceiver,表示接收者,这样Person类与接口发生依赖,因为Email还有微信等都属于接收的范围,它们各自实现 IReceiver 接口就ok,这样我们就符合依赖倒转原则:

public class DependencyInversion {
    public static void main(String[] args) {
        // 客户端无需改变
        Person person = new Person();
        person.receive(new Email());
        person.receive(new WeChat());
    }
}

interface IReceiver{
    String getInfo();
}

class Email implements IReceiver{
    @Override
    public String getInfo(){
        return "电子邮件信息:Hello,world!";
    }
}

class WeChat implements IReceiver{
    @Override
    public String getInfo() {
        return "微信消息:hello,ok!";
    }
}

class Person{
    public void receive(IReceiver receiver){
        // 这里我们是对接口的依赖
        System.out.println(receiver.getInfo());
    }
}

5. 接口隔离原则

  定义:是客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。简单来说就是建立单一的接口,不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少。
  举例来说明接口隔离原则:

interface I {
	public void method1();
	public void method2();
	public void method3();
}

class A{
	public void depend1(I i){
		i.method1();
	}
	public void depend2(I i){
		i.method2();
	}
}
 
class B implements I{
	public void method1() {
		System.out.println("类B实现接口I的方法1");
	}
	public void method2() {
		System.out.println("类B实现接口I的方法2");
	}
	//对于类B来说,method3不是必需的,但是由于接口中有这个方法,
	//所以在实现过程中即使这个方法的方法体为空,也要将这个没有作用的方法进行实现。
	public void method3() {	}

}
public class Client{
	public static void main(String[] args){
		A a = new A();
		a.depend1(new B());
		a.depend2(new B());
	}
}

  可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分:

interface I1 {
	public void method1();
	public void method2();
}
interface I2 {
	public void method3();
}

class A{
	public void depend1(I1 i){
		i.method1();
	}
	public void depend2(I1 i){
		i.method2();
	}
}
 
class B implements I1{
	public void method1() {
		System.out.println("类B实现接口I1的方法1");
	}
	public void method2() {
		System.out.println("类B实现接口I1的方法2");
	}
}
public class Client{
	public static void main(String[] args){
		A a = new A();
		a.depend1(new B());
		a.depend2(new B());
	}
}

6. 合成/聚合复用原则

  定义:尽量使用合成/聚合,而不是通过继承达到复用的目的。
  类A有2个方法,类B刚好需要调用这两个方法,我们第一可能会想到直接继承,这样“多快好省”,但随着业务进展,功能越来越复杂,A类需要增加其他方法,比如Method3 ,与B类毫无关联,将会大大增加耦合性,合用复用原则的核心就是使用关联,我们可以通过依赖、聚合、合成等关联方法,降低耦合,提高可维护性和降低维护成本。

public class A{
    public void Method1(){
         Console.WriteLine("我是方法一");
    }
    public void Method2(){
         Console.WriteLine("我是方法二");
    }
}
//依赖
public class B{
    public void Method1(A a){
         Console.WriteLine("调用A方法");
    }
}
//聚合
public class C{
    private A a;
	public void SetA(A al){
        a = al;
    }
}
//合成
public class D{
    public A a = new A();
}

7. 迪米特法则

  定义:迪米特法则,有时候也叫做最少知识原则,一个软件实体应当尽可能少地与其他实体发生相互作用。

迪米特法则还有个更简单的定义:只与直接的朋友通信
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类不要以局部变量的形式出现在类的内部。

  举个很简单的例子,体育老师要知道班里面女生的人数,他委托体育课代表点清女生的人数:

public class Girl {
}

public class GroupLeader {
	private final List<Girl> girls;
	public GroupLeader(List<Girl> girls) {
		this.girls = girls;
	}
	public void countGirls() {
		System.out.println("The sum of girls is " + girls.size());
	}
}

public class Teacher {
	public void command(GroupLeader leader){
		leader.countGirls();
	}
}

public class App {
	public static void main(String[] args) throws Exception {
		Girl girl1 = new Girl();
		Girl girl2 = new Girl();
		List<Girl> list = new ArrayList<>();
		list.add(girl1);list.add(girl2);
		
		Teacher teacher = new Teacher();
		GroupLeader groupLeader = new GroupLeader(list);
		teacher.command(groupLeader);
	}
}

  这个例子中,体育课代表就是中间类,体育课代表对于体育老师来说就是"直接的朋友",如果去掉体育课代表这个中间类,体育老师必须亲自清点女生的人数(实际上就数人数这个功能,体育老师是不必要获取所有女生的对象列表),这样做会违反迪米特法则。

参考文章:
https://www.cnblogs.com/throwable/p/9315318.html
https://www.cnblogs.com/zhaye/p/11176906.html

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值