最近在学设计模式,很多东西很抽象,理解起来挺困难的,所以在此写一些博客记录一些我的理解,如果能够帮到一些人,那自然更好,当然欢迎在评论区批评指正交流,不胜感激。
一、单一职责原则
定义:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
分析:一个类的主要职责包括两个方面,数据职责和行为职责。数据职责通过其属性来体现,行为职责通过方法来体现。如果职责太多,将导致系统非常脆弱,一个职责可能会影响其他职责,因此我们应将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中。
理解:这个其实不难理解,就是要将一个类的功能细化,每个类完成某一项特定的职责,分工明确。这个原则主要是实现高类聚、低耦合的指导方针。是最简单最容易理解的原则。
二、开闭原则
定义:一个软件实体应当对扩展开放,对修改关闭。
分析:所有的软件都会面临一个问题,即对他们的需求会随着时间的变化而发生变化。当软件系统需要面对新的需求时,我们应该尽量保证系统的设计框架是稳定的,不能随意的为实现功能而对系统框架进行修改。如果一个软件的设计符合开闭原则,那么可以非常方便的对系统进行扩展,并且在扩展的同时无需对原有代码进行修改,使得软件在整个生命周期中都具有较好的稳定性。
理解:即需要对某个模块的功能进行扩展时,可以在不对原有代码进行修改的情况下,只通过添加新的代码实现新功能的扩展。就像插卡的游戏机,只需要将游戏卡插到游戏机上,游戏机就可以玩卡上的游戏了,但你不能这么干:我的游戏存在U盘里,我想玩这个游戏,但游戏机上又没有USB接口,所以我就改装这个游戏机,给它装一个USB接口。
正确的方式是,你在设计这个游戏机时,就应该考虑到游戏可能存储在U盘里的这种情况,所以你要为U盘预留USB接口,这才是符合开闭原则的。
三、里氏代还原则
定义:所有引用父类的地方必须能透明地使用其子类的对象。
分析:在软件中,如果能使用其基类对象,那么一定能够使用其子类对象。把父类对象都替换成它的子类对象,程序将不会产生任何错误和异常(注意:一般顺序颠倒后将无法成立)。
理解:比如一个方法中需要一个A类的参数 void method1(A a);并且有一个类B,B继承了A,那么可以用一个B类的对象把这个A类的对象给替换掉,同理其它继承了A类的对象也可以将A类对象替换点,并且程序本身不会产生错误,那么我们可以说这个程序是符合里氏代还原则的。
一般里氏代还原则都会伴随着依赖倒转原则一起出现,不过也不尽然。但正常情况下,里氏代还原则一定会伴随着依赖注入的概念一起出现。
什么是依赖注入?
public interface AbstractBook
{
public void view();
}
public interface AbstractReader
{
public void read();
}
public class Reader implements AbstractReader
{
private AbstractBook book;
public Reader(AbstractBook book)
{
this.book = book;
}
public void read()
{
book.view();
}
}
上面定义了两个接口,一个抽象book,一个抽象reader,一个Reader类实现了抽象reader接口,同时这个Reader类中还存在一个抽象book接口的对象,并且在下面的read方法中,用这个抽象book接口的对象调用了view方法,而我们知道接口中的方法都是空方法,这样的话Reade类中的read方法就会是没有意义的。
此时我们应该能想到里氏代还原则,把Reader类中的AbstractBook对象替换成其子类对象,其子类对象一定会实现AbstractBook类中的方法,这样就可以使Reader类中的read方法变得有意义。所以:
public class Book implements AbstractBook
{
public void view()
{
......
......
}
}
这里实现了AbstractBook接口,此时我们就可以用这个Book类的对象把上面Reader类中的其基类(AbstractBook)对象给替换掉。那如何替换呢?
这里就用到了依赖注入中的一种方式——构造注入。
简单来说,依赖注入就是将一个类的对象传入另一个类,注入时应该尽量注入父类对象,而在程序运行时再通过子类对象来覆盖父类对象,这一定程度上也体现了依赖倒转原则,这个后面再说。
可以发现Reader类中有一个构造方法,我们可以利用这个构造方法来实现我们的目的。
在客户端类中可以这样实现:
public class Client{
public static void main(String args[])
{
Book book = new Book();
Reader reader = new Reader(book);
reader.read();
...
...
}
}
可以发现我们利用Reader类的构造方法,成功将Book类的对象注入了Reader类,替换覆盖了Reader类里面的AbstractBook类对象,成功用其子类对象替换了父类对象,并达到实现相应功能的目的。我们可以说,这个程序的设计**符合里氏代换原则**。
除了上述的构造注入方式外,依赖注入还有两种方式:设值注入、接口注入。这两种方式只是完成目的用的方法不一样,这里不做赘述。
四、依赖倒转原则
定义:高层模块不应该依赖低级模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。要针对接口编程,不要针对实现编程。
分析:代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。也就是说,在程序代码中传递参数时或在组合聚合关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换,不要用具体类来做这些事情。
理解:分析中的橙字部分已经将这个原则阐述的很通俗易懂了,即程序设计面向抽象类和接口进行编程。通常在运用了这个原则过后,需要通过依赖注入的方式,将实体类的对象注入,覆盖替换原来的抽象对象。
五、接口隔离原则
定义:客户端不应该依赖那些它不需要的接口。
分析:每个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。
理解:这个原则比较好理解,感觉和单一职责原则有点像。一个类实现一个接口,必须要实现接口中的所有方法,有些他不应该或不用实现的方法就不要放到这个接口里了。
六、合成复用原则
定义:尽量使用对象组合,而不是继承来达到复用的目的。
分析&理解:一个类中若想使用另一个类中的方法,有两种方式:
(1) 继承那个类,这样就继承了那个类中的方法,从而实现目的,但这种方法有一定的缺点,当一个类被多个子类继承过后,此时对这个类进行修改,则会“牵一发而动全身”,所有继承了这个类的类都会受到影响,若要降低影响,则要创建新的类,修改其他类使其继承这个新类,不符合开闭原则。因此我们要尽量少使用这种方式。
(2) 组合/聚合方式,此种方式简要来说就是将上面方法中父类的对象作为一个类的成员变量。比如一个类A要使用类B中的一个方法,则可以将B类的一个对象作为A类的一个成员变量,因此可以通过调用这个成员变量的方式调用B类中的方法。并且当某个类要修改这个B类中的方法时,可以添加一个新的类继承类B,在新类中重写那个需要变化的方法,如果符合里氏代还原则,只要将这个新类对象注入到需要的类中即可。
七、迪米特法则
定义:只与你的朋友直接通信。每个软件单位对其他的单位都只有最少的知识,而且局限于那些于本单位密切相关的软件。
分析&理解:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
这里举一个例子:
public class Friends
{
Stranger str;
public void talk()
{
str.operation();
}
}
public class Stranger
{
public void operation()
{
System.out.println("hello world");
}
}
public class Someone
{
Friends f;
f.talk();
}
从上面可以发现,Someone类中有一个Friends对象,但此时若想调用Stranger类中的operation方法,有两种方式:
1、在类中创建一个Stranger类的对象,通过该对象调用方法
2、通过类中已有的Friends对象间接调用
这里第二种方式是符合迪米特法则的,这也符合类之间低耦合的指导思想。