Patrones de diseño - Principios del diseño de software

Este artículo ha participado en el evento "Ceremonia de creación de recién llegados" para comenzar juntos el camino de la creación de oro.

3. Principios del diseño de software

En el desarrollo de software, para mejorar la capacidad de mantenimiento y reutilización de los sistemas de software y aumentar la escalabilidad y flexibilidad del software, los programadores deben hacer todo lo posible para desarrollar programas de acuerdo con seis principios, mejorando así la eficiencia del desarrollo de software y ahorrando costos y mantenimiento de software. costos

3.1 El principio de apertura y cierre

Abierto para extensión, cerrado para modificación . Cuando es necesario expandir el programa, el código original no se puede modificar para lograr un efecto de conexión en caliente. En resumen, es hacer que el programa sea ampliable, fácil de mantener y actualizar.

Para lograr este efecto, necesitamos usar interfaces y clases abstractas.

Debido a que la abstracción es flexible y adaptable, siempre que la abstracción sea razonable, básicamente se puede mantener la estabilidad de la arquitectura del software. Los detalles volátiles en el software se pueden extender desde la clase de implementación derivada de la abstracción.Cuando el software necesita cambiar, solo es necesario volver a derivar una clase de implementación para expandirse de acuerdo con los requisitos.

A continuación se toma 搜狗输入法la piel de como ejemplo para introducir la aplicación del principio abierto-cerrado.

【Ejemplo】搜狗输入法El diseño de la piel.

Análisis: 搜狗输入法una máscara es una combinación de elementos como la imagen de fondo del método de entrada, el color de la ventana y el sonido. Los usuarios pueden cambiar la máscara de su método de entrada de acuerdo con sus propias preferencias y también pueden descargar nuevas máscaras de Internet. Estos skins tienen características comunes, por lo que se puede definir una clase abstracta (AbstractSkin), y cada skin específico (DefaultSpecificSkin y HeimaSpecificSkin) es una subclase de la misma. Los formularios de usuario pueden elegir o agregar nuevos temas según sea necesario sin modificar el código original, por lo que satisface el principio abierto-cerrado.

inserte la descripción de la imagen aquí

Código:

// 抽象皮肤类
public abstract class AbstractSkin {
	public abstract void display();
}
// 默认皮肤类
public class DefaultSkin extends AbstractSkin {
	@Override
	public void display() {
		System.out.println("默认皮肤");
	}
}
// 自定义皮肤
public class HeiSkin extends AbstractSkin {
	@Override
	public void display() {
		System.out.println("自定义皮肤");
	}
}
// 搜狗输入法
public class SougouInput {
	private AbstractSkin skin;

	public void setSkin(AbstractSkin skin) {
		this.skin = skin;
	}

	public void display() {
		skin.display();
	}
}
public class Client {
	public static void main(String[] args) {
		// 1. 创建搜狗输入法对象
		SougouInput sougouInput = new SougouInput();
		// 2. 创建皮肤对象
		// DefaultSkin skin = new DefaultSkin();
		HeiSkin skin = new HeiSkin();
		// 3. 将皮肤设置到输入法中
		sougouInput.setSkin(skin);

		// 4. 显示皮肤
		sougouInput.display();
	}
}

inserte la descripción de la imagen aquí

3.2 Principio de responsabilidad única

Una clase solo es responsable de las responsabilidades correspondientes en un área funcional, o puede definirse como: Para una clase, debe haber solo una razón para cambiar.

Controle la granularidad de las clases, desacople objetos y mejore su cohesión.

3.3 Principio de sustitución de Liskov

El principio de sustitución de Liskov es uno de los principios básicos del diseño orientado a objetos.

Principio de sustitución de Liskov: dondequiera que pueda aparecer una clase base, debe aparecer una subclase. Entendimiento popular: las subclases pueden extender las funciones de la clase principal, pero no pueden cambiar las funciones originales de la clase principal. En otras palabras, cuando una subclase hereda la clase principal, intente no anular los métodos de la clase principal, excepto agregar nuevos métodos para completar las nuevas funciones.

如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

下面看一个里氏替换原则中经典的一个例子

【例】正方形不是长方形。

在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。

inserte la descripción de la imagen aquí

代码如下:

长方形类(Rectangle):

public class Rectangle {

	private double length;
	private double width;

	public double getLength() {
		return length;
	}

	public void setLength(double length) {
		this.length = length;
	}

	public double getWidth() {
		return width;
	}

	public void setWidth(double width) {
		this.width = width;
	}
}

正方形(Square):

由于正方形的长和宽相同,所以在方法setLength和setWidth中,对长度和宽度都需要赋相同值。

