软件设计模式的七大原则

发布时间 2023-12-06 18:21:31作者: Arkiya

1.单一职责原则 一个类应该有且仅有一个引起它变化的原因 例如 实现登录功能,不应该设计一个类,即负责数据库的连接,又负责页面的初始化,又负责数据的接收和处理 而应该把这些功能分开,分成多个不同的类,各司其职

2.开闭原则 一个软件实体应该对拓展开放对修改关闭,也就是说,当需要修改功能或者添加新的功能时,不应该通过修改源代码的方式,而应该通过拓展 举例来说,某一个图形页面提供了各种各样的按钮,例如CircleButton RectangleButton,假设一开始页面使用的是圆形按钮,并且通过一个display方法显示,那如果想换成矩形的按钮,则需要修改图形页面的源代码,这就是违背了对修改关闭

那么如何做才符合开闭原则呢? 可以实现一个抽象按钮类,让各种各样的按钮类,无论是CircleButton、RectangleButton还是其他的,都继承它,在图形页面中只需要修改XML文件即可实现切换,而不需要修改源代码。

3.里氏代换原则

简单来说,代码中任何父类的地方都应该能替换成他的子类,反之则不一定成立,更专业的说法是 任何使用基类对象的地方都可以使用子类对象替换

举例来说

public abstract class Shape {
    public abstract double getArea();
}

public class Rectangle extends Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }
}

public class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}
在这个例子中,我们有抽象类 Shape 和它的两个子类:Rectangle 和 Circle。每个形状都有一个获取面积的方法 getArea()。

假设现在有一个场景,我们需要计算一组形状的总面积。我们可以创建一个方法来处理这个问题:

java
public double getTotalArea(Shape[] shapes) {
    double total = 0.0;
    for (Shape shape : shapes) {
        total += shape.getArea();
    }
    return total;
}

那么在主类中 

public static void main(String[] args) {
    Shape[] shapes = new Shape[3];
    shapes[0] = new Rectangle(5, 4);
    shapes[1] = new Circle(3);
    shapes[2] = new Rectangle(2, 6);

    double totalArea = getTotalArea(shapes);
    System.out.println("Total area: " + totalArea);
}

可以把Shape[]中的任意一个对象赋值为它的子类对象

可以看出,里氏替换原则就遵循了开闭原则的对拓展开放对修改关闭

4.依赖倒转原则

针对接口编程,而不是针对实现

举例来说

假设我们有一个Person类,它需要接收消息,最初的设计可能是这样的:

java
class Person {
    private Email email;

    public Person(Email email) {
        this.email = email;
    }

    public void receiveMessage(String message) {
        email.send(message);
    }
}

class Email {
    public void send(String message) {
        // 发送电子邮件的具体实现
    }
}

可以看到,在Person的构造方法中,设置了this.email=email,说明它直接依赖了Email类,并与他进行了耦合

那么假设我现在需要修改,用Wechat接收,那么我需要修改Person的源代码,这显然不符合开闭原则,因此可以这样做

public Abstarct一个接受类型类,存在一个抽象的send方法,让其他的方法,无论是email、phone、wechat类都继承他,实现各自的send方法,然后在Person的构造函数中

直接写 Public Person(AbstractReceive ab),然后再receiveMessage中,使用ab.send(message),这样调用的时候,直接实例化对象,然后注入想要的类型即可

例如在main方法中

Person person = new Person(Wechat wechat);

String message = "用于测试";

person.receiveMessage;

这样就满足了依赖倒转原则

5.接口隔离原则

简单来说,使用多个专门的接口,而不是使用一个单一的总接口,分割的思想

假设我们有一个Animal接口,包含各种动物可能会执行的动作:

java
interface Animal {
    void eat();
    void sleep();
    void fly(); // 飞行方法
}
然后我们创建了两个实现类:Dog和Bird,它们分别实现了Animal接口:

java
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }

    @Override
    public void sleep() {
        System.out.println("Dog is sleeping");
    }

    @Override
    public void fly() {
        throw new UnsupportedOperationException("Dogs can't fly!");
    }
}

class Bird implements Animal {
    @Override
    public void eat() {
        System.out.println("Bird is eating");
    }

    @Override
    public void sleep() {
        System.out.println("Bird is sleeping");
    }

    @Override
    public void fly() {
        System.out.println("Bird is flying");
    }
}
在这个例子中,Dog类需要实现fly()方法,尽管狗不会飞。这就是违反了接口隔离原则的一个例子,因为Dog类被迫实现了它并不需要的方法。
为了解决这个问题,我们可以将Animal接口拆分为多个更细粒度的接口:

java
interface Eater {
    void eat();
}

interface Sleeper {
    void sleep();
}

interface Flyer {
    void fly();
}

class Dog implements Eater, Sleeper {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }

    @Override
    public void sleep() {
        System.out.println("Dog is sleeping");
    }
}

class Bird implements Eater, Sleeper, Flyer {
    @Override
    public void eat() {
        System.out.println("Bird is eating");
    }

    @Override
    public void sleep() {
        System.out.println("Bird is sleeping");
    }

    @Override
    public void fly() {
        System.out.println("Bird is flying");
    }
}

7.合成复用原则

多用关联少用继承

假设我们有一个Car类,它需要一个发动机。最初的设计可能是这样的: //Engine是一个抽象类,符合依赖倒转原则

java
class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine();
    }

    public void drive() {
        engine.start();
    }
}

class Engine {
    public void start() {
        // 启动发动机的具体实现
    }
}
在这个例子中,Car类通过组合关系包含了一个Engine对象。当我们创建一个新的Car实例时,它会自动拥有一个引擎。这就是合成复用的体现。

如果我们将上述设计改为通过继承实现,可能会像这样:

java
class Car extends Engine {
    public void drive() {
        start();
    }
}

class Engine {
    public void start() {
        // 启动发动机的具体实现
    }
}
在上述例子中,如果Car类继承了Engine类,意味着Car类不仅具有汽车的功能,还承担了发动机的责任。这是因为继承是一种"是-a-kind-of"的关系,即子类代表了一种特殊的父类。在这种情况下,
Car类被看作是一种特殊的Engine,这与现实世界中的概念不符。

 迪米特法则

不要和“陌生人说话”

假设我们有一个订单系统,包括Order、Customer和Product三个类:

java
class Order {
    private Customer customer;
    private List<Product> products;

    public Order(Customer customer, List<Product> products) {
        this.customer = customer;
        this.products = products;
    }

    public double calculateTotalPrice() {
        double totalPrice = 0;
        for (Product product : products) {
            totalPrice += product.getPrice();
        }
        return totalPrice;
    }

    public void deliver() {
        // 调用快递服务将订单送给客户
        // 这里违反了迪米特法则,因为Order类知道太多关于快递服务的知识
    }
}

class Customer {
    private String name;
    private Address address;

    // 构造函数、getter和setter省略...
}

class Product {
    private String name;
    private double price;

    // 构造函数、getter和setter省略...
}

在编写deliver的时候,如果我们直接在其中写快递的方法,就会导致Order了解太多无关的快递服务内容,那么如何避免呢?

定义一个新的类,把deliver方法作为它的方法,在Order中只需要实例化这个新的类并调用方法即可。