前言
软件设计模式和设计原则是成为一个软件架构师的基本功,较好的理解这些基础知识无疑是十分重要的。在这篇文章中荔枝将会比较详细梳理一下面向对象的七大原则,大家可以先看看这部分内容再去学习设计模式会比较好哈哈哈哈~~~
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,从而提高软件开发效率、节约软件开发成本和维护成本,开发必须要根据面向对象的七大原则来执行。首先我们需要了解一些术语:
- 可维护性:在不破坏原有代码设计、不引入新的ug的情况下,能够快速地修改或者添加代码;
- 可扩展性:在不修改或少量修改原有代码的情况下,可以通过扩展的方式添加新的功能代码;
- 可复用性:尽量减少重复代码的编写,直接复用已有的代码;
- 内聚性:模块内部元素的紧密程度,内聚性越高,那么模块独立性越好,可维护性,复用性也越高;
- 耦合性:模块与模块之间的关联关系,耦合性越高,那么模块与模块之间关联越复杂,那么可维护性,复用性越差;
面向对象程序设计七大原侧:
单一职责原则SRP
一个类或者模块只负责完成一个功能。通俗的讲,如果这个类包含了两个或多个业务不相干的功能,那么这个类的职责就不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又是最难运用的原则,需要设计人员发现类的不同职责并将其分离。
下面demo中的两个类的职责是划分出来的
package com.crj.principle;import java.util.List;public class UserInfo {private Long UserID;private String userName;private String phone;private List<Address> addresses;public void save(){System.out.println("用户信息");}
}
package com.crj.principle;public class Address {private String province;private String city;public void saveAddress(){System.out.println("保存地址信息");}
}
里氏替换原则LSP
子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,如果重写父类方法,程序运行会发生出错概率。如果一定要用多态,那么父类可以设计成抽象父类或者接口。 所有可以使用父类的地方,必须能够透明的使用子类。
下面来看看一个例子:
package com.crj.principle.t12;public class Bird {protected double runspeed;protected double flyspeed;public void setRunspeed(double runspeed) {this.runspeed = runspeed;}public void setFlyspeed(double flyspeed) {this.flyspeed = flyspeed;}public double canGetInstance(double distance){return distance/flyspeed;}
}
我们定义了一个Bird类,如果子类A重写了Bird的setFlyspeed的方法并修改其功能(比如说不会飞的鸟我们返回一个0.0),那么就破环了里氏替换原则。这时候我们可以在Bird上面抽象一个父类出来并由新的不会飞的鸟类C去继承相应的父类来遵循里氏替换原则。
开闭原则OCP
对扩展开放,对修改关闭。在程序需要修进行拓展的时候,不能去改原有的代码,实现一个热插拔的效果。想要达到这样的效果,我们需要使用接口和抽象类。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
依赖倒置原则DIP
模块之间要依赖抽象,不依赖实现类,要面向接口编程,不要面向实现编程。高层模块不应该直接依赖低层模块,这样就降低了客户端与实现模块间的耦合。简单来说我们封装模块的时候首先应该在接口或者抽象类定义相应功能方法,再用实现类来实现相应的具体功能,而不是直接用一个类就把所有的功能实现,在具体的功能实现时类只会调用相应接口对象的实现方法。
上面这段话感觉是不是比较难懂呢,我们看看这个例子:假设一个场景我们需要挑选不同的CPU和内存来组装计算机,我们当然可以只用类来实现功能,但是类中的CPU和内存类型写死了,如果又有新的选择无疑使得代码比较难以维护,因此我们可以根据依赖倒置原则来实现功能:
- 首先定义两个接口
- 接着定义各自的接口实现类
- 定义对象类
package com.crj.principle.t3;import com.crj.principle.t3.cpu.CPU;
import com.crj.principle.t3.memory.Memory;public class Computer {private CPU cpu;private Memory memory;public Computer(CPU cpu, Memory memory) {this.cpu = cpu;this.memory = memory;}public Computer(){}public void startRun(){cpu.calculate();memory.storage();}
}
- 测试类测试
package com.crj.principle.t3;import com.crj.principle.t3.cpu.AmdCPU;
import com.crj.principle.t3.cpu.CPU;
import com.crj.principle.t3.memory.IMemory;
import com.crj.principle.t3.memory.Memory;public class Test {public static void main(String[] args) {CPU cpu = new AmdCPU();Memory memory = new IMemory();Computer com = new Computer(cpu,memory);com.startRun();}
}
主要看Computer类中我们执行startRun()方法时候并不需要知晓具体的产品(CPU、AMD)的类型,而是调用其接口类的类型方法即可实现。这降低了系统的耦合度,既符合依赖倒置原则也符合开闭原则
接口隔离原则 ISP
Interface Segregation Principle,接口最小粒度设计。客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。一个类实现一个接口,就必须实现这个接口的所有抽象方法。如果接口设计的过于庞大的话,实现类就被迫实现不需要的抽象方法。简单来说其实就是在定义接口时要考虑到实现类不应该被迫实现接口中不需要的方法!解决方法就是按照接口功能粒度最小去设计接口。
迪米特原则 LoD
Law of Demeter,迪米特法则来自于1987年美国东北大学的一个名为Demeterl的一个研究项目,只跟朋友联系,不跟“陌生人”说话。如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
合成复用原则CRP
Composite Reuse Principle,合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,优点如下:
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
- 对象间的耦合度低。可以在类的成员位置声明抽象抽象类或者接口)。
总结
梳理完成,有关软件设计的知识荔枝就梳理到这里,接下来荔枝将会继续深入中间件的学习了,后面自己再抽时间回来看看笔记吧。知识是需要复盘的,也是需要总结和分享的~希望能帮助到有需要的小伙伴嘿嘿。
今朝已然成为过去,明日依然向往未来!我是荔枝,在技术成长之路上与您相伴~~~
如果博文对您有帮助的话,可以给荔枝一键三连嘿,您的支持和鼓励是荔枝最大的动力!
如果博文内容有误,也欢迎各位大佬在下方评论区批评指正!!!