public class Square extends Rectangle {

    @Override
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }

    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setLength(width);
    }
}

类RectangleDemo是我们的软件系统中的一个组件,它有一个resize方法依赖基类Rectangle,resize方法是RectandleDemo类中的一个方法,用来实现宽度逐渐增长的效果。

public class RectangleDemo {
	public static void main(String[] args) {
		// 创建长方形
		Rectangle rectangle = new Rectangle();

		rectangle.setLength(20);
		rectangle.setWidth(10);

		resize(rectangle);
		printLengthAndWidth(rectangle);

		System.out.println("=============");
        
		// 创建正方形
		Square square = new Square();
		square.setLength(10);
		resize(square);
		printLengthAndWidth(square);
	}

	// 扩宽方法
	public static void resize(Rectangle rectangle) {
		// 判断宽如果比长小,进行扩宽的操作
		if (rectangle.getWidth() <= rectangle.getLength()) {
			rectangle.setWidth(rectangle.getLength() + 1);
		}
	}

	// 打印长和宽
	public static void printLengthAndWidth(Rectangle rectangle) {
		System.out.println(rectangle.getLength());
		System.out.println(rectangle.getWidth());
	}
}

我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。

我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。

如何改进呢?此时我们需要重新设计他们之间的关系。抽象出来一个四边形接口(Quadrilateral),让Rectangle类和Square类实现Quadrilateral接口

inserte la descripción de la imagen aquí

public interface Quadrilateral {

   double getLength();

   double getWidth();
}
public class Square implements Quadrilateral {

   private double side;

   public double getSide() {
      return side;
   }

   public void setSide(double side) {
      this.side = side;
   }

   @Override
   public double getLength() {
      return side;
   }

   @Override
   public double getWidth() {
      return side;
   }
}
public class Rectangle implements Quadrilateral {

   private double length;
   private double width;

   public void setLength(double length) {
      this.length = length;
   }

   public void setWidth(double width) {
      this.width = width;
   }

   @Override
   public double getLength() {
      return length;
   }

   @Override
   public double getWidth() {
      return width;
   }
}
public class RectangleDemo {
   public static void main(String[] args) {
      // 创建长方形
      Rectangle rectangle = new Rectangle();
      rectangle.setLength(20);
      rectangle.setWidth(10);
      resize(rectangle);
      printLengthAndWidth(rectangle);
   }

   // 扩宽方法
   public static void resize(Rectangle rectangle) {
      // 判断宽如果比长小,进行扩宽的操作
      if (rectangle.getWidth() <= rectangle.getLength()) {
         rectangle.setWidth(rectangle.getLength() + 1);
      }
   }

   // 打印长和宽
   public static void printLengthAndWidth(Quadrilateral quadrilateral) {
      System.out.println(quadrilateral.getLength());
      System.out.println(quadrilateral.getWidth());
   }
}

3.4、依赖倒转原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

下面看一个例子来理解依赖倒转原则

【例】组装电脑

现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。

类图如下:

inserte la descripción de la imagen aquí

代码如下:

希捷硬盘类(XiJieHardDisk):

public class XiJieHardDisk implements HardDisk {

    public void save(String data) {
        System.out.println("使用希捷硬盘存储数据" + data);
    }

    public String get() {
        System.out.println("使用希捷希捷硬盘取数据");
        return "数据";
    }
}

Intel处理器(IntelCpu):

public class IntelCpu implements Cpu {

    public void run() {
        System.out.println("使用Intel处理器");
    }
}

金士顿内存条(KingstonMemory):

public class KingstonMemory implements Memory {

    public void save() {
        System.out.println("使用金士顿作为内存条");
    }
}

电脑(Computer):

public class Computer {

    private XiJieHardDisk hardDisk;
    private IntelCpu cpu;
    private KingstonMemory memory;

    public IntelCpu getCpu() {
        return cpu;
    }

    public void setCpu(IntelCpu cpu) {
        this.cpu = cpu;
    }

    public KingstonMemory getMemory() {
        return memory;
    }

    public void setMemory(KingstonMemory memory) {
        this.memory = memory;
    }

    public XiJieHardDisk getHardDisk() {
        return hardDisk;
    }

    public void setHardDisk(XiJieHardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public void run() {
        System.out.println("计算机工作");
        cpu.run();
        memory.save();
        String data = hardDisk.get();
        System.out.println("从硬盘中获取的数据为:" + data);
    }
}

测试类(TestComputer):

测试类用来组装电脑。

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();
    }
}

上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。

根据依赖倒转原则进行改进:

代码我们只需要修改Computer类,让Computer类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。

类图如下:

inserte la descripción de la imagen aquí

电脑(Computer):

public class Computer {

    private HardDisk hardDisk;
    private Cpu cpu;
    private Memory memory;

    public HardDisk getHardDisk() {
        return hardDisk;
    }

    public void setHardDisk(HardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public Cpu getCpu() {
        return cpu;
    }

    public void setCpu(Cpu cpu) {
        this.cpu = cpu;
    }

    public Memory getMemory() {
        return memory;
    }

    public void setMemory(Memory memory) {
        this.memory = memory;
    }

    public void run() {
        System.out.println("计算机工作");
    }
}

面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象, 实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。

3.5、接口隔离原则

客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。

下面看一个例子来理解接口隔离原则

【例】安全门案例

我们需要创建一个黑马 品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。类图如下:

inserte la descripción de la imagen aquí

public interface SafetyDoor {

   // 防盗
   void antiTheft();

   // 防火
   void fireProof();

   // 防水
   void waterProof();
}
public class HeimaSafetyDoor implements SafetyDoor {
   @Override
   public void antiTheft() {
      System.out.println("防盗");
   }

   @Override
   public void fireProof() {
      System.out.println("防火");
   }

   @Override
   public void waterProof() {
      System.out.println("防水");
   }
}
public class Client {
   public static void main(String[] args) {
      HeimaSafetyDoor door = new HeimaSafetyDoor();
      door.antiTheft();
      door.fireProof();
      door.waterProof();
   }
}

上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:

inserte la descripción de la imagen aquí

代码如下:

AntiTheft(接口):

public interface AntiTheft {
    void antiTheft();
}

Fireproof(接口):

public interface Fireproof {
    void fireproof();
}

Waterproof(接口):

public interface Waterproof {
    void waterproof();
}

HeiMaSafetyDoor(类):

public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof {
    public void antiTheft() {
        System.out.println("防盗");
    }

    public void fireproof() {
        System.out.println("防火");
    }

    public void waterproof() {
        System.out.println("防水");
    }
}

ItcastSafetyDoor(类):

public class ItcastSafetyDoor implements AntiTheft,Fireproof {
    public void antiTheft() {
        System.out.println("防盗");
    }

    public void fireproof() {
        System.out.println("防火");
    }
}

3.6、迪米特法则

迪米特法则又叫最少知识原则。

只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。

其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

下面看一个例子来理解迪米特法则

【例】明星与经纪人的关系实例

明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。

类图如下:

inserte la descripción de la imagen aquí

代码如下:

明星类(Star)

public class Star {
    private String name;

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

    public String getName() {
        return name;
    }
}

粉丝类(Fans)

public class Fans {
    private String name;

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

    public String getName() {
        return name;
    }
}

媒体公司类(Company)

public class Company {
    private String name;

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

    public String getName() {
        return name;
    }
}

经纪人类(Agent)

public class Agent {
    private Star star;
    private Fans fans;
    private Company company;

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

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

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

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

    public void business() {
        System.out.println(company.getName() + "与明星" + star.getName() + "洽淡业务。");
    }
}
public class Client {
   public static void main(String[] args) {
      Agent agent = new Agent();

      Star star = new Star("林青霞");
      agent.setStar(star);

      Fans fans = new Fans("李四");
      agent.setFans(fans);

      Company company = new Company("黑马公司");
      agent.setCompany(company);

      agent.meeting();
      agent.business();
   }
}

3.7、合成复用原则

合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

通常类的复用分为继承复用和合成复用两种。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. Alta flexibilidad de reutilización. Esta reutilización se puede realizar dinámicamente en tiempo de ejecución, y los nuevos objetos pueden hacer referencia dinámicamente a objetos del mismo tipo que los objetos constituyentes.

Veamos un ejemplo para entender el principio de reutilización compuesta.

[Ejemplo] Programa de gestión de clasificación de automóviles

Los autos se pueden dividir en autos de gasolina, autos eléctricos, etc. según la "fuente de energía", según el "color", se pueden dividir en autos blancos, autos negros y autos rojos. Si ambas clasificaciones se consideran juntas, hay muchas combinaciones. El diagrama de clases es el siguiente:

inserte la descripción de la imagen aquí

Del diagrama de clases anterior, podemos ver que hay muchas subclases generadas por herencia y reutilización.Si hay una nueva fuente de energía o un nuevo color, necesitamos definir una nueva clase. Intentemos cambiar la reutilización de herencia por la reutilización de agregación.

inserte la descripción de la imagen aquí

Supongo que te gusta

Origin juejin.im/post/7120516348156837919
Recomendado
Clasificación