👉文章示例代码👈

定义

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则强调的是用抽象构建框架,用实现来扩展细节。

场景示例

笔者这里以超市中的商品做一个简单的示例来方便理解。

创建商品接口

首先创建一个商品接口,同时在接口内部定义获取商品ID、名称、价格的各个方法。一般来说,一个商品是一个实体,后续我们可以去写一个类包含商品ID、名称、价格这三个成员变量,这里仅仅为了演示开闭原则。

/**
 * @author zhh
 * @description 商品接口
 * @date 2020-02-04 22:47
 */
public interface IGoods {

    /**
     * 获取商品ID
     */
    Integer getId();

    /**
     * 获取商品名称
     */
    String getName();

    /**
     * 获取商品价格
     */
    Double getPrice();
}

创建商品子类

我们的商品其实是有很多分类的,比如食品、用品、电器等等。

下面笔者以食品为例,创建食品类的同时实现商品接口。

/**
 * @author zhh
 * @description 食品类
 * @date 2020-02-04 23:42
 */
public class Food implements IGoods {

    private Integer id;

    private String name;

    private Double price;

    public Food(Integer id, String name, Double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public Integer getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public Double getPrice() {
        return this.price;
    }
}

测试类及输出

/**
 * @author zhh
 * @description 测试类
 * @date 2020-02-04 23:57
 */
public class Test {

    public static void main(String[] args) {
        IGoods iGoods = new Food(1, "巧克力", 9.9);

        String msg = String.format("商品ID: %s, 商品名称: %s, 商品价格: %s",
                iGoods.getId(), iGoods.getName(), iGoods.getPrice());

        System.out.println(msg);
    }
}

测试类的输出结果如下:

商品ID: 1, 商品名称: 巧克力, 商品价格: 9.9

类结构图

以上示例类的结构图如下所示

需求变动

假设现在超市进行一系列的商品促销活动,全场食品类的商品打6折,那我们如何来实现这个需求?

方式一

针对上述的场景示例,我们想当然地会进行如下的改动,既快速又方便。

  • 首先在商品接口中新定义一个获取商品折扣价的方法。
/**
 * @author zhh
 * @description 商品接口
 * @date 2020-02-04 22:47
 */
public interface IGoods {
    // 此处省略其余方法
    
    // 新增 获取商品折扣价格 方法
    Double getDiscountPrice();
}
  • 然后在具体的商品子类当中去实现该方法。
/**
 * @author zhh
 * @description 食品类
 * @date 2020-02-04 23:42
 */
public class Food implements IGoods {
	// 此处省略其余方法
    
    // 子类实现该方法
    public Double getDiscountPrice() {
        return getPrice() * 0.6;
    }
}

上述简单的小改动,看样子确实是满足了我们的需求,但是仔细观察你会发现,这种改动不仅修改了接口,同时也对具体的实现类也进行了改动。假设我们的商品分类有很多,那么所有的商品实现类都需要去实现接口新增的方法。

问题也就来了。假设我们商品分类有很多,那么所有的商品实现类都需要去实现接口新增的方法。而接口作为一种契约,是不应该经常变化的,它应该是稳定且可靠的。

方式二

通过扩展新建一个食品的子类用来处理食品类打折,覆写食品类获取价格的方法,同时提供一个获取商品原价的方法。

/**
 * @author zhh
 * @description 食品打折类
 * @date 2020-02-05 00:27
 */
public class FoodDiscount extends Food {

    public FoodDiscount(Integer id, String name, Double price) {
        super(id, name, price);
    }

    public Double getOriginPrice() {
        return super.getPrice();
    }

    @Override
    public Double getPrice() {
        return this.getOriginPrice() * 0.6;
    }
}
测试类及输出
/**
 * @author zhh
 * @description 测试类
 * @date 2020-02-04 23:57
 */
public class Test {

    public static void main(String[] args) {
        IGoods iGoods = new FoodDiscount(1, "巧克力", 9.9);

        FoodDiscount foodDiscount = (FoodDiscount) iGoods;

        String msg = String.format("商品ID: %s, 商品名称: %s, 商品原价: %s, 商品折后价: %s",
                foodDiscount.getId(), foodDiscount.getName(), foodDiscount.getOriginPrice(), foodDiscount.getPrice());

        System.out.println(msg);
    }
}

测试类的输出结果如下:

商品ID: 1, 商品名称: 巧克力, 商品原价: 9.9, 商品折后价: 5.94

类结构图

方式二改动后类的结构图如下所示

好处
  • 不修改底层的基类和接口,防止了风险的扩散。
  • 通过继承基类,使得对扩展开放,对于修改接口和基类是关闭的。

优点

提高软件系统的可复用性和可维护性

参考

  • 《Head First 设计模式》
  • 《大话设计模式》