文章目录
开闭原则
描述
- 定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
- 强调
用抽象构建框架,用实现扩展细节
,是所有设计原则的一个基础 - 优点:可以提高可复用性和可维护性,通过接口或抽象类可以约束类的变化行为,基于指定策略对变化行为进行封装
举例
- 场景:现有一个原价书籍,需要一个打折价,这个时候不应该修改书籍接口,而应该扩展一个打折书籍类。
- 正例:
/**
* 书籍接口
*/
public interface Book {
String getName();
Double getPrice();
}
/**
* 原价书籍
*/
public class ParityBook implements Book {
private String name;
private Double price;
ParityBook(String name, Double price) {
this.name = name;
this.price = price;
}
@Override
public String getName() {
return this.name;
}
@Override
public Double getPrice() {
return this.price;
}
}
/**
* 打折后书籍
*/
public class DiscountBook extends ParityBook {
DiscountBook(String name, Double price) {
super(name, price);
}
@Override
public Double getPrice() {
double oldPrice = super.getPrice();
return oldPrice * 0.8 ;
}
}
/**
* 测试类
*/
public class BookPriceDemo {
public static void main(String[] args) {
ParityBook parityBook = new DiscountBook("Java", 100.00);
System.out.println(parityBook.getPrice());
}
}
// ================================== 打印结果 ===================================
80.0
- UML类图:
依赖倒置原则
描述
- 定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
针对接口编程,不要针对实现编程
- 优点:以抽象为基础搭建的架构比以细节为基础的架构要稳定灵活。下层模块尽量都要有抽象类或接口,程序稳定性更好。
变量的声明类型尽量是抽象类或接口
,这样变量引用和实际对象之间存在一个过渡空间,可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险
举例
- 场景:实现一个农场饲养的动物有哪些
- 正例:
/**
* 定义一个动物接口,以及Dog和Pig实现类
*/
public interface Animal {
String getAnimalName();
}
public class Dog implements Animal {
@Override
public String getAnimalName() {
return "哈士奇";
}
}
public class Pig implements Animal {
@Override
public String getAnimalName() {
return "佩奇";
}
}
/**
* 定义一个农场接口,声明接口方法breed,接收Animal接口参数,以及创建其实现类Farming
*/
public interface FarmFactory {
void breed(Animal animal);
}
public class Farming implements FarmFactory {
@Override
public void breed(Animal animal) {
System.out.println("农场饲养:" + animal.getAnimalName());
}
}
/**
* 创建农场类,调用breed方法,分别传入Animal子类
*/
public class FarmFactoryDemo {
public static void main(String[] args) {
FarmFactory farm = new Farming();
farm.breed(new Dog());
farm.breed(new Pig());
}
}
// ================================== 打印结果 ===================================
农场饲养:哈士奇
农场饲养:佩奇
- UML类图:可以发现,Farming和具体的实现是解耦的,和Animal接口是耦合的,以后扩展的鸡鸭鱼实现Animal接口,也不用和Farming耦合在一起。
- 反例:将所有饲养的动物全部写在Farming类,会发现这个实现类需要经常修改,扩展性很差。典型的面向实现编程,非面向接口编程。
public class Farming {
public void breedDog() {
System.out.println("正在饲养:哈士奇");
}
public void breedPig() {
System.out.println("正在饲养:佩奇");
}
}
单一职责原则
描述
- 定义:不要存在多于一个导致类变更的原因
一个类、接口、方法只负责一项职责
- 优点:降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险
举例
- 场景:定义动物的叫声
- 反例:
/**
* 一个动物声音类既有狗叫声,又有牛叫声
*/
public class AnimalVoice {
public void dogVoice() {
System.out.println("狗叫声:汪汪");
}
public void cowVoice() {
System.out.println("牛叫声:哞哞");
}
}
- 正例:
/**
* 直接将两个动物分开,保证职责的单一性(当然此处可以创建AnimalVoice接口,接口中声明声音方法,所有动物都实现该接口,扩展细节)
*/
public class DogVoice {
public void dogVoice() {
System.out.println("狗叫声:汪汪");
}
}
public class CowVoice {
public void cowVoice() {
System.out.println("牛叫声:哞哞");
}
}
接口隔离原则
描述
- 定义:用多个专门的接口,而不使用单一的总接口,
客户端不应该依赖它不需要的接口
- 一个类对一个类的依赖应该建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少
注意适度原则,一定要适度
,接口中的方法也不应过少- 优点:符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性
举例
- 场景:实现博主(Admin)和读者(User)分别对博客拥有的权限
- 正例:
/**
* 管理员权限接口:增加、修改、删除
*/
public interface AdminBlog {
Boolean insertBlog();
Boolean updateBlog();
Boolean deleteBlog();
}
/**
* 读者权限接口:阅读
*/
public interface ReadBlog {
String getBlog();
}
/**
* 博主拥有博客的所有权限
*/
public class AdminUser implements ReadBlog, AdminBlog {
@Override
public Boolean insertBlog() {
return null;
}
@Override
public Boolean updateBlog() {
return null;
}
@Override
public Boolean deleteBlog() {
return null;
}
@Override
public String getBlog() {
return null;
}
}
/**
* 读者只开放博客阅读接口
*/
public class Reader implements ReadBlog {
@Override
public String getBlog() {
return null;
}
}
- UML类图:
- 反例:将所有博客权限都写在一个接口
迪米特原则
描述
- 定义:一个对象对其他对象保持最少的了解,又叫
最少知道原则
- 强调
降低类与类之间的耦合
- 优点:降低类之间的耦合,强调只和朋友交流,不和陌生人说话。朋友即出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。
举例
- 场景:老板命令组长帮他查一下一共有多少个课程
- 反例:
/**
* 课程类
*/
public class Course {
}
/**
* 老板类,有个方法命令组长检查课程数量
*/
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader) {
// 这里模拟数据库查出20个课程
List<Course> courseList = new ArrayList<Course>();
for (int i = 0; i < 20; i++) {
courseList.add(new Course());
}
teamLeader.checkNumberOfCourses(courseList);
}
}
/**
* 组长类,打印课程数量
*/
public class TeamLeader {
public void checkNumberOfCourses(List<Course> courseList){
System.out.println("在线课程的数量是:" + courseList.size());
}
}
/**
* 测试类,老板命令组长检查课程数量
*/
public class Test {
public static void main(String[] args) {
Boss boss = new Boss();
TeamLeader teamLeader = new TeamLeader();
boss.commandCheckNumber(teamLeader);
}
}
- UML类图:
- 说明:这里看起来能实现课程数量的查询,其实发现是老板查询的课程数量,然后命令组长说出(打印)这个数量,问题在于课程不该出现在老板命令方法中,而应该和组长有关系。
- 正例:
/**
* 老板只负责命令组长检查数量
*/
public class Boss {
public void commandCheckNumber(TeamLeader teamLeader) {
teamLeader.checkNumberOfCourses();
}
}
/**
* 组长负责数课程,然后打印数量
*/
public class TeamLeader {
public void checkNumberOfCourses(){
List<Course> courseList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courseList.add(new Course());
}
System.out.println("在线课程的数量是:" + courseList.size());
}
}
public class Test {
public static void main(String[] args) {
Boss boss = new Boss();
TeamLeader teamLeader = new TeamLeader();
boss.commandCheckNumber(teamLeader);
}
}
- UML类图:
里氏替换原则
描述
- 定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型
里氏替换原则是对开闭原则的补充
- 定义扩展:一个软件实体如果适用一个父类的话,那一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变
- 引申意义:
子类可以扩展父类的功能,但不能改变父类原有的功能
- 含义一:子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 含义二:子类中可以增加自己特有的方法
- 含义三:当子类方法重载父类的方法时,方法的前置条件(即方法的入参)要比父类方法的输入参数更宽松
- 含义四:当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等。
- 优点:
- 优点一:约束继承泛滥,开闭原则的一种体现
- 优点二:加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩展性。降低需求变更时引入的风险。
举例
- 场景:
public class Calculate {
}
public class BaseCalculate extends Calculate {
public int add(int a, int b) {
return a + b;
}
}
/**
* 这里使用组合的方式完成计算
*/
public class BizCalculate extends Calculate {
private BaseCalculate baseCalculate = new BaseCalculate();
public int add(int a, int b) {
return this.baseCalculate.add(a, b);
}
}
public class CalculateDemo {
public static void main(String[] args) {
BizCalculate bizCalculate = new BizCalculate();
System.out.println(bizCalculate.add(2, 3));
}
}
- UML类图:
合成(组合)聚合原则
描述
- 定义:
尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的
- 聚合(has-A)和组合(contains-A)
- 优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
- 组合/聚合缺点:通过组合/聚合建造的系统会有较多的对象需要管理,比如A类里面有B类也有C类,也就是说管理的对象较多,而继承复用的优点是扩展性比较容易实现,因为继承父类,父类的所有功能通过继承都会自动进入子类,修改和扩展相对容易些
- 继承的缺点:会破坏包装,因为继承会将父类的实现细节暴露给子类
- 何时使用组合/聚合、继承:
聚合是 has-A,组合是 contains-A,而继承是 is-A
举例
- 场景:获取数据库连接
- 反例:
public class DBConnection {
public String getConnection() {
return "MySQL数据库连接";
}
}
public class ProductDao extends DBConnection {
public void addProduct() {
String conn = super.getConnection();
System.out.println("使用" + conn + "增加产品");
}
}
public class Test {
public static void main(String[] args) {
ProductDao productDao = new ProductDao();
productDao.addProduct();
}
}
- 说明:代码很简单,Dao层获取父类Mysql连接进行操作,现在要增加第二个数据库连接,继承方式就不好实现了,所以可以改成组合方式
- 正例:
public abstract class DBConnection {
public abstract String getConnection();
}
public class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "MySQL数据库连接";
}
}
/**
* PostgreSQL数据库
*/
public class PostgreSQLConnection extends DBConnection {
@Override
public String getConnection() {
return "PostgreSQL数据库连接";
}
}
/**
* 使用组合的方式
*/
public class ProductDao{
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addProduct(){
String conn = dbConnection.getConnection();
System.out.println("使用"+conn+"增加产品");
}
}
public class Test {
public static void main(String[] args) {
ProductDao productDao = new ProductDao();
productDao.setDbConnection(new PostgreSQLConnection());
productDao.addProduct();
}
}
- UML类图:
说在最后
其实在实际开发过程中,讲究的是一个平衡,不应该只为了程序的扩展性而一味的去遵循这些设计原则,还要考虑人力、时间、成本、质量、Deadline,如果一开始就把扩展性做的特别完美的话,成本又上来了,所以不应该过度,在适当的场景去遵循设计原则,体现的是一个取舍的问题,就像有些设计模式可能遵循两样三样而破坏一样两样,最重要的是合适的业务场景,所以设计原则不是强行遵守的,而是要讲究一个度,讲究一个取舍。