JAVA设计模式(一)面向对象设计原则
在进行软件开发时,不仅仅需要将最基本的业务给完成,还要考虑整个项目的可维护性和可复用性,我们开发的项目不单单需要我们自己来维护,同时也需要其他的开发者一起来进行共同维护,因此我们在编写代码时,应该尽可能的规范。
因此在进行代码开发设计时,应当遵循一定的设计原则,基于这些设计原则,衍生出了很多种常用的设计模式。因此,这一节对常用的设计原则进行一个总结。
单一职责原则
单一职责原则(Simple Responsibility Pinciple,SRP)是最简单的面向对象设计原则,它用于控制类的粒度大小。
单一职责,在开发中是最不要钱的优秀的编码手段,它指的是引起类变化的原因,一个类只能有一个。有什么好处?当一个类职责越多,被引用的地方就越多,依赖关系就越复杂,同样可能引起类变化的原因就越多,这样的类就会越脆弱。
例如:定义一个消息中心,可以发送邮件,发送手机短信和微信推送三种方式,有如下代码:
public interface MessageCenter {
// 发送邮件
void sendByEmail();
// 发送手机短信
void sendByPhone();
// 微信推送
void sendByWeChat();
}
上面的代码违反了单一职责原则,对于一个类或者接口,原则上只需要完成单一的职责,为了解决这个问题,代码写成如下的方式:
1)定义发送消息接口,只定义消息发送功能
public interface MessageCenter {
// 发送消息
void sendMessage();
}
2)具体的实现类实现单一功能
public class EmailSender implements MessageCenter{
@Override
public void sendMessage() {
System.out.println("发送邮件...");
}
}
再举一个例子,对于一个类Computer
,可以实现如下的功能:
public class Computer {
public void open(){
System.out.println("正在开机中...");
}
public void calculate(){
System.out.println("正在执行计算任务");
}
public void shutdown(){
System.out.println("正在关机中...");
}
public void video(){
System.out.println("播放视频...");
}
}
看起来设计的没有问题,但是却是违背了单一职责要求。其中开机、关机属于单一的职责,计算属于单一职责,播放视频同样也是一个职责。单一职责指一个接口或者类只有一个原因引起变化,以上显然不属于。进行修改后如下:
1)定义Calculate接口 实现计算功能
public interface Calculate {
void add();
void sub();
}
2)定义LifeCircle接口,实现生命周期管理
public interface LifeCircle {
void open();
void init();
void destroy();
void close();
}
3) 实现这两个接口
public class Computer implements Calculate, LifeCircle{
@Override
public void add() {
}
@Override
public void sub() {
}
@Override
public void open() {
}
@Override
public void init() {
}
@Override
public void destroy() {
}
@Override
public void close() {
}
}
虽然一个接口中有多个接口方法,但是做的事情是一样的,可以理解成只有一个原因引起变化。
这样的设计才是完美的,一个类实现两个接口,把两个职责融合在一个类中。这样看起来Computer类有两个原因引起了变化。是的,但是别忘记了,Java是面向接口编程,对外公布接口而不是实现类。这一原则在很多源码中都有所体现。此外,Computer类可以继续实现多个接口对功能进行扩展,但是依然满足单依职责原则。并且通常一个类不会实现很多接口,但是由于Java接口可以继承接口,会使用接口继承的方式实现各个功能。
开闭原则
软件实体应当对扩展开放,对修改关闭。
在面向对象领域中,开闭原则规定软件中的对象、类、模块和函数对扩展应该是开放的,但对于修改是封闭的。这意味着应该用抽象定义结构,用具体实现扩展细节,以此确保软件系统的开发和维护过程的稳定性。开闭原则也可以理解为面向抽象编程。
当功能需要变化的时候,我们应该是通过扩展的方式实现,而不是通过修改已有的代码来实现。随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。
例如:在交易场景中,常常需要定义多种支付方式。现在有两种支付方式,分别是微信和支付宝,各自定义了支付的细节。
public class AliPay {
void transferMoney(){
System.out.println("扣除手续费");
System.out.println("交易中...");
System.out.println("微信交易完成...");
}
}
public class WeChatPay {
void transferMoney(){
System.out.println("扣除手续费");
System.out.println("交易中...");
System.out.println("微信交易完成...");
}
}
public class TransferService {
public void transfer(Integer type){
// 微信支付
if (type == 1){
WeChatPay weChatPay = new WeChatPay();
weChatPay.transferMoney();
}
// 支付宝
else if (type == 2){
AliPay aliPay = new AliPay();
aliPay.transferMoney();
}
}
}
这样的代码有典型的问题,当新增一个新的支付方式时,不得不修改TransferService中的transfer方法。而开闭原则的核心就是把这种硬编码转换成抽象的类或者接口。我们可以定义一个IPay接口,接口中定义transferMoney方法,在转账时不再传入type而是接口。
public interface IPay {
void transferMoney();
}
public class AliPay implements IPay{
@Override
public void transferMoney() {
System.out.println("扣除手续费");
System.out.println("交易中...");
System.out.println("微信交易完成...");
}
}
public class WeChatPay implements IPay{
@Override
public void transferMoney(){
System.out.println("扣除手续费");
System.out.println("交易中...");
System.out.println("微信交易完成...");
}
}
public class TransferService {
public void transfer(IPay payType){
// 不使用硬编码 对扩展开放
payType.transferMoney();
}
}
这样就实现了对扩展开放,对修改关闭。另外,JDK8新特性中的default方法,允许在接口中提供一些方法的默认实现。同样很好的体现了这一思想,如果子类需要扩展只需要重写default方法而完全不影响其他实现该接口的类。
里氏替换原则
所有引用基类的地方必须能透明地使用其子类的对象。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
具体来说,有以下四点:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或与父类一样。
总的来说,通过重写父类的方法来完成新的功能写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
例如:定义一个Coder抽象类,实现了一个方法coding方法,并返回完成的代码行数。
public abstract class Coder {
public Integer coding(){
System.out.println("敲代码呢...");
System.out.println("敲了100行");
// 返回敲代码的行数
return 100;
}
}
public class CCoder extends Coder{
}
public class JavaCoder extends Coder{
}
public class TrashCoder extends Coder{
@Override
public Integer coding() {
return -100;
}
}
分别有三个类继承了Coder,但是有一个程序员比较垃圾,写不了代码,得靠别人帮忙写代码,因此重写了coding方法返回-100。这就违反了里氏替换原则。由于重写了父类的方法,在基类作为变量时候容易发生异常。
public static void main(String[] args) {
Coder coder = new TrashCoder();
Integer codingNum = coder.coding();
System.out.println("今日每小时代码行数: " + String.valueOf(codingNum / 8));
}
这就会出现一些奇怪的问题,最终的每小时代码量是负数。这里更合适的解决方法是TransCoder不要继承Coder,而继承更一般的如People,从而解决这样的问题。
说的再简单一些,就是尽量用基类作为变量接收,子类替换父类不会报错(因为没有改写父类逻辑)。如果一定要改写,那就抽出一个更一般的类,将这个方法作为抽象方法,取消继承父类转为继承这个更一般的类实现抽象方法。这样的思想在很多框架中被使用。
依赖倒转原则
高层模块不应依赖于底层模块,它们都应该依赖抽象。抽象不应依赖于细节,细节应该依赖于抽象。
依赖倒置原则主要有以下几点:
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转(倒置)的中心思想是面向接口编程
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
举个例子,我们有一个厨师类,擅长做面条:
public class Cooker {
public void cook(Noodles noodles){
noodles.component();
}
}
class Noodles {
public void component(){
System.out.println("面条, 油, 盐...");
}
}
public static void main(String[] args) {
Cooker cooker = new Cooker();
cooker.cook(new Noodles());
}
这种情况下,在后续扩展中如果Cooker需要做其他的菜,就需要进行大的改动。如果改成如下方式:
public interface Dish {
void component();
}
public interface ICook {
void cook(Dish dish);
}
public class Cooker implements ICook{
@Override
public void cook(Dish dish) {
dish.component();
}
}
对于Dish和Cooker都进行抽象,面向抽象编程。
public class DiSanxian implements Dish{
@Override
public void component() {
System.out.println("土豆, 茄子, 辣椒...");
}
}
// 这样Cooker可以更加方便的进行扩展,类与类之间也实现了解耦
public static void main(String[] args) {
Cooker2 cooker2 = new Cooker2();
cooker2.cook(new DiSanxian());
}
这样的方式在spring中经常使用到。并且依赖是可以传递的。A对象依赖B对象,B依赖C,C依赖D...,只要做到抽象依赖,即使是多层的依赖传递也无所畏惧。在spring中依赖的注入可以通过构造函数和setter方法注入,并且spring还实现了IOC(控制反转),使得对象的控制交由spring去管理,进一步降低了编写代码过程中耦合的程度,更利于大规模系统的开发。
接口隔离原则
客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
接口隔离原则可以定义为:建立单一接口,不要建立臃肿庞大的接口。也就是说,接口尽量细化,同时接口中的方法尽量少。
看到这,可能会与单一职责原则弄混,单一职责原则,强调的是职责,站在业务逻辑的角度;而接口隔离原则,强调接口的方法尽量少。
举个简单的例子,定义一个Student接口,可以输出学生基本信息。
public interface Student {
void printName();
void printAge();
void printChinese();
void printMath();
void printPhysics();
void printHistory();
}
有一个学生Tom实现了这个接口:
public class Tom implements Student{
@Override
public void printName() {
System.out.println("Tom...");
}
@Override
public void printAge() {
System.out.println("12...");
}
@Override
public void printChinese() {
System.out.println("Chinese...");
}
@Override
public void printMath() {
System.out.println("Math...");
}
@Override
public void printPhysics() {
System.out.println("Physics...");
}
@Override
public void printHistory() {
System.out.println("History...");
}
}
但是有一个新的学生是理科生,不学历史,因此printHistory方法不能实现,经过观察,为了提高接口的复用性,我们应用接口隔离原则对代码进行如下改进:
public interface Student {
void printName();
void printAge();
void printChinese();
void printMath();
}
public interface Science {
void printPhysics();
void printChemistry();
}
public interface Art {
void printHistory();
void printPolitics();
}
将接口拆分成Student、Art、Science,如果仅仅实现Student可以实现学生基本的信息,并根据程序中的需求实现多个不同的接口。
合成复用原则
优先使用对象组合,而不是通过继承来达到复用的目的。
一般而言,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”关系可使用继承。"Is-A"是严格的分类学意义上的定义,意思是一个类是另一个类的"一种";而"Has-A"则不同,它表示某一个角色具有某一项责任。
也就是说,当我们需要在当前类中实现一个在其他类中已经实现的功能,如果当前类与其他类是同一类(Is-a)的可以使用集成,这样不仅可以使用功能,并且通过继承还能得到这一类别的基本属性与方法。如果不是,通常使用对象组合的方式。因为如果仅仅为了实现这一个功能而通过继承的方式实现复用,将类B直接继承自类A,在后续维护的时候成本会成倍的增加,并且由于父类的属性和方法暴露给了子类,会造成一定的不安全性。
应用合成复用原则主要有以下几类情况:
- USE 动态之间临时的、动态的调用
- ASSOCIATION 长期的静态的联系
- COMPOSITION 将简单对象组合合并成更复杂的对象
- AGGREGATION 部分与整体的关系
通常第一种使用传参的方式,后三种通常使用构造函数注入或直接new的方式创建对象。例如在某一个模块中需要使用已经实现了的交易功能:
// 定义支付接口
public interface IPay {
void pay(Double money);
}
// IPay的一个实现类,以支付宝为例
public class AliPay implements IPay{
@Override
public void pay(Double money) {
System.out.println("成功支付了" + money + "元!");
}
}
如果在我们的系统中使用合成复用原则:
// 1.使用参数传递
public class MyClass1 {
public void transfer(IPay pay, Double money){
pay.pay(money);
}
public static void main(String[] args) {
MyClass1 myClass1 = new MyClass1();
myClass1.transfer(new AliPay(), 0.05);
}
}
// 2.使用构造函数
public class MyClass2 {
private IPay pay;
public MyClass2(IPay pay) {
this.pay = pay;
}
public void transfer(Double money){
pay.pay(money);
}
public static void main(String[] args) {
MyClass2 myClass2 = new MyClass2(new AliPay());
myClass2.transfer(0.1);
}
}
// 3.直接new
public class MyClass3 {
private IPay pay = new AliPay();
// public MyClass3(IPay pay) {
// this.pay = new AliPay();
// }
public void transfer(Double money){
pay.pay(money);
}
public static void main(String[] args) {
MyClass3 myClass3 = new MyClass3();
myClass3.transfer(0.1);
}
}
总之,合成复用原则的核心思想是继承不变功能,委托可变功能。
迪米特法则
每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
迪米特法则指出了一个对象应该对其他对象保持最少的了解,又叫最少知道原则,尽量降低类与类之间的耦合度。迪米特原则主要强调:只和朋友交流,不和陌生人说话。出现在成员变更、方法的输入、输出参数中的类都可以称为成员朋友类,而出现在方法体内部的类不属于朋友类。
举个简单的例子,部门需要进行汇报,P8要求P7做一下汇报,但是实际做PPT的是P6,干活的是P5。但是P8不能够直接找到P7,P6,P5把他们组合一下,因为P7是”朋友“,需要P7去直接或间接的联系P6和P5。最开始是这样的,上代码:
public class P7 {
public void doPresentation(){
System.out.println("拿P6和P5的内容来汇报");
}
}
public class P6 {
public void doPPT(){
System.out.println("做PPT呢");
}
}
public class P5 {
public void doWork(){
System.out.println("干活被人拿来汇报");
}
}
public class P8 {
private P7 p7;
private P6 p6;
private P5 p5;
public P8(P7 p7, P6 p6, P5 p5) {
this.p7 = p7;
this.p6 = p6;
this.p5 = p5;
}
public void projectMeeting(){
p5.doWork();
p6.doPPT();
p7.doPresentation();
}
public static void main(String[] args) {
P8 p8 = new P8(new P7(), new P6(), new P5());
p8.projectMeeting();
}
}
这样导致的现象就是耦合性比较大,不符合迪米特法则,修改成这样:
public class P8 {
public void projectMeeting(P7 p7){
p7.doPresentation();
}
public static void main(String[] args) {
P8 p8 = new P8();
p8.projectMeeting(new P7());
}
}
public class P7 {
public void doPresentation(){
new P6().doPPT();
System.out.println("拿P6和P5的内容来汇报");
}
}
public class P6 {
public void doPPT(){
new P5().doWork();
System.out.println("做PPT呢");
}
}
public class P5 {
public void doWork(){
System.out.println("干活被人拿来汇报");
}
}
当然很多时候过度使用也会造成一定的问题,但是迪米特原则的好处在于职责清晰,很多时候利于维护。
参考文章
1.[设计原则与模式](https://www.jianshu.com/p/ecdb91945153)
2.[Java 设计模式6大原则之(一):开闭原则](https://blog.csdn.net/qq\_40116418/article/details/124745725)
3.[Java设计模式](https://blog.csdn.net/qq\_25928447/article/details/124884700)
4.[里氏替换原则](http://c.biancheng.net/view/1324.html)
5.[依赖倒置](https://blog.csdn.net/m0\_54485604/article/details/113756740)
6.[依赖倒置原则](https://www.jianshu.com/p/a7f51723228b)
7.[合成复用原则](https://blog.csdn.net/logictime2/article/details/105583064)
2 条评论
《我们的新生活》剧情片高清在线免费观看:https://www.jgz518.com/xingkong/65581.html
真棒!