是指一个软件实体(如类、模块和函数)应该对外扩展开放,对修改关闭。所谓的关闭,也正是对扩展和修改两个行为的一个原则。
它强调的是用抽象构建框架,用实现扩展细节,可以提高软件系统的可复用性和可维护性。
开闭原则是面向对象设计的最基本原则,例如版本更新,可以实现尽量不修改源代码的前提下增加新功能。
是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。
抽象不应该依赖细节,细节应该依赖抽象。可以减少类与类之间的耦合性,提高系统的稳定性,提高代码可读性和可维护性,
降低修改程序的风险。
是指不要存在多于一个导致变更的原因。如果一个类负责两个职责,
修改其中一个,则可能导致另一个出现问题。所以将多个职责用多个类进行解耦。
是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。
①一个类对另一个类的依赖应该建立在最小的接口之上。
②建立单一接口,不要建立庞大臃肿的接口。
③尽量细化接口,接口中的方法尽量少(适度,不是越少越好)
接口隔离原则符合高内聚低耦合的设计思想,可以使类具有很好的可读性、可扩展性。
是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合度。
迪米特原则主要强调:只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称为成员朋友类,
而出现在方法体内部的类不属于朋友类,也就是说陌生的类不要以局部变量的形式出现在类的内部。
是指如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都替换为O2时,程序P
的行为没有发生变化,那么类型T2是类型T1的子类型。
可以理解为一个软件实体如果适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,
子类对象能够替换父类对象,而程序逻辑不变。
或者说子类可以扩展父类的功能,但不能改变父类原有的功能:
①子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
②子类可以增加自己特有的方法。
③当子类的方法重载父类的方法时,方法的前置条件(方法的输入/入参)要比父类方法的输入参数更宽松。
④当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(方法的输出/返回值)要比父类更严格或与父类一样。
是指尽量使用对象组合/聚合而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,
一个类的变化对其他类造成的影响相对较少。
继承叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合称为黑箱复用,我们是无法获取到类以外的对象的实现细节的。
虽然我们要根据具体的业务场景来做代码设计,但也需要遵循OOP模型。
注:
博客:
霸道流氓气质的博客_CSDN博客-C#,架构之路,SpringBoot领域博主
关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。
1、以商城商品为例,新建一个商品接口Goods
package com.ruoyi.demo.designPrinc.ocp;
/**
* 商品接口
*/
public interface IGoods {
Integer getId();
String getName();
Double getPrice();
}
2、商品有很多种类,这里新建一个电脑的类Computer
package com.ruoyi.demo.designPrinc.ocp;
public class Computer implements IGoods{
private Integer Id;
private String name;
private Double price;
public Computer(Integer id, String name, Double price) {
Id = id;
this.name = name;
this.price = price;
}
@Override
public Integer getId() {
return this.Id;
}
@Override
public String getName() {
return this.name;
}
@Override
public Double getPrice() {
return this.price;
}
}
3、现在要给电脑做活动,价格打88折。
如果修改Computer中的getPrice()方法,则存在一定风险,可能会影响另外调用获取价格的地方。既要不修改原有代码,又
要实现价格优惠这个功能,可以写一个处理优惠逻辑的类ComputerDiscounts
package com.ruoyi.demo.designPrinc.ocp;
/**
* 要给电脑类做活动,搞优惠。如果修改Computer的getPrice方法,则存在一定的风险,可能影响其他地方的调用结果
*/
public class ComputerDiscounts extends Computer{
public ComputerDiscounts(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getOrginPrice(){
return super.getPrice();
}
public Double getPrice(){
return super.getPrice()*0.88;
}
}
1、还是以商品类为例,先创建学生类
package com.ruoyi.demo.designPrinc.dip;
public class Student {
public void buyComputer(){
System.out.println("学生购买了电脑");
}
public void buyBook(){
System.out.println("学生购买了书籍");
}
}
调用学生类的购买电脑和购买书籍的方法
package com.ruoyi.demo.designPrinc.dip;
public class dipTest {
public static void main(String[] args) {
Student student = new Student();
student.buyComputer();
student.buyBook();
}
}
学生购买了电脑和书籍,如果还要购买别的商品。这时候因为业务扩展,要从低层到高层(调用层)依次修改代码。
在Student类中添加buyPen()方法,在高层调用方也要追加调用。这样一来,系统发布之后很不稳定,可能导致
意想不到的风险。
下面优化代码
创建一个商品的抽象IGoods接口,抽离出购买方法
package com.ruoyi.demo.designPrinc.dip;
public interface IGoods {
void buy();
}
然后编写购买电脑类
package com.ruoyi.demo.designPrinc.dip;
public class BuyComputer implements IGoods{
@Override
public void buy() {
System.out.println("学生买了电脑");
}
}
再编写购买书籍类
package com.ruoyi.demo.designPrinc.dip;
public class BuyBook implements IGoods{
@Override
public void buy() {
System.out.println("学生买了书籍");
}
}
修改Student
package com.ruoyi.demo.designPrinc.dip;
public class Student {
public void buy(IGoods goods){
goods.buy();
}
}
最后修改调用方代码
package com.ruoyi.demo.designPrinc.dip;
public class dipTest {
public static void main(String[] args) {
Student student = new Student();
student.buy(new BuyComputer());
student.buy(new BuyBook());
}
}
这时,当再有新的业务,比如购买手机等扩展时,只需要新建一个类,通过传参的方式告诉Student,
而不需要修改底层代码。这种方式也叫依赖注入。注入的方式还有构造器注入和Setter方法。
用商品举例,自营商品可以无条件退货,代理商品需要协商退货。创建商品类
package com.ruoyi.demo.designPrinc.srp;
public class Goods {
public void buy(String goodsType){
if("自营".equals(goodsType)){
System.out.println(goodsType+"商品可以无条件退货");
}else {
System.out.println(goodsType+"商品需要协商退货");
}
}
}
调用代码
package com.ruoyi.demo.designPrinc.srp;
public class srpTest {
public static void main(String[] args) {
Goods goods = new Goods();
goods.buy("自营");
goods.buy("代理");
}
}
上面逻辑中,商品Goods类处理两种逻辑,假如需要对商品进行活动促销,两种类型商品的活动逻辑不一样,
必须修改代码,而修改代码势必会相互影响。所以对职责进行解耦。
分别创建两个类
proprietaryGoods:
package com.ruoyi.demo.designPrinc.srp;
/**
* 自营商品
*/
public class proprietaryGoods {
public void buy(String goodsType){
System.out.println(goodsType+"商品需要协商退货");
}
}
agentGoods :
package com.ruoyi.demo.designPrinc.srp;
/**
* 代理商品
*/
public class agentGoods {
public void buy(String goodsType){
System.out.println(goodsType+"商品可以无条件退货");
}
}
调用代码修改
package com.ruoyi.demo.designPrinc.srp;
public class srpTest {
public static void main(String[] args) {
proprietaryGoods proprietaryGoods = new proprietaryGoods();
proprietaryGoods.buy("自营");
agentGoods agentGoods = new agentGoods();
agentGoods.buy("代理");
}
}
业务发展,要增加会员VIP业务。VIP可以获取商品优惠券,普通会员只能获取商品基本价格。
所以在控制商品上有两个职责,可以将展示的职责和管理的职责分开,实现同一个抽象依赖。
设计顶层接口IGoods
package com.ruoyi.demo.designPrinc.srp;
import java.util.List;
public interface IGoods {
//获取商品价格
String getGoodsPrice();
//获取商品优惠券
List getCoupons();
//购买商品
void buyGoods();
//商品退货
void returnGoods();
}
将这个接口拆分成两个接口
IGoodsInfo
package com.ruoyi.demo.designPrinc.srp;
import java.util.List;
public interface IGoodsInfo {
//获取商品价格
String getGoodsPrice();
//获取商品优惠券
List getCoupons();
}
IGoodsManager
package com.ruoyi.demo.designPrinc.srp;
public interface IGoodsManager {
//购买商品
void buyGoods();
//商品退货
void returnGoods();
}
商品类接口
package com.ruoyi.demo.designPrinc.isp;
public interface IGoods {
void eat();
void drink();
void wear();
}
有能吃的、能喝的、能穿的。
食品类
package com.ruoyi.demo.designPrinc.isp;
public class FoodGoods implements IGoods{
@Override
public void eat() {
}
@Override
public void drink() {
}
@Override
public void wear() {
}
}
衣物类
package com.ruoyi.demo.designPrinc.isp;
public class ClothesGoods implements IGoods{
@Override
public void eat() {
}
@Override
public void drink() {
}
@Override
public void wear() {
}
}
所以,如果按照上面的进行设计,那么食品类的wear只能空着,衣物类的eat和drink也只能空着。
所以分别设计吃、喝、穿三个接口
package com.ruoyi.demo.designPrinc.isp;
public interface IEatGoods {
void eat();
}
package com.ruoyi.demo.designPrinc.isp;
public interface IDrinkGoods {
void drink();
}
package com.ruoyi.demo.designPrinc.isp;
public interface IWearGoods {
void wear();
}
然后食品类是需要实现吃、喝接口即可
package com.ruoyi.demo.designPrinc.isp;
public class FoodGoodsNew implements IEatGoods,IDrinkGoods{
@Override
public void drink() {
}
@Override
public void eat() {
}
}
老板要采购员统计符合条件的商品数量。
商品类
package com.ruoyi.demo.designPrinc.lod;
public class Goods {
}
采购员
package com.ruoyi.demo.designPrinc.lod;
import java.util.List;
public class Buyer {
public void checkNumberOfGoods(List goodsList){
System.out.println("符合条件的商品数量为:"+goodsList.size());
}
}
老板
package com.ruoyi.demo.designPrinc.lod;
import java.util.ArrayList;
import java.util.List;
public class Boss {
public void commandCheckNumber(Buyer buyer){
//模拟老板一页一页往下翻页,采购员实时统计
List goodsList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
goodsList.add(new Goods());
}
buyer.checkNumberOfGoods(goodsList);
}
}
测试代码
package com.ruoyi.demo.designPrinc.lod;
public class lodTest {
public static void main(String[] args) {
Boss boss = new Boss();
Buyer buyer = new Buyer();
boss.commandCheckNumber(buyer);
}
}
根据迪米特原则,Boss只想要结果,不需要跟商品直接交流。但是采购员统计需要引用商品对象。
所以在Boss类中就出现了Goods商品类。
只需要将Boss类中的Goods类交给采购员,使Boss与Goods不再有关联。
Buyer:
package com.ruoyi.demo.designPrinc.lod;
import java.util.ArrayList;
import java.util.List;
public class Buyer {
public void checkNumberOfGoods(){
List goodsList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
goodsList.add(new Goods());
}
System.out.println("符合条件的商品数量为:"+goodsList.size());
}
}
Boss:
package com.ruoyi.demo.designPrinc.lod;
public class Boss {
public void commandCheckNumber(Buyer buyer){
buyer.checkNumberOfGoods();
}
}
新建鸟类,有飞行速度属性,可以根据飞行距离和飞行速度计算飞行时间
package com.ruoyi.demo.designPrinc.lsp;
/**
* 鸟类
*/
public class Bird {
//飞行速度
double flySpeed;
public double getFlySpeed() {
return flySpeed;
}
public void setFlySpeed(double flySpeed) {
this.flySpeed = flySpeed;
}
public double getFlyTime(double distance){
return (distance/flySpeed);
}
}
新建燕子类,继承自鸟类
package com.ruoyi.demo.designPrinc.lsp;
/**
* 燕子类
*/
public class Swallow extends Bird{
}
新建企鹅类,继承自鸟类
package com.ruoyi.demo.designPrinc.lsp;
/**
* 企鹅类
*/
public class Penguin extends Bird{
public void setFlySpeed(double speed){
flySpeed = 0;
}
}
因为企鹅不会飞,所以设置其飞行速度为0
计算飞行时间业务类
package com.ruoyi.demo.designPrinc.lsp;
public class lspTest {
public static void main(String[] args) {
Bird swallow = new Swallow();
swallow.setFlySpeed(100);
Bird penguin = new Penguin();
penguin.setFlySpeed(100);
try {
System.out.println("燕子飞行50公里耗时:"+swallow.getFlyTime(50));
System.out.println("企鹅飞行50公里耗时:"+penguin.getFlyTime(50));
}catch (Exception e){
System.out.println("出错:"+e.getMessage());
}
}
}
上面的示例中,因为企鹅不具备飞行能力,重写了鸟类的速度方法,违背了里氏替换原则,所以当计算
企鹅的飞行时间时出现了除数不能为0的错误。
将上面的继续关系修改为,增加一个动物类,描述动物的普遍通用行为,比如计算时间和速度,鸟类和企鹅分别继承动物类,
燕子继承鸟类。这样燕子能计算飞行时间,企鹅能计算奔跑时间,同时又避免了重写父类的方法,符合里氏替换原则。
以数据库操作添加商品为例,创建数据库连接类
public class DBConnection {
public String getConnection(){
return "Mysql 数据库连接";
}
}
然后创建GoodsDao
package com.ruoyi.demo.designPrinc.carp;
public class GoodsDao {
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addGoods(){
String connection = dbConnection.getConnection();
System.out.println("使用连接"+connection+"增加商品成功");
}
}
上面的DBConnection还不是一种抽象,不便于系统扩展。目前的系统支持Mysql数据库连接。
后续业务扩展,需要支持Oracle数据库连接。
如果直接在DBConnection中增加对Oracle数据库的支持,会违背开闭原则。
可以在不修改Dao代码的前提下,而将DBConnection修改为abstract的。
package com.ruoyi.demo.designPrinc.carp;
public abstract class DBConnection {
public abstract String getConnection();
}
然后将Mysql的逻辑抽离
package com.ruoyi.demo.designPrinc.carp;
public class MySqlConnection extends DBConnection{
@Override
public String getConnection() {
return "Mysql数据库连接";
}
}
再创建Oracle连接
package com.ruoyi.demo.designPrinc.carp;
public class OracleConnection extends DBConnection{
@Override
public String getConnection() {
return "Oracle 数据库连接";
}
}
具体选择交给应用层。