设计模式概念:
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
当有人问起面向对象设计模式的时候,我们脑海里多少能浮现几个设计模式的用法,例如什么单例、工厂等。但很多时候我们在代码中使用了所谓的设计模式我们自己都不知道。因为这些设计模式已经成为我们编程经验的一部分,只是我们一直对他们知其然而不知其所以然。为什么要使用这些所谓设计模式的范式规则进行编程,我们是一知半解的,我自己就是这样的感觉。
所以打算对设计模式实操记录一下。首先设计模式还是蛮多的,普遍的认识是有二十一种常用的,那么他们有没有基于什么基本的原则呢,是有的,基本的原则如下:
- 单一职责原则
- 里氏替换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特法则
- 开放封闭原则
一个一个原则具体学习理解(其中有很多内容是参考大神们的思考):
1. 单一职责原则
一个类的修改只能有一个被修改的原因
通俗地讲,就是一个类只能负责一个职责,修改一个类不能影响到别的功能,也就是说只有一个导致该类被修改的原因。我们写代码的都知道尽量要做到低耦合、高内聚的特性,单一职责原则正是保证了类与类之间的低耦合性。一个类如果承担过多的职责,就会有很多原因来导致这个类的被修改,就有很大可能性影响到别的功能。
单一职责原则,看起来是一个非常简单的原则,但真正实践起来也并非易事,因为职责的联合在实际当中是经常遇到的事,也不能随便地去拆分类去适配单一职责模式,所以如何从这些联合的职责中合理地把职责分隔出来更合适的遵守单一职责原则要好好考虑。
看看下面这这个接口是否符合单一职责原则呢?
public interface UserInterface{
void saveUser(User user); // 保存用户信息
User getUser(long id); // 获取用户信息
void makeMoney(User user);// 赚钱
void doHousework(User user) ; // 做家务
}
上面这个类使用户的接口,定义了保存和获取用户的方法,还包括用户赚钱和做家务的接口,虽然用户的赚钱和做家务和用户有关,但是很明显他们不是同一个层次的信息。
这样的在修改用户信息的的时候需要修改到赚钱,做家务等内容,这样就不太符合单一原则。最好是重新创建一个新的接口:
public interface UserDoSomeInterface{
void makeMoney(User user);// 赚钱
void doHousework(User user) ; // 做家务
}
如果有需要的避免耦合可以把赚钱和做家务再分开。
以上的设计的拆分的粒度还是要根据具体的程序设计需要进行具体的处理,这个就是架构设计的经验和能力问题,但类的单一职责的保证还是要尽量遵守,避免随着代码的增多带来不必要的耦合问题,限制程序的灵活性。
2. 里氏替换原则
只要有父类出现的地方,都可以用子类来替代,程序不会出现什么变化。但是反过来则不行,有子类出现的地方,不能用其父类替代
四层含义
里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
下面就一个一个来详细解析上面的含义:
子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法
以下的这个例子展示的就是符合里氏替换原则的例子:
public class A {
public void fun0(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
public abstract void fun1(); // 抽象方法
}
public class B extends A{
@Override
public void fun0(int a,int b){
System.out.println(a+"-"+b+"="+(a-b));
}
public void fun1(){
System.out.println(“实现抽象方法!”);
} // 实现父类的抽象方法
}
public class demo {
public static void main(String[] args){
System.out.println("父类的运行结果");
A a=new A();
a.fun(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.println("子类替代父类后的运行结果");
B b=new B();
b.fun(1,2);
}
}
运行的结果:
父类的运行结果 1+2=3
子类替代父类后的运行结果 1-2=-1
这样就会让子类替换父类之后结果错乱,违背了第一条。
子类中可以增加自己特有的方法
这一条理解起来很简单就不做过多解释了。
当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
下面列举的是一个反例, 子类B的fun的参数是HaseMap,父类A中的fun方法的参数是Map,HaseMap是Map的子类,是更受限制的。所以在Map为参数的地方,HaseMap不一定能用。
import java.util.Map;
public class A {
public void fun(Map map){
System.out.println("父类被执行...");
}
}
import java.util.HashMap;
public class B extends A{
public void fun(HashMap map){
System.out.println("子类被执行...");
}
}
import java.util.HashMap;
public class demo {
static void main(String[] args){
System.out.print("父类的运行结果:");
A a=newa3C/span> A();
HashMap map=new HashMap();
a.fun(map);
System.out.print("子类替代父类后的运行结果:");
B b=new B();
b.fun(map);
}
}
输出的结果:
父类的运行结果:父类被执行…
子类替代父类后的运行结果:子类被执行…
这样就在子类重载了父类的方法时,传入同样的参数,调用了父类和子类不同的方法。容易造成一些程序上的问题。 所以最好是确保在重载父类方法的时候,传入的参数范围比父类的更宽泛。
当子类的方法实现父类的(抽象)方法时,方法的后置条件(即方法的返回值)要比父类更严格
这一条举一个正常的例子:
import java.util.Map;
public abstract class A {
public abstract Map fun();
}
import java.util.HashMap;
public class B extends A{
@Override
public HashMap fun(){
HashMap b=new HashMap();
b.put("b","子类被执行...");
return b;
}
}
import java.util.HashMap;
public class demo {
public static void main(String[] args){
A a=new B();
System.out.println(a.fun());
}
}
这条原则主要是由于代码编译的限制,当子类在实现父类方法或者重写(不是重载)的时候,返回值不能比父类规定的返回值更宽泛,编译器会发出警告!
3.依赖倒转原则
依赖倒置原则在java语言中,表现是:
1.模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的
2.接口或抽象类尽量不依赖实现类
3.实现类尽量依赖接口或抽象类
更直白的说法就是我们尽量基于接口和抽象编程。
优快云上有很多大神写的的很通俗易懂并很详细,看到一篇,地址如下,可以参考学习:
https://blog.youkuaiyun.com/u011857683/article/details/80287274?tdsourcetag=s_pctim_aiomsg
4.开闭原则
有大神说开闭原则是编程的底层武功,是属于内功范畴的。是程序的扩展性和稳定性的基:
核心:对扩展开放、对修改关闭,即尽量在不修改原有代码的基础上进行扩展。要想系统满足“开闭原则”,需要对系统进行抽象。
方法:通过 接口 或 抽象类 将系统进行抽象化设计,然后通过实现类对系统进行扩展。当有新需求需要修改系统行为,简单的通过增加新的实现类,就能实现扩展业务,达到在不修改已有代码的基础上扩展系统功能这一目标。
其实遵循了上面的那个依赖倒置的原则,依赖接口和抽象进行编程的时候,开闭原则就基本没什么问题了。
下面以一个餐馆来举例:
餐馆有一张菜单,每道菜有名字和价格和显示的图片。
定义一个菜单的接口如下
public interface MenuBean {
public String getFoodName();// 菜的名字
public String getFoodPrice();//菜的价格
public String getFoodPic();// 菜的展示图
}
实现这个菜单接口,定义一道鸡蛋的菜:
public class FoodEgg implements MenuBean{
private String foodName;
private String foodPrice;
private String foodPic;
public FoodEgg(String foodName, String foodPrice,String foodPic){
this.foodName = foodName;
this.foodPrice = foodPrice;
this.foodPic = foodPic;
}
@Override
public String getFoodName() {
return foodName;
}
@Override
public String getFoodPrice() {
return foodPrice;
}
@Override
public String getFoodPic() {
return foodPic;
}
}
一个顾客点了一道鸡蛋的菜,西红柿炒蛋, 有价格、有图片。
public class Client{
public static void main(Strings[] args){
MenuBean eggFood= new Eggfood("西红柿炒蛋",“20”,"大图");
System.out.println("菜的名字:"+eggFood.getFoodName()+"菜的价格:"+eggFood.getFoodPrice()+“菜的展示图”:"+eggFood.getFoodPic());
}
}
新建了很多的菜品,这个菜单一切妥当,打印出来,供顾客选择。
突然有一天老板想做一些打折的特价菜,具体那些菜是打折菜每天还是变动的。这个需求怎么解决?
-
首先想到的是直接修改MenuBean这个接口,添加一个打折的抽象方法,然后就会发现这样的修改,所有继承了该接口的菜品类都需要修改,去实现该打折的方法。这样就需要修改所有的菜品类。是比较废了和存在风险的。
-
直接修改实现类getFoodPrice,由于打折的菜每天是变动的,或者说打折的力度也是变化的,所以这就会造成频繁的修改。
修改接口和实现类都是不恰当的方法。增加一个打折的接口,然后针对要打折的菜再具体去实现。这样对之前实现的类就不需要改动,只是做扩展,封闭了老的方法,开放了实现新的需求。
5.接口隔离原则
接口的隔离原则就是想尽量细粒度接口的功能,把接口的功能进行聚合,减少功能的耦合和接口的臃肿,这个和第一条类的单一功能原则差不多。再基于接口编程的过程中,如果进行了必要的接口隔离,保持了接口的细粒度和清洁性基本上相对的保证了类的单一原则性。
这一条相对好理解也好执行,下面的链接的作者就用了一个星探发掘美女的例子很好的说明了这个原则,供参考学习理解:
https://blog.youkuaiyun.com/hfreeman2008/article/details/52304172
6.迪米特原则
迪米特原则,也叫最少知识原则:
一个对象应该对其他对象有最少的了解。
通俗的讲:一个类对自己需要耦合或调用的类知道的最少,你(被耦合或调用的类)的内部是如何复杂和我没有关系,我就知道你提供的public方法,我只调用这些方法,其它的我不关心。
大神们解释的很清楚:就收藏记录下地址,方便回顾学习:
https://blog.youkuaiyun.com/hfreeman2008/article/details/52335601
总结: 基于以上的一些原则,说到底就是让代码结构更清晰,更容易扩展,让后根据这些原则,结合语言的特性,就演化出了三七二十一招(二十一种设计模式)。后面对一些典型的设计模式进行学习总结记录。