目录
一、单一职责原则(Single Responsibility Principle, SRP)
二、开放封闭原则(Open-Closed Principle, OCP)
三、依赖倒置原则(Dependency Inversion Principle, DIP)
四、里氏替换原则(Liskov Substitution Principle, LSP)
五、接口隔离原则(Interface Segregation Principle, ISP)
六、合成复用原则(Composite Reuse Principle, CRP)
七、迪米特法则(Demeter Principle, DP)
设计模式的七大原则是软件设计和开发中的重要指导原则,它们帮助开发者创建可扩展、可维护和灵活的软件系统。这些原则包括:
- 单一职责原则(Single Responsibility Principle, SRP):一个类应该只有一个引起变化的原因。这意味着每个类应该有一个明确的职责,并且仅负责完成一项功能,这样可以使类更加模块化和可维护。
- 开放封闭原则(Open-Closed Principle, OCP):软件实体(如类、模块或函数)应该对扩展开放,对修改关闭。这意味着当软件需要适应新的环境或需求时,应该通过添加新的代码来扩展系统的行为,而不是修改现有的代码。
- 依赖倒置原则(Dependency Inversion Principle, DIP):高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这意味着代码应该依赖于接口或抽象类,而不是具体的实现类。
- 里氏替换原则(Liskov Substitution Principle, LSP):子类型必须能够替换其基类型而不会产生任何问题。这确保了继承关系的正确使用,子类应该能够保持与父类相同的接口和行为,从而保持系统的稳定性和可维护性。
- 接口隔离原则(Interface Segregation Principle, ISP):客户端不应该依赖于它不需要的接口。这意味着接口应该被细分为更小的、更具体的接口,这样客户端只需要知道和使用它感兴趣的方法。
- 合成复用原则(Composite Reuse Principle, CRP):优先使用对象组合而不是继承来达到复用的目的。这意味着在面向对象设计中,应该优先考虑通过组合或聚合关系来复用已有的设计和实现,而不是通过继承。
- 迪米特法则(Demeter Principle, DP):一个对象应当仅与它的朋友(friendly objects)说话。这有助于减少对象之间的耦合,提高软件的可维护性和可读性。
遵循这些原则可以帮助开发人员创建更加灵活、可维护和可扩展的软件系统。
一、单一职责原则(Single Responsibility Principle, SRP)
定义:一个类应该只有一个引起它变化的原因。
一个类 / 接口 / 方法只负责一项职责
优点:降低类的负责度、提高类的可读性,提高系统的可维护性、降低变更的风险。
public interface UserService {
void updateUser(User user,int opt);
}
传入修改类型,用户名,去修改用户信息。但这里却违背了(单一职责原则)
改造方法:
public interface UserService {
void changeName(String name);
void changePwd(String pwd);
}
这种更符合我们单一职责原则。
但是我们开发的时候可能会有些迷茫,什么时候去划分,什么时候放到一起,那这个时候,就应该去重新审视代码,哪些是需要合并到一起,哪些是要应用。
这里的原则,既是最简单的原则,也是最难的原则,难的时候,可能我们不是特别好区分。
二、开放封闭原则(Open-Closed Principle, OCP)
定义:一个软件实体,例如类、模块、函数,应该对扩展是开放的,对修改是关闭的。
实现:用抽象构建框架,用实现扩展细节。
优点:提高软件系统的可复用性及可维护性。
用例:
书籍实体:
public interface IBook {
/**
* 编号
* @return
*/
Integer getId();
/**
* 名称
* @return
*/
String getName();
/**
* 价格
* @return
*/
Double getPrice();
}
书籍接口:
public class BookImpl implements IBook {
private Integer id;
private String name;
private Double price;
public BookImpl(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public Integer getId() {
return id;
}
@Override
public String getName() {
return name;
}
@Override
public Double getPrice() {
return price;
}
}
测试用例:
public class Test {
public static void main(String[] args) {
DiscountBookImpl book = new DiscountBookImpl(1,"java", 100.0);
System.out.println("Book Id: " + book.getId() + ", Title: " + book.getTitle() + ", Price: " + book.getPrice());
}
}
假设我们来了业务需求,书籍打折,我们在原来的书籍接口上这样修改:
public class BookImpl implements IBook {
private Integer id;
private String name;
private Double price;
public BookImpl(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public Integer getId() {
return id;
}
@Override
public String getName() {
return name;
}
@Override
public Double getPrice() {
return this.price * 0.8;
}
}
这样本身就违背了我们这条原则,正确的做法应该是:
新建一个打折书籍接口:
public class DiscountBookImpl extends BookImpl{
public DiscountBookImpl(Integer id, String name, Double price, Double discount) {
super(id, name, price);
this.discount = discount;
}
@Override
public Double getPrice() {
return super.getPrice() * 0.8;
}
public Double getOriginalPrice() {
return super.getPrice();
}
}
三、依赖倒置原则(Dependency Inversion Principle, DIP)
定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程。
面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而大大提高了开发的成本。
面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。
优点:可以减少类间的耦合性,提高系统的稳定性,提高代码可读性和维护性,可降低修改程序所造成的风险。
用例:
学生实体:
public class Student {
public void studyJavaCourse() {
System.out.println("学习Java课程");
}
public void studyPythonCourse() {
System.out.println("学习Python课程");
}
}
学生学习课程:
public class Test {
public static void main(String[] args) {
Student student = new Student();
student.studyJavaCourse();
student.studyPythonCourse();
}
}
降低耦合改造:
新建课程接口:
public interface ICourse {
void study();
}
新建Java课程类:
public class JavaCourse implements ICourse {
@Override
public void study(){
System.out.println("JavaCourse study");
}
}
新建Python类:
public class PythonCourse extends ICourse {
@Override
public void study(){
System.out.println("PythonCourse study");
}
}
改造学生实现类:
public class Student {
public void study(ICourse course){
course.study();
}
}
改造测试类:
public class Test {
public static void main(String[] args) {
JavaCourse javaCourse = new JavaCourse();
Student student = new Student();
Student.study(javaCourse);
}
}
改造之后,依赖于上层、下层通过接口访问。
改造2【利用Spring构造器依赖注入原理】
Student改造:
public class Student {
ICourse course;
public Student(ICourse course){
this.course = course;
}
public void study(){
course.study();
}
}
测试类改造:
public class Test {
public static void main(String[] args) {
JavaCourse javaCourse = new JavaCourse();
Student student = new Student(javaCourse);
Student.study();
}
}
改造3【利用Spring Set注入】
Student改造:
public class Student {
ICourse course;
public void setCourse(ICourse course){
this.course = course;
}
public void study(){
course.study();
}
}
测试类改造:
public class Test {
public static void main(String[] args) {
JavaCourse javaCourse = new JavaCourse();
Student student = new Student();
student.setCourse(javaCourse);
Student.study();
}
}
备注:执行的过程是一样的
四、里氏替换原则(Liskov Substitution Principle, LSP)
定义:派生类(子类)对象可以在程式中替代其基类(超类)对象。
因为继承带来的侵入性,增加了耦合性,也降低了代码灵活性,父类修改代码,子类也会受到影响,此时就需要里氏替换原则。
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。【在java里面可以重写,但是不建议】
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
用例:
父类:
public class Parent {
public void method(int i){
System.out.println("Parent method");
}
}
子类:
public class Child extends Parent {
@Override
public void method(int i){
System.out.println("Child method");
}
}
单元测试:
public class Test {
public static void main(String[] args) {
Parent p = new Parent();
p.method(10);
}
}
执行结果:
当改成子类时:
public class Test {
public static void main(String[] args) {
Parent p = new Child();
p.method(10);
}
}
用里氏替换原则进行调整。
父类:
public abstract class Parent {
public List method(ArrayList i){
return null;
}
public abstract ArrayList<String> method(List i);
public abstract void abstractMethod(int i);
}
子类:
public class Child extends Parent {
@Override
public ArrayList<String> method(List i){
System.out.println("Child method");
return null;
}
@Override
public void abstractMethod(int i) {
}
public void method2(int i){
System.out.println("Child method2");
}
}
优点:
- 提高了代码的重用性,子类拥有父类的方法和属性;
- 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;
缺点:侵入性、不够灵活、高耦合
- 继承是侵入的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性;
- 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。
五、接口隔离原则(Interface Segregation Principle, ISP)
定义:用多个专门的接口,不使用单一的总接口,客户端不应该依赖他不需要的接口
- 一个类对应一个类的依赖应该建立在最小的接口上
- 建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
优点:符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性
通俗点讲,把几个大的接口分成小接口,把接口细化。
动物接口:
public interface IAnimal {
void pref();
void fly();
void swim();
}
狗实体:
public class Dog implements Animal {
@Override
public void prey(){
}
@Override
public void fly(){
}
@Override
public void swim(){
}
}
但是狗不会飞
鸟实体:
public class Bird implements Animal{
@Override
public void prey(){
}
@Override
public void fly(){
}
@Override
public void swim(){
}
}
鸟也不会游泳
这个时候,我们就应该把接口隔离,把它分开。
public class Bird implements IPreyable, IFlyable{
@Override
public void prey(){
}
@Override
public void fly(){
}
}
设计接口的时候呢,过大过小都不好,只有不断的思考,才能去实现
六、合成复用原则(Composite Reuse Principle, CRP)
定义:软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
问题由来:通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
解决方案:合成复用原则,是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用的效果。
继承关系:
引擎类:
public class Engine {
//发动机功率
String power;
//发动机排量
String displacement;
}
汽车类:
public class Car extends Engine {
//汽车型号
String type;
//汽车重量
String weight;
}
使用合成类进行改造:
public class Car{
//汽车型号
String type;
//汽车重量
String weight;
Engine engine;
}
使用之前的类作为现在的一个属性
合成是拥有关系,聚合是整体与部分的关系。
聚合:班级,学生,班级是一个整体,学生是一个部分,一个班级里有很多班级,这种关系叫聚合。而汽车,汽车有很多构建,他们有很多东西组成。聚合是实心的对象与实心的组合,而合成是空心与实心的实现。两者非常像。要根据具体业务进行区分。
七、迪米特法则(Demeter Principle, DP)
定义:一个对象对其他对象保持最少的了解。又叫最少知道原则
通俗点讲:小明、小张都想知道彼此新入职公司对方的工资,但是他们作为职业打工人心里都默念打工人基本素养(社会上的事情少打听,以免破坏员工和谐)
- 强调只和朋友交流
- 朋友:出现在成员变量、方法的输入、输出参数中的类成为成员朋友类,而出现在方法体内部的类不属于朋友类
优点:降低类之间的耦合
1987年美国在一个项目组提出的概念。
用例:
学生类:
public class Student {
}
班长类:
public class Monitor {
public void count(List<Student> students){
System.out.println("学生数:"+students.size());
}
}
老师类:
public class Teacher {
public void commond(Monitor monitor){
List<Student> students = new ArrayList<>();
for(int i=0;i<10;i++){
students.add(new Student());
}
monitor.count(students);
}
}
测试类:
public class Test {
public static void main(String[] args) {
Teacher teacher = new Teacher();
teacher.commond(new Monitor());
}
}
改造,老师只跟班长进行交流、不跟学生进行交流:
班长类:
public class Monitor {
public void count(){
List<Student> students = new ArrayList<>();
for(int i=0;i<10;i++){
students.add(new Student());
}
System.out.println("学生数:"+students.size());
}
}
老师类:
public class Teacher {
public void commond(Monitor monitor){
monitor.count();
}
}
单元测试类:
public class Test {
public static void main(String[] args) {
Teacher teacher = new Teacher();
teacher.commond(new Monitor());
}
}
明显职责变得更清晰了