视频:
https://www.bilibili.com/video/BV1vE411E7ZQ/
文章:
https://www.cnblogs.com/pony1223/p/7594803.html
https://www.cnblogs.com/wyq178/p/8284107.html
5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已;
单一职责原则告诉我们实现类要职责单一;
里氏替换原则告诉我们不要破坏继承体系;
依赖倒置原则告诉我们要面向接口编程;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合;
而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
类的设计一个是行为,一个是属性,使用者可以抽象出针对统一属性的行为操作,由具体行为执行者去执行。
1. 单一职责原则
1.1. 定义
不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责
1.2. 问题由来
类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障
薛定谔的猫理论,用户需求不确定理论
1.3. 解决方案
遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险
同理,当修改T2时,也不会使职责P1发生故障风险。
1.4. 注
在职责扩散到我们无法控制的程度之前,立刻对代码进行重构(P1可能细分为 P11、P12、P13).
//针对一个类的设计 一个类只负责一项职责
public class A单一职责原则 {
public static void main(String[] args) {
Controller c = new Controller();
c.showView("首页");
}
}
//下面的类各司其职
//视图控制
class Controller{
public String showView(String pageName){
Server server = new Server();
System.out.println("将获取的 '" + server.showView("页面数据") + "' 返回给" + pageName);
return pageName;
}
}
//业务逻辑实现
class Server{
public String showView(String pageDate){
System.out.println("获取" + pageDate);
Dao dao = new Dao();
return dao.showView(new Entity());
}
}
//数据库操作
class Dao{
public String showView(Entity en){
en.uName = "用户名";
return en.uName;
}
}
//实体
class Entity{
public String uName;
}
2. 里氏替换原则
2.1. 定义
定义1
如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,
程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型
定义2
所有引用基类的地方必须能透明地使用其子类的对象,也就是子类可以扩展父类的功能,但不能改变父类原有的功能
2.2. 问题由来
有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。
新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障
2.3. 解决方案
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,
尽量不要重写父类A的方法,也尽量不要重载父类A的方法。【有时候我们可以采用final的手段强制来遵循】
我们必须遵守的:(子类可以扩展父类的功能,但不能改变父类原有的功能)
子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
子类中可以增加自己特有的方法
当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
2.4. 注
继承包含这样一层含义:
父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约
但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义
public class B里氏替换原则 {
public static void main(String[] args) {
数据相减 ct1 = new 数据相减();
System.out.println(ct1.subtract(7, 12));
数据相乘 ct2 = new 数据相乘();
System.out.println(ct2.multiplicationAB(7, 12));
System.out.println(ct1.addAB(7, 12));//不影响父类的功能
System.out.println(ct2.addAB(7, 12));//不影响父类的功能
}
}
//父类 数据具体操作
class 数据相加{
public int addAB(int a, int b){
return a + b;
}
}
//子类 数据具体操作
class 数据相减 extends 数据相加{
public int subtract(int a, int b){
return a - b;
}
}
//子类 数据具体操作
//类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法
class 数据相乘 extends 数据相加{
//将父类的方法重写了 在这里不能影响父类的功能
// 如果重写了 对于其他的继承子类就不透明了,大家都以为父类原来是相加 但是在这里变成相乘了
// 所有引用基类的地方必须能透明地使用其子类的对象
//public int addAB(int a, int b){
// return a * b;
//}
public int multiplicationAB(int a, int b){
return a * b;
}
}
3. 依赖倒置原则
3.1. 定义
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
也就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
3.2. 问题由来
类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;
类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险
3.3. 解决方案
将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率
3.4. 注
低层模块尽量都要有抽象类或接口,或者两者都有。【可能会被人用到的】
变量的声明类型尽量是抽象类或接口。
使用继承时遵循里氏替换原则。
依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置
类的设计一个是行为,一个是属性,使用者可以抽象出针对统一属性的行为操作来使用属性。
使用者 ---- 抽象行为(阅读,具体阅读什么不知道,只关注行为)----【具体行为执行者读书,读报纸】
public class C依赖倒置原则 {
public static void main(String[] args) {
new Children().readSomething(new Book());
new Children().readSomething(new Newspaper());
}
}
//抽象行为
interface IReader{
void readContent();
}
//具体行为执行者
class Book implements IReader{
@Override
public void readContent() {
System.out.println("从前有座山");
}
}
//具体行为执行者
class Newspaper implements IReader{
@Override
public void readContent() {
System.out.println("号外号外");
}
}
//调用者 依赖于抽象
class Children{
void readSomething(IReader iReader){
iReader.readContent();
}
}
4. 接口隔离原则
4.1. 定义
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
4.2. 问题由来
类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法
4.3. 解决方案
将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则
4.4. 注
建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大 的接口供所有依赖它的类去调用
在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散, 提高系统的灵活性和可维护性
```
单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离
单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节
而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建
```
调用者—>抽象操作行为<—操作者—具体操作行为----操作行为执行对象
//将针对一个操作者的操作行为定义在一个单一的接口中
//多个不同操作者可以共同相同的操作行为
//具体操作者需要哪个操作行为就去实现哪个操作行为,不需要的不实现
//调用者
public class D接口隔离原则 {
public static void main(String[] args) {
B b = new B();
D d = new D();
A aa = new A();
aa.depend1(b);
aa.depend1(d);
aa.depend2(b);
aa.depend3(b);
C cc = new C();
cc.depend1(b);
cc.depend2(d);
cc.depend3(d);
}
}
//抽象操作行为1
interface I1 {
void method1();
}
//抽象操作行为2
interface I2 {
void method2();
void method3();
}
//抽象操作行为3
interface I3 {
void method4();
void method5();
}
//操作者A
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
//操作者C
class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
//具体操作行为B
class B implements I1, I2{
public void method1() {
System.out.println("类B实现接口I1的方法1");
}
public void method2() {
System.out.println("类B实现接口I2的方法2");
}
public void method3() {
System.out.println("类B实现接口I2的方法3");
}
}
//具体操作行为D
class D implements I1, I3{
public void method1() {
System.out.println("类D实现接口I1的方法1");
}
public void method4() {
System.out.println("类D实现接口I3的方法4");
}
public void method5() {
System.out.println("类D实现接口I3的方法5");
}
}
5.迪米特法则(最少知道原则)
5.1. 定义
一个对象应该对其他对象保持最少的了解(一个类应该尽量的封装自己,与自己的朋友类打交道,一般朋友类都是成员变量和参数,非朋友类是局部参数)
只与直接的朋友产生关系,不要和陌生人产生关系(出现在方法体内部的类就不是直接的朋友,出现在成员变量、方法的输入输出参数中的类就是直接的朋友)
一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立
一个模块修改时,尽量不的影响其他模块,扩展相对容易,是对软件实体通信的限制,它要求限制软件实体之间通信的宽度和深度尽可能的少
5.2. 问题由来
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大
5.3. 解决方案
尽量降低类与类之间的耦合(软件开发尽量遵循低耦合,高内聚。耦合的方式很多,依赖、关联、组合、聚合等)
5.4. 注
自己本类的成员变量尽量设置为private的,降低外界的访问权限
自己本类的方法尽量为private的,只暴露业务逻辑上的接口方法,而不是技术细节内部的方法
如果朋友类有间接的引用,这时候应该隐藏间接引用,不从中间暴露。我们可以新增一个中间类来管理所有的需要类
一个人应尽可能的完善自己,尽量减少与其他人联系。“你若盛开,蝴蝶自来”。盛开可以理解为“内聚”性。让自己的能力聚集起来,绽放出最美丽的自己
public class E迪米特法则 {
public static void main(String[] args) {
Form1 f1 = new Form1();
f1.ss();
Form2 f2 = new Form2();
f2.ss();
}
}
class Form1{
//成员变量 直接的朋友
private Control1 c1 = new Control1();
public void ss(){
c1.getData();
}
}
class Form2{
Control2 c2 = new Control2();
public void ss(){
c2.getData2();
c2.getData1();
}
}
class Control1{
//Dao1 对Form1来说使陌生人
private Dao1 d1 = new Dao1();
private void ccc(){ //减小不必要的暴漏
System.out.println("C1");
}
void getData(){
this.ccc();
d1.showMsg();
}
}
class Control2{
private Dao2 d2 = new Dao2();
void getData2(){
d2.showMsg();
}
//Form2要和Dao1产生关系可以依赖Control2(中介) 他两没有必要直接从陌生人产生关系
private Dao1 d1 = new Dao1();
void getData1(){
d1.showMsg();
}
}
class Dao1{
void showMsg(){
System.out.println("D1");
}
}
class Dao2{
void showMsg(){
System.out.println("D2");
}
}
6. 合成复用原则
6.1. 定义
尽量使用对象组合,而不使用继承来达到复用目的
6.2. 问题由来
有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及复杂度,因此需要慎重使用继承复用
6.3. 解决方案
通过关联关系来使用一些已有的对象,使之成为新对象的一部分,尽量使用组合/聚合关系,少用继承。
6.4. 注
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,以为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见
的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不改变,从基类继承而来的实现是静态的,不可能在运行时发生改变,没有
足够的灵活性,而且继承只能在有限的环境中使用
public class F合成复用原则 {
public static void main(String[] args) {
B1 b1 = new B1();
b1.SetA(new A1());
b1.callFun1();
C1 c1 = new C1();
c1.callFun2();
}
}
class A1{
void fun1(){
System.out.println("fun1");
}
void fun2(){
System.out.println("fun2");
}
}
//B1需要调用A1的两个方法 但是不要使用 B1:A1 这种继承关系
class B1{
//聚合
private A1 a;
public void SetA(A1 a){
this.a = a;
}
public void callFun1(){
this.a.fun1();
}
}
//C1也不继承A1
class C1{
//合成
private A1 a1 = new A1();
public void callFun2(){
a1.fun2();
}
}
7. 开闭原则
7.1. 定义
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭, 也就是在不修改代码的情况下改变程序的行为
7.2. 问题由来
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误
也可能会使我们不得不对整个功能进行重 构,并且需要原有代码经过重新测试
7.3. 解决方案
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
a. 通过接口或者抽象类约束扩展,对扩展进行边界限定,不存在的public方法不允许出现在接口或抽象类中
b. 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类
c. 抽象层尽量保持稳定,一旦确定即不允许修改
//对修改关闭 对扩展开放
//抽象是稳定的 可靠的 不被轻易改变的
//针对一个由多个类组成的结构或者是一个软件模块或者是一个独立的类
//抽象(接口或者抽象类)是开闭原则的关键 也就是对 可变性进行封装
public class G开闭原则 {
public static void main(String[] args) {
形状 xz = new 圆形();
System.out.println("需要的类型:" + xz.showMsg());
xz = new 三角形();
System.out.println("需要的类型:" + xz.showMsg());
xz = new 正方形();
System.out.println("需要的类型:" + xz.showMsg());
}
}
// 需要 多个形状时 不需要修改 形状类 而是 创建新的形状类来继承形状类
abstract class 形状{
//形状可能变动 将其抽象 只有在程序运行时才能确定具体是那种形状
abstract String showMsg();
}
class 圆形 extends 形状{
public String showMsg(){
return "圆形";
}
}
//扩展的 三角形 正方形
class 三角形 extends 形状{
public String showMsg(){
return "三角形";
}
}
class 正方形 extends 形状{
public String showMsg(){
return "正方形";
}
}