设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。1995 年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了 23 种设计模式,从此树立了软件设计模式领域的里程碑,人称「GoF设计模式」。这 23 种设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。虽然我命名为“Java设计模式”,但是设计模式并不是 Java 的专利,它同样适用于 C++、C#、JavaScript 等其它面向对象的编程语言。
编写软件过程中,程序员面临着来自 耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件),具有更好:
设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模 式的基础(即:设计模式为什么这样设计的依据)
设计模式常用的七大原则有:
对类来说的,即一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2。 当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为 A1,A2.
public class singleton1 {
public static void main(String[] args) {
Vehicle vehicle=new Vehicle();
vehicle.run("汽车");
vehicle.run("轮船");
vehicle.run("飞机");
}
}
//非单一模式
class Vehicle{
public void run(String ve){
System.out.println(ve+"在公路运行");
}
}
public class singleton3 {
public static void main(String[] args) {
RoadVehicle roadVehicle=new RoadVehicle();
roadVehicle.run("汽车");
AirVehicle airVehicle=new AirVehicle();
airVehicle.run("飞机");
SeaVehicle seaVehicle=new SeaVehicle();
seaVehicle.run("轮船");
}
}
//类单一原则
class RoadVehicle{
public void run(String ve){
System.out.println(ve+"在陆地跑");
}
}
class AirVehicle{
public void run(String ve){
System.out.println(ve+"在天空飞");
}
}
class SeaVehicle{
public void run(String ve){
System.out.println(ve+"在海航线");
}
}
方法单一职责(只有在方法少可用)
public class singleton2 {
public static void main(String[] args) {
Vehicle2 vehicle2=new Vehicle2();
vehicle2.runRoad("汽车");
vehicle2.runAir("飞机");
vehicle2.runSea("轮船");
}
}
//非标准单例模式,在方法少的情况下,可以在方法中实现单例
class Vehicle2{
public void runRoad(String ve){
System.out.println(ve+"在公路上跑");
}
public void runAir(String ve){
System.out.println(ve+"在天空上飞行");
}
public void runSea(String ve){
System.out.println(ve+"在大海上航行");
}
}
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
类图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IEv04Ut9-1606384654582)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20201125141055412.png)]
代码:
可以看出A通过接口使用B只用了三个方法,但是B却要实现五个方法,这样就是不符合接口的隔离原则。
package com.fys.segregation;
public class SegregationTest {
}
interface interface1{
public void test1();
public void test2();
public void test3();
public void test4();
public void test5();
}
class B implements interface1{
@Override
public void test1() {
System.out.println("B实现了test1");
}
@Override
public void test2() {
System.out.println("B实现了test2");
}
@Override
public void test3() {
System.out.println("B实现了test3");
}
@Override
public void test4() {
System.out.println("B实现了test4");
}
@Override
public void test5() {
System.out.println("B实现了test5");
}
}
class D implements interface1{
@Override
public void test1() {
System.out.println("D实现了test1");
}
@Override
public void test2() {
System.out.println("D实现了test2");
}
@Override
public void test3() {
System.out.println("D实现了test3");
}
@Override
public void test4() {
System.out.println("D实现了test4");
}
@Override
public void test5() {
System.out.println("D实现了test5");
}
}
class A{
public void depends1(interface1 i){
i.test1();
}
public void depends2(interface1 i){
i.test2();
}
public void depends3(interface1 i){
i.test3();
}
}
class C{
public void depends1(interface1 i){
i.test1();
}
public void depends2(interface1 i){
i.test4();
}
public void depends3(interface1 i){
i.test5();
}
}
按隔离原则应当这样处理: 将接口Interface1拆分为独立的几个接口, 类A和类C分别与他们需要的接口建立依赖 关系。也就是采用接口隔离原则
类图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-54NtzJOy-1606384654587)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20201125143024695.png)]
代码:
这样把一个大接口,分到最小接口,就可以满足接口隔离原则
package com.fys.segregation;
public class SegregationTest2{
public static void main(String[] args) {
AA aa=new AA();
aa.depends1(new BB());
aa.depends2(new BB());
aa.depends3(new BB());
CC cc=new CC();
cc.depends1(new DD());
cc.depends2(new DD());
cc.depends3(new DD());
}
}
interface interface2{
public void test1();
}
interface interface3{
public void test2();
public void test3();
}
interface interface4{
public void test4();
public void test5();
}
class BB implements interface2,interface3{
@Override
public void test1() {
System.out.println("B实现了test1");
}
@Override
public void test2() {
System.out.println("B实现了test2");
}
@Override
public void test3() {
System.out.println("B实现了test3");
}
}
class DD implements interface2,interface4{
@Override
public void test1() {
System.out.println("D实现了test1");
}
@Override
public void test4() {
System.out.println("D实现了test4");
}
@Override
public void test5() {
System.out.println("D实现了test5");
}
}
class AA{
public void depends1(interface2 i){
i.test1();
}
public void depends2(interface3 i){
i.test2();
}
public void depends3(interface3 i){
i.test3();
}
}
class CC{
public void depends1(interface2 i){
i.test1();
}
public void depends2(interface4 i){
itest4();
}
public void depends3(interface4 i){
i.test5();
}
}
依赖倒转原则(Dependence Inversion Principle)是指:
高层模块不应该依赖低层模块,二者都应该依赖其抽象
高层就是调用者,底层就是被调用者,例如:
//person就是调用者即高层,email就是被调用者即底层
class Person{
public void sendEmail(Email email){
email.send();
}
}
抽象不应该依赖细节,细节应该依赖抽象
依赖倒转(倒置)的中心思想是面向接口编程
依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象 指的是接口或抽象类,细节就是具体的实现类
使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的 任务交给他们的实现类去完成
依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。
通俗易懂的话就是:
在这种情况下,person和email之间发生了依赖关系,而且person想增加其它发送方式,又要新建其他方法非常不方便。
package com.fys.inversion;
public class DependencyInversion {
public static void main(String[] args) {
Person person=new Person();
person.sendEmail(new Email());
}
}
class Email{
public void send(){
System.out.println("发送邮件");
}
}
class Person{
public void sendEmail(Email email){
email.send();
}
}
让功能类实现接口,person依赖接口,一样也可以完成需求,而且扩展性很高,满足了实现类只依赖接口。
package com.fys.inversion.improve;
public class DependencyInversion {
public static void main(String[] args) {
Person person=new Person();
person.receive(new Email());
person.receive(new Qq());
}
}
interface Send{
public void getInfo();
}
class Email implements Send{
@Override
public void getInfo() {
System.out.println("发送邮件");
}
}
class Qq implements Send{
@Override
public void getInfo() {
System.out.println("发送qq消息");
}
}
class Person{
public void receive(Send send){
send.getInfo();
}
}
package com.fys.inversion.three;
public class DependencyPass {
public static void main(String[] args) {
OpenAndClose openAndClose=new OpenAndClose();
openAndClose.open(new ChangHong());
}
}
//打开与关闭的抽象
interface IOpenAndClose{
public void open(ITV itv);
}
//电视机的抽象
interface ITV{
public void play();
}
//电视机的实现
class ChangHong implements ITV{
@Override
public void play() {
System.out.println("长虹电视打开");
}
}
//打开与关闭的实现
class OpenAndClose implements IOpenAndClose{
@Override
public void open(ITV itv) {
itv.play();
}
}
package com.fys.inversion.three;
public class DependencyPass {
public static void main(String[] args) {
OpenAndClose openAndClose=new OpenAndClose(new ChangHong());
openAndClose.open();
}
}
//打开与关闭的抽象
interface IOpenAndClose{
public void open();
}
//电视机的抽象
interface ITV{
public void play();
}
//电视机的实现
class ChangHong implements ITV{
@Override
public void play() {
System.out.println("长虹电视打开");
}
}
//打开与关闭的实现
class OpenAndClose implements IOpenAndClose{
private ITV itv;
public OpenAndClose(ITV itv){
this.itv=itv;
}
@Override
public void open() {
this.itv.play();
}
}
package com.fys.inversion.three;
public class DependencyPass {
public static void main(String[] args) {
OpenAndClose openAndClose=new OpenAndClose();
openAndClose.setTv(new ChangHong());
openAndClose.open();
}
}
//打开与关闭的抽象
interface IOpenAndClose{
public void open();
public void setTv(ITV itv);
}
//电视机的抽象
interface ITV{
public void play();
}
//电视机的实现
class ChangHong implements ITV{
@Override
public void play() {
System.out.println("长虹电视打开");
}
}
//打开与关闭的实现
class OpenAndClose implements IOpenAndClose{
private ITV itv;
@Override
public void open() {
this.itv.play();
}
@Override
public void setTv(ITV itv) {
this.itv=itv;
}
}
继承是面向对象三大特性之一,是一种非常优秀的语言机制,它有如下有点:
继承有它的优点,但是也有一些致命的缺点:
这个程序,B继承了A,却把A的方法重写了,如果B没有注意,还以为调用的是A的方法,结果就会出错了。
public class LisKov {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("-----------------------");
B b = new B();
System.out.println("11-3=" + b.func1(11, 3));
System.out.println("1-8=" + b.func1(1, 8));
System.out.println("11+3+9=" + b.func2(11, 3));
}
}
class A {
public int func1(int num1, int num2) {
return num1 - num2;
}
}
class B extends A {
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
}
对于上面的程序,我们该怎么优化呢?
通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉, 采用依赖,聚合,组合等关系代替.
public class LisKov {
public static void main(String[] args) {
B b=new B();
b.func3(1,1);
}
}
class C{
}
class A extends C {
public int func1(int a, int b) {
return a - b;
}
}
class B extends C {
//这里是用组合的方式使用A
private A a=new A();
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
public int func3(int a, int b){
return this.a.func1(a,b);
}
}
在学习Java里面的多态时,我们知道多态的前提就是要有子类继承父类并且子类重写父类的方法。那这是否和LSP矛盾呢?因为LSP要求我们只可以扩展父类的功能,但不能改变父类原有的功能,也就是不能对父类原有的方法进行重写,只能去实现父类的方法或重载。下面是我在知乎上找到的一种比较合理的解释:
不符合LSP最常见的情况就是:父类和子类都是非抽象类,且父类的方法被子类重新定义,这样实现继承会造成子类和父类之间的强耦合,将不相关的属性和方法搅和在一起,不利于程序的维护和扩展。所以总结一句:尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承(也就是面向接口和抽象编程)
ming和mark互相不认识,那为什么代表ming类中会有代表mark类呢?这样明显是违背了迪米特法则的。现在我们对上面的代码进行重构,根据迪米特法则的第二点:从依赖者的角度来看,只依赖应该依赖的对象。在本例中,张三只认识李四,那么只能依赖李四
public class Demeter {
public static void main(String[] args) {
Ming ming=new Ming();
ming.work();
}
}
//ming需要找朋友jack完成工作,jack不会去求助自己的朋友mark,mark完成了
class Ming{
public Jack getFriend(){//方法返回值,jack是直接朋友
return new Jack();
}
public void work(){
Jack jack=getFriend();
Mark mark=jack.getFriend();//局部变量,mark是间接朋友
mark.work();
}
}
class Jack{
public Mark getFriend(){
return new Mark();
}
}
class Mark{
public void work(){
System.out.println("做完");
}
}
public class Demeter {
public static void main(String[] args) {
Ming ming=new Ming();
ming.work();
}
}
//ming需要找朋友jack完成工作,jack不会去求助自己的朋友mark,mark完成了
class Ming{
public Jack getFriend(){//方法返回值,jack是直接朋友
return new Jack();
}
public void work(){
Jack jack=getFriend();
jack.work();
}
}
class Jack{
public Mark getFriend(){
return new Mark();
}
public void work(){
Mark mark=getFriend();//局部变量,mark是间接朋友
mark.work();
}
}
class Mark{
public void work(){
System.out.println("做完");
}
}
原则是尽量使用合成/聚合的方式,而不是使用继承
非开闭原则(错误示范)
可以看到GraphicEditor是使用方,但是现在新增三角形会修改使用方,不满足开闭原则。
package com.fys.ocp;
public class Ocp {
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("绘制三角形");
}
}
//Shape类,基类
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;
}
}
遵循开闭原则:
修改过后,需要新增图形,只需要继承基类,然后实现方法就可以啦
package com.fys.ocp;
public class Ocp {
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) {
s.draw();
}
}
//Shape类,基类
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("画三角形");
}
}
把开闭原则应用于实际项目中,我们需要注意至关重要的一点:抽象约束
抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:
原则 | 缩写 | 核心思想 |
---|---|---|
单一职责 | SRP | 类功能越少越好。一个类所拥有的方法尽可能为最少,便于维护,而且在用这个类的时候,如果该类的方法过多的话,在使用这个类的时候就会不合适,而且会出现不必要的麻烦。 |
接口隔离 | ISP | 类似SRP(单一职责),功能越少越好。宁肯多写一个接口,也不用在一个接口里写太多的方法。 |
依赖倒置原则 | DIP | 高端类尽量依赖于接口,而不依赖于低端子类。就是电脑主板依赖于接口,而内存条、硬盘等去实现接口。如果主板直接依赖于内存条、硬盘的话,内存条或者硬盘出现问题,整个主板就都用不成了。 |
里氏替换原则 | LSP | 表示子类替换父类,父类指向子类引用对象。声明一个父类,new上一个子类,来调用父类的方法,替换父类。 |
迪米特法则 | LOD | 类与类之间的关联关系越少越好。如果类与类之间的关联关系多的话,每修改一个类就要去修改与其关联的所有的类,如果有一个“调度中心”的话,任何一个类出现问题,不管是修改还是删除,都是“调度中心”的事儿了,就不用管其他的类了。 |
复用原则 | CARP | 原则是尽量使用合成/聚合的方式,而不是使用继承 |
开放封闭原则 | OCP | 对外扩展开放,对内修改封闭。 |
原则 | 缩写 | 核心思想 |
---|---|---|
单一职责 | SRP | 类功能越少越好。一个类所拥有的方法尽可能为最少,便于维护,而且在用这个类的时候,如果该类的方法过多的话,在使用这个类的时候就会不合适,而且会出现不必要的麻烦。 |
接口隔离 | ISP | 类似SRP(单一职责),功能越少越好。宁肯多写一个接口,也不用在一个接口里写太多的方法。 |
依赖倒置原则 | DIP | 高端类尽量依赖于接口,而不依赖于低端子类。就是电脑主板依赖于接口,而内存条、硬盘等去实现接口。如果主板直接依赖于内存条、硬盘的话,内存条或者硬盘出现问题,整个主板就都用不成了。 |
里氏替换原则 | LSP | 表示子类替换父类,父类指向子类引用对象。声明一个父类,new上一个子类,来调用父类的方法,替换父类。 |
迪米特法则 | LOD | 类与类之间的关联关系越少越好。如果类与类之间的关联关系多的话,每修改一个类就要去修改与其关联的所有的类,如果有一个“调度中心”的话,任何一个类出现问题,不管是修改还是删除,都是“调度中心”的事儿了,就不用管其他的类了。 |
复用原则 | CARP | 原则是尽量使用合成/聚合的方式,而不是使用继承 |
开放封闭原则 | OCP | 对外扩展开放,对内修改封闭。 |