六大设计原则
名称 | 概念 |
---|---|
开闭原则 | 对拓展开发,对修改关闭 |
里氏代换原则 | 任何基类出现的地方,子类一定可以出现 |
依赖倒转原则 | 针对接口编程,而不是实体类 |
接口隔离原则 | 单一责任原则,通过接口来降低耦合 |
迪米特法则 | 一个实体尽量少的与其他实体发生相互作用的关系 |
合成复用原则 | 尽量使用和合成/聚合的方式,而不是使用继承 |
开闭原则
1. 个人理解
1. 开发原则是软件开发的终极目标,做好其他5中设计原则就能实现开闭原则
2. 开闭原则的核心是:抽象构建框架,用实现拓展细节
3. 可以理解为对基类继承,子类根据需求增加方法,而不必改变基类代码。
根据需求来拓展实现类,而不改变接口的代码。
开闭原则要懂得对哪些进行变化(子类,实现类),哪些是不变的(接口,基类)。
2. 代码示例
2.1 如何使用开闭原则
1. 抽象约束
2. 封装变化
~~~~
将相同的变化封装到一个接口或抽象类中,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同变化出现在同一个接口或抽象类中。
2.2 实现代码
//鸟接口
public interface Bird {
public void fly();
public void call();
}
/红鸟
public class RedBird implements Bird {
public void fly() {
System.out.println("我会飞!");
}
public void call() {
System.out.println("嘎嘎叫!");
}
}
//鸟有了新的本领-游泳,开闭原则:不能更改鸟接口的代码,在鸟接口的实现类进行拓展开发
public class EnhanceBird implements Bird {
//构造函数
public EnhanceBird(){
super();
}
public void fly() {
System.out.println("我会飞!");
}
public void call() {
System.out.println("叽叽的叫!");
}
public void swim(){
System.out.println("我还会游泳!");
}
}
//测试,查看结果
public class Test {
public static void main(String[] args) {
System.out.println("我是原始版的鸟!");
Bird bird=new RedBird();
bird.fly();
bird.call();
System.out.println("我是加强版的鸟,加入了新的本领!!!");
EnhanceBird enhanceBird1 =new EnhanceBird();
enhanceBird1.call();
enhanceBird1.fly();
enhanceBird1.swim();
}
}
//结果如下
我是原始版的鸟!
我会飞!
嘎嘎叫!
···············································
我是加强版的鸟,加入了新的本领!!!
叽叽叫!
我会飞!
我还会游泳!
3. 总结
- 上面这段代码是我对开闭原则的理解,就是不管接口设计的是是否很完善,当有新的需求时都不能改变接口的代码。上面代码中只有RedBird实现Bird接口,但是在实际开发中一个接口可能被几十上百个类实现,改接口这些类都要进行改变。
- 因此对于要增加就在接口的实现类中进行拓展。这样只需要改一个类就行了,并且以后的类需要这个拓展(swim)时,直接继承增强类就可以了。
- 其实也可以定义一个接口,接口继承Bird接口,在该接口中添加swim()方法。以后的实现类都实现该新定义的接口就行。
- 该代码示例是有缺陷的,在抽象约束这一点上并没做好,导致之后要增加swim()方法,该方法应该是写在Bird的。正真的拓展,是对接口中的方法进行不同的实现。如代码例子中,鸟的叫声有几种,“叽叽叫"和"嘎嘎叫”。这才是"封装变化"的正确理解
2. 里氏代换原则
1. 个人理解
- 该设计原则是使用继承的基础,什么时候使用继承,什么时候不能使用继承。
- 继承必须确保超类所拥有的性质在子类中仍然成立
1.1 里氏替换原则的作用
- 里氏替换原则是实现开闭原则的重要方式之一。
- 它克服了继承中重写父类造成的可复用性变差的缺点。
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
1.2 里氏替换原则的实现方法
- 子类继承父类时尽量不重写父类的方法,因为重写会破坏整个继承结构。
- 如果必须要重写父类方法,那么就要破坏原来的继承关系,重新设计新的继承关系,满足子类不重写父类方法来满足要求。
2. 代码示例
//父类-鸟
public class Bird {
public void fly(){
System.out.println("我会飞!");
}
public void call(){
System.out.println("我会嘎嘎叫!");
}
}
//鸟的子类-黑鸟
public class BlackBird extends Bird {
}
//鸟的子类-红鸟~~~~
public class RedBird extends Bird {
@Override
public void call() {
System.out.println("我呀叽叽叫!");
}
}
//运行结果
黑鸟的叫声!~~~~~~~~~~~~~~~~~~~
我会叽叽叫!
红鸟的叫声!~~~~~~~~~~~~~~~~~~~
我会嘎嘎叫!
由于红鸟不会"叽叽叫",所以重写了父类call()方法,这样就破坏了里氏替换原则
重新设计继承关系
//animal
public class animal {
public void fly(){
System.out.println("我会飞");
}
}
//Bird继承animal
public class Bird extends animal {
public void call(){
System.out.println("我会嘎嘎叫!");
}
}
//黑鸟继承Bird
public class BlackBird extends Bird {
}
//红鸟继承animal
public class RedBird extends animal {
public void call(){
System.out.println("我会叽叽叫!!");
}
}
public class liskovTest {
public static void main(String[] args) {
RedBird redBird=new RedBird();
Bird blackBird=new BlackBird();
System.out.println("黑鸟的叫声!~~~~~~~~~~~~~~~~~~~");
blackBird.call();
System.out.println("红鸟的叫声!~~~~~~~~~~~~~~~~~~~");
redBird.call();
}
}
//结果
黑鸟的叫声!~~~~~~~~~~~~~~~~~~~
我会嘎嘎叫!
黑鸟的叫声!~~~~~~~~~~~~~~~~~~~
我会叽叽叫!!
3. 总结
- 这个代码例子有不恰当之处,不具现实性。在这个例子中,红鸟已经不是鸟的范畴了。
- 我所表达思想已经到位了,只要子类重写父类方法,就要重新设计继承,还要进行进一步的抽象。
3. 依赖倒转原则
1. 个人理解
- 就是面向接口编程,而不是具体实现类
- 将能够抽象定义的都写在接口中,而不是实现类中。
- 其实就是实现类实现接口,在定义变量是通过向上转型将实现类对象赋值给接口对象,在进行操作时时对接口对象进行操作,而不是具体的实现类。
1.1 依赖倒转原则的作用
- 依赖倒置原则可以降低类间的耦合性。
- 依赖倒置原则可以提高系统的稳定性。
- 依赖倒置原则可以减少并行开发引起的风险。
- 依赖倒置原则可以提高代码的可读性和可维护性。
1.2 依赖倒转的实现
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
2. 代码示例
//鸟接口
public interface Bird {
public void fly();
public void call();
public void run();
}
//红鸟-实现鸟接口
public class RedBird implements Bird {
public void fly() {
System.out.println("我是红鸟,我会高空飞翔!!");
}
public void call() {
System.out.println("我是红鸟,我会叽叽叫!");
}
public void run() {
System.out.println("我是红鸟,我会慢跑!");
}
}
//黑鸟-实现鸟接口
public class BlackBird implements Bird {
public void fly() {
System.out.println("我是黑鸟,我会低空飞翔!");
}
public void call() {
System.out.println("我是黑鸟,我会嘎嘎叫!");
}
public void run() {
System.out.println("我是黑鸟,我会快速跑!");
}
//创建鸟对象,红鸟和黑鸟通过向上转型给接口
public class InverseDependentTest {
public static void main(String[] args) {
Bird bird= new RedBird();
Bird bird1= new BlackBird();
System.out.println("红鸟 ~~~~~~~~~~~~~~~~~~~~~~~");
bird.call();
bird.fly();
bird.run();
System.out.println("黑鸟 ~~~~~~~~~~~~~~~~~~~~~~~");
bird1.call();
bird1.fly();
bird1.run();
}
}
//
在执行过程中通过动态绑定,来确定执行哪个实现类的方法。
//执行结果
红鸟 ~~~~~~~~~~~~~~~~~~~~~~~
我是红鸟,我会叽叽叫!
我是红鸟,我会高空飞翔!!
我是红鸟,我会慢跑!
黑鸟 ~~~~~~~~~~~~~~~~~~~~~~~
我是黑鸟,我会嘎嘎叫!
我是黑鸟,我会低空飞翔!
我是黑鸟,我会快速跑!
3. 总结
-
面像接口编程,定义好接口之后,具体的细节留给实现来完成。
-
代码中对接口进行操作,这样系统更加稳定,因为接口很稳定。然而实现类时多样的,其存在的不稳定性大于接口。
4. 接口隔离原则
1. 个人理解
- 一个类对另一个类的依赖应该建立在最小的接口上
- 如果接口太臃肿,使接口发生变化的因素就会有很多,接口时常发生变化,那么依赖该接口的类也要随之变化。这样不能时耦合度增加
- 只能有一个因素造成接口的变化,这样时接口内达到高内聚,与外部达到低耦合。
- Java语言中,一个类可以实现多个接口,这样就可以将一个大的接口拆分成多个小的接口。实现类再实现这些接口
2. 代码示例
//商品接口
public interface Goods {
public void getGoodName();
public void getStock();
}
//物流接口
public interface Logistics {
//物流接口
public void getLogisticsConpany();
}
//订单实现类
public class Order implements Goods, Logistics {
public void getGoodName() {
System.out.println("得到商品名称。");
}
public void getStock() {
System.out.println("得到商品库存。");
}
public void getLogisticsConpany() {
System.out.println("得到物流公司。");
}
}
//
public class oneInterfaceTest {
public static void main(String[] args) {
Goods goods= new Order();
Logistics logistics=new Order();
goods.getGoodName();
goods.getStock();
System.out.println("~~~~~~~~~~~~~~~");
logistics.getLogisticsConpany();
}
}
//结果
~~~~~~商品接口~~~~~~~~~
得到商品名称。
得到商品库存。
~~~~~~~物流接口~~~~~~~~
得到物流公司。
- 在订单实现类,之前的做法是将商品和物流都方法一个订单接口中,通过接口拆分,将订单接口拆分成商品接口和物流接口。
3. 总结
-
接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
-
为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。(从上面代码可以看出,当调用商品接口方法是,会屏蔽实现类中的物流相关的方法)
-
提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。(这里提高内聚是指,将该接口不需要的方法提到接口之外,这样接口内都是方法都是该接口所需要的,提高了内聚。)
5. 迪米特法则
1. 个人理解
- 最少知识原则
- 只与你的直接朋友交谈,不跟“陌生人”说话
- 这个设计原则还有点抽象,就是如果不是直接能对话的话,就需要一个中间类来传递双方的信息。
2. 代码示例
老师通过学生联系家长 - 老师和家长就可以用迪米特法则
//老师类
public class MyTeacher {
private String name;
public MyTeacher(String name) {
this.name=name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
父母类
public class MyParent {
private String name;
public MyParent(String name){
this.name=name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
//学生类
public class Student {
private MyParent myParent;
private MyTeacher myTeacher;
public MyParent getMyParent() {
return myParent;
}
//叫家长
public void callParent(){
System.out.println(myTeacher.getName()+"叫小明通知"+myParent.getName()+"来学校");
}
public void setMyParent(MyParent myParent) {
this.myParent = myParent;
}
public MyTeacher getMyTeacher() {
return myTeacher;
}
public void setMyTeacher(MyTeacher myTeacher) {
this.myTeacher = myTeacher;
}
}
//测试类
public class LckRuleTest {
public static void main(String[] args) {
Student student=new Student();
student.setMyParent(new MyParent("小明的爸爸"));
student.setMyTeacher(new MyTeacher("小明的老师"));
student.callParent();
}
}
//结果
小明的老师叫小明通知小明的爸爸来学校
3. 总结
-
使用迪米特原则时要更具具体情况而定,因为该设计原则会产生中间类,如果中间类过多,会导至系统很复杂。
-
其实迪米特原则可以和spring 的IOC进行类比,ioc相当于中间类,将对象之间的关系交给中间类来管理。目的和ioc容器一样都是为了减低对象之间的耦合关系。
6. 合成复用原则(组合/聚合复用原则)
1. 个人理解
- 合成复用和继承时对代码实现复用两种策略,但相比之下合成复用更加合理一下。
- 继承复用,父类和子类的耦合度很高。父类发生变化,子类也会发生变化。继承复用简称“白盒”复用
- 合成复用依然保持了类的封装性,复合复用简称"黑盒"复用。
2. 代码示例
-
用继承来实现
-
用复合来实现
-以上两张图借鉴网站如下:
借鉴网站
3. 总结
- 复合复用是我用的很少的设计原则
- 在好几本书上都建议,少用继承,多用复合(这足以体现复合复用的重要性)