Design Patterns - Principles of Software Design

This article has participated in the "Newcomer Creation Ceremony" event to start the road of gold creation together.

3. Principles of software design

In software development, in order to improve the maintainability and reusability of software systems and increase the scalability and flexibility of software, programmers should try their best to develop programs according to six principles, thereby improving software development efficiency and saving software development. costs and maintenance costs.

3.1. The principle of opening and closing

Open for extension, closed for modification . When the program needs to be expanded, the original code cannot be modified to achieve a hot-plug effect. In short, it is to make the program expandable, easy to maintain and upgrade.

To achieve this effect, we need to use interfaces and abstract classes.

Because the abstraction is flexible and adaptable, as long as the abstraction is reasonable, the stability of the software architecture can be basically maintained. The volatile details in the software can be extended from the implementation class derived from abstraction. When the software needs to change, it is only necessary to re-derive an implementation class to expand according to the requirements.

The following takes 搜狗输入法the skin of as an example to introduce the application of the open-closed principle.

【Example】搜狗输入法The skin design.

Analysis: 搜狗输入法A skin is a combination of elements such as the input method background image, window color, and sound. Users can change the skin of their input method according to their own preferences, and can also download new skins from the Internet. These skins have common characteristics, for which an abstract class (AbstractSkin) can be defined, and each specific skin (DefaultSpecificSkin and HeimaSpecificSkin) is a subclass of it. User forms can choose or add new themes as needed without modifying the original code, so it satisfies the open-closed principle.

insert image description here

Code:

// 抽象皮肤类
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();
	}
}

insert image description here

3.2. Single Responsibility Principle

A class is only responsible for the corresponding responsibilities in one functional area, or can be defined as: For a class, there should be only one reason for it to change.

Control the granularity of classes, decouple objects, and improve their cohesion.

3.3. Liskov substitution principle

The Liskov Substitution Principle is one of the basic principles of object-oriented design.

Liskov Substitution Principle: Wherever a base class can appear, a subclass must appear. Popular understanding: subclasses can extend the functions of the parent class, but cannot change the original functions of the parent class. In other words, when a subclass inherits the parent class, try not to override the parent class's methods except adding new methods to complete the new functions.

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

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

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

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

insert image description here

代码如下:

长方形类(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接口

insert image description here

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等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。

类图如下:

insert image description here

代码如下:

希捷硬盘类(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类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。

类图如下:

insert image description here

电脑(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、接口隔离原则

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

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

【例】安全门案例

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

insert image description here

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接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:

insert image description here

代码如下:

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)。

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

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

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

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

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

类图如下:

insert image description here

代码如下:

明星类(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. High flexibility of reuse. This reuse can be done dynamically at runtime, and new objects can dynamically reference objects of the same type as the constituent objects.

Let's look at an example to understand the principle of composite reuse

[Example] Automobile classification management program

Cars can be divided into gasoline cars, electric cars, etc. according to "power source"; according to "color", they can be divided into white cars, black cars and red cars. If both classifications are considered together, there are many combinations. The class diagram is as follows:

insert image description here

From the above class diagram, we can see that there are many subclasses generated by inheritance and reuse. If there is a new power source or a new color, we need to define a new class. Let's try to change inheritance reuse to aggregation reuse.

insert image description here

Guess you like

Origin juejin.im/post/7120516348156837919