1、软件设计的七大原则
设计模式不是为每个人准备的,而是基于业务来选择设计模式,要明白一点,技术永远为业务服务,技术只是满足业务需要的一个工具。在软件开发中,应当尽量提高系统的可维护性和可复用性,增加软件的可扩展性和灵活性。
在了解设计模式之前,我们先了解下软件设计的七大原则,它们分别是:单一职责原则、里氏替换原则、接口隔离原则、开闭原则、依赖倒置原则、迪米特法则和合成复用原则。
实际上,这些原则的目的只有一个:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。
1.1、单一职责原则-SRP
1.1.1、定义
单一职责原则(Single Responsibility Principle,SRP)又称单一功能原则,由罗伯特·C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。
英文定义:There should never be more than one reason for a class to change。
一个类改变的原因不应该超过一个(这里的职责被定义为“变化的原因”)。
单一职责原则:是指一个类(大到模块、小到方法)只负责完成一个职责。通俗的说就是一个类、模块、方法不要承担过多的任务,要符合高内聚、低耦合的思想。
所以我们设计一个类的时候不应该设计成大而全的类,要设计粒度小,功能单一的类。一个类承担的职责越多,它被复用的可能性更小,当一个职责变化时,可能会影响其他职责的运作。
单一职责原则,就是为了处理“胖”接口的缺点,如果一个类有两个或者两个以上互不想干的功能,那我们就说它违背了单一职责原则(是不内聚的类),应该将其拆分功能单一、粒度更细的类。但是如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
1.1.2、原则解析
其实在生活的方方面面都体现了我们设计中的很多原则。古人说的不错:狡兔死,走狗烹;飞鸟尽,良弓藏;敌国破,谋臣亡。善谋略、握兵权,功高震主,一个人功能太多,太锋芒毕露,就是比较危险的人物。
同理,一个类承担过多的职责,也是比较危险的一种行为。一旦涉及到修改,尤其是较大的修改,就可能出现出人意料的bug。
在JavaEE中的分层架构中,实际上就体现了单一职责原则。对用户操作,可以分解为存储数据的实体类User,完成数据库操作的UserDao,完成业务操作的业务类UserService以及显示用户信息的页面user.jsp。
-
案例实践
如果有这样一个抽象类Door。
abstract class Door{
abstract void open();
abstract void close();
}
一个很纯粹的抽象类,符合单一职责。那现在我们需要为这个门增加一个报警功能alarm方法呢?你会怎么加?
abstract class Door{
abstract void open();
abstract void close();
abstract void alarm();
}
或者是
class AlarmDoor extends Door{
void open();
void close();
void alarm();
}
你觉得哪种方式对呢?
其实,上面两种方式都违反了ISP原则。
首先第一种方式:把Door概念本身具有的行为方法和另外一个概念“报警器”的行为混在一起了。并不是所有的门都需要报警器这个功能,门这个抽象类(或接口)已经被不需要的功能污染了,那么以Door作为基类的子类会因“报警器”这个概念的改变而改变,并使其变“胖”。
第二种方式:这个AlarmDoor在概念本质上到底是Door还是报警器?一般使用继承在本质上就是is-a的关系。既然open、close和alarm属于两个不同的概念,根据ISP原则就应该把alarm拆出来。
interface Alarm{
void alarm();
}
class AlarmDoor extends Door implements Alarm{
void open();
void close();
void alarm();
}
注:这样写说明AlarmDoor本质上是Door,又具有报警概念。如果AlarmDoor本质上是报警器,那就应该反过来。
1.1.3、总结及建议
单一职责原则是开发中最基础、最简单的一个原则,但又是一个最难把握的一个原则,在实际中有时很难界定职责的边界,这个变化原因的粒度也需要结合实际去运用,因为可能每个人理解的职责范围或者考虑方向不一样,得出的结论可能就不一样。就像做菜,说明上是写,盐方少许,那这个少许的标准是多少?我们和厨师区别大概就在这个度的把握上。
再比如:在西方文化中,吃饭的工具有刀和叉,他们对刀和叉的分工就很明确,刀负责分离食物,叉负责运送食物。而我们吃饭的工具就是筷子,一双筷子既可以分离食物也可以运送食物。很大程度上,中西文化差异也会造成理解上的差异。
所以呢,不必严格遵守原则,只是作为参考。实际开发中可以参考以下意见:
1)一般对一个类或模块职责的分解可以从两个方面去考虑:一个是属性(结构)职责和一个是行为职责。
2)类依赖过多的其他类,或者代码直接依赖关系过于复杂时,不符合高内聚低耦合的设计思想时,就可以考虑对代码进行拆分;
3)类名和实际功能关系不大或者没有任何关联时,可以把无关的功能独立出去;
4)类的代码函数过多影响可读性和代码维护时,可以对代码进行方法级别的拆分;
总之一句话:接口尽量做到单一职责,类的设计尽量做到只有一个原因引起变化,因为我们的最终目的都是让我们的代码具有更好的复用性、可读性、可维护性、可扩展性......
1.2、开闭原则-OCP
1.2.1、定义
1988年,Bertrand Meyer在他的著作《Object Oriented Software Construction》中提出了开闭原则(Open-Closed Principle, OCP)。
英文定义:Software entities(classes,modules,function,etc)should be open for extension but closed for modification。
一个软件实体(如类、模块和函数等)应该对扩展开放,对修改关闭。
开闭原则中的“开”,是指对于组件功能(提供方)的扩展是开放的,是允许对其进行功能扩展的;开闭原则中的“闭”,是指对于原有代码的修改(使用方)是封闭的,即不应该修改原有的代码。
抽象化是开闭原则的关键,强调的是用抽象构建框架,用实现扩展细节。简单的说:尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来引入新功能,达到开闭原则的要求。
其他设计原则都可以看作是开闭原则实现的手段和方法。
1.2.2、原则解析
其实看完上面那些定义就很抽象,因为我也没明白怎么扩展?怎么关闭?
-
什么是为扩展开放?为修改关闭?
有一句话总结的不错:对扩展开放是为了应对需求的变化,对修改关闭是为了保证代码的稳定性。
我们添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等,比如:说普遍一点就是通过继承和组合的方式扩展,说的详细一点,实际中我们常见的方式有多态、依赖注入、面向接口编程等等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
这里的关闭并不是说完全杜绝了修改,因为不管一个模块有多“封闭”,总会有一些变化,它不会或者无法完全关闭。这就要我们找到代码的稳定点和变化点,将变化控制在一定的范围,事先在这里预留好扩展点,以最小的代价来完成新功能的开发,新代码能够很灵活的插入到扩展点上。
开闭原则作为最重要的原则之一,我们这里就描述的更详细一点。
-
为什么抽象是开闭原则的关键?
我们来看一个案例:
这符合开闭原则吗?首先这是一个客户端连接服务端的一个例子。客户端是固定的,稳定点是客户端,服务端是不固定的变化点,有一天我需要新增或者更换服务端,那我客户端Client的代码是不是需要较大的修改了。这不符合开闭原则。
那如何保证整体的稳定性,如何去隔离变化点,使其趋于稳定?
开闭原则的关键就是抽象,我们抽象出变化的部分,将其隔离起来。即客户端类的对象将使用派生服务器类的对象。如果我们希望客户机对象使用不同的服务器类,那么可以创建AbstractServer类的新派生,那客户端的代码几乎不用修改,新增服务器也只需要通过派生新的类去扩展。
-
案例实践
假如我们做一个银行的系统,客户要求支持最基本的存款、取款、转账功能。看下面的例子:
public class BankBusiness {
public void SaveMoney(){
System.out.println("存款");
}
public void drawMoney(){
System.out.println("取款");
}
public void transferAccounts(){
System.out.println("转账");
}
public void bankService(Customer cust){
//如果是jdk1.7可以直接用字符串,1.6需要char
switch (cust.getActionType().charAt(0)){
case 'c' :SaveMoney(); break;
case 's' :drawMoney(); break;
case 't' :transferAccounts(); break;
default:break;
}
}
public static void main(String[] args) {
Customer cust = new Customer("c");
new BankBusiness().bankService(cust);
}
}
class Customer{
private String actionType; //业务类型
public String getActionType() {
return actionType;
}
public void setActionType(String actionType) {
this.actionType = actionType;
}
public Customer(String actionType) {
this.actionType = actionType;
}
}
上面这个案例符合开闭原则吗?
首先我们找到变化点,这里的变化点很明显是我们的业务类型,bankService方法就不符合开闭原则,因为它不能完成对银行新业务的支持。如果有一天银行因业务拓展,支持理财业务呢?那客户来办理银行业务BankBusiness中要新增方法,bankService是不是也要修改,来满足我的新业务呢。
注:switch语句会在应用程序中的各种函数中反复出现,并不是所有的业务都不能用switch或if/else,只要是稳定点也是可以使用的。
那如何来实现对业务类型的关闭呢?
public class BankBusiness {
IBankService bankService;
//为BankBusiness对象注入IBankService实现类对象
public BankBusiness(IBankService bankService){
this.bankService = bankService;
}
public static void main(String[] args) {
IBankService ibs = new SaveMoney();
new BankBusiness(ibs).bankService.business();
IBankService ibsm = new ManageMoney();
new BankBusiness(ibsm).bankService.business();
}
}
interface IBankService{
public void business();//业务
}
class SaveMoney implements IBankService{
@Override
public void business() {
System.out.println("存款");
}
}
class DrawMoney implements IBankService{
@Override
public void business() {
System.out.println("取款");
}
}
class TransferAccounts implements IBankService{
@Override
public void business() {
System.out.println("转账");
}
}
class ManageMoney implements IBankService{
@Override
public void business() {
System.out.println("理财");
}
}
开闭原则的关键就是抽象,将变化点抽象出来,在运用一些方法预留扩展点,使得我们的程序更加稳定。我们把不同类型的业务服务抽象出来,提供统一的入口都叫IBankService,在BankBusiness中使用依赖注入的方式注入IBankService实现对象。以后要扩展业务类型只需要实现IBankService接口,而不用在修改BankBusiness,以及之前的switch中的一些。
扩展-组合和继承的区别
一般java扩展的方式都会说是:继承和组合。我们在学习UML关系时,其实一共包含六种关系(依赖、关联、聚合、组合、泛化、实现),只不过是更细的划分,其中依赖,关联、聚合都属于组合。
组合和继承是面向对象中两种代码复用方式,组合就是在新类中创建原有类的对象,重复利用已有类的功能。继承是面向对象的主要特征之一,通过继承子类可以使用父类中的一些成员变量与方法。
既然组合和继承都可以实现代码的复用,为什么在java设计原则中反复强调组合要优先于继承?那继承和组合该如何选择?
一般情况下,遵循以下两点原则:
1)除非两个类之间是[is-a]的关系,否则不要轻易使用继承,过多的继承会破坏代码的可维护性,当父类被修改时,会影响到所有继承自它的子类。
2)不要仅仅为了实现多态而使用继承,如果类之间没有[is-a]的关系,可以通过接口与组合的方式来达到相同的目的。
例如:
从下图中我们可以看出Car是Vechicle交通工具的一种,因此是一种继承关系(又被称为[is-a]的关系);而Car包含了多个Tire轮胎,因此是一种组合关系(又被称为[has-a]关系)。
//继承关系
class Verchicle{
}
class Car extends Verchicle{
}
//组合关系
class Tire{
}
class Car extends Verchicle{
private Tire t = new Tire();
}
总之是[is-a]的关系就用继承,不是的话就用组合。为什么强调组合优于继承?后续再合成复用原则中会更详细的说明。
1.2.3、总结及建议
开闭原则的目的是为了代码的可扩展性,并且避免了对现有代码的修改给软件带来的风险。开闭原则的前提是分析出稳定点和变化点,对变化点进行抽象,再为变化点预留未来的扩展点,使得我们能以最少的变动来扩展现有功能。
如何找到我们的扩展点?
1)针对业务驱动的系统,要充分了解业务需求;
2)针对通用技术开发,需要为功能升级预留扩展点;
3)不一定要为未来每个变化点都预留扩展点,扩展点过多也会给系统带来极大的复杂度。
1.3、里氏替换原则-LSP
1.3.1、定义
里氏替换原则(Liskov Substitution Principle,LSP)是对子类型的特别定义。它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。
英文定义:If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。
如果S是T的子类型,则T类型的对象可以替换为S类型的对象,而不会破坏程序。
Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
所有引用其父类对象方法的地方,都可以透明的使用其子类对象
里氏替换原则强调的是:只要父类出现的地方子类就可以出现,而且调用子类还不产生任何错误或异常,但是子类出现的地方未必就能适应父类。
通俗的说就是:它强调的是一种继承思想的规范,继承必须确保超类拥有的性质在子类中依然成立。
1.3.2、原则解析
里氏替换原则和多态有点类似,但是他们关注的角度不一样,多态是面向对象编程的特性,而里氏替换是一种设计原则,用来指导继承中子类如何设计,子类的设计要确保在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
子类在设计的时候,要遵循父类的行为约定,父类定义了方法的行为,子类可以改变方法的内部实现,但是不能改变原有方法的行为约定,比如:对参数值、返回值、异常的约定等。
-
看一个案例
圆是椭圆的一种特殊形态。下图Circle(圆)继承Ellipse(椭圆),那下面这种继承符合里氏替换原则?
从数学的角度来讲,圆是椭圆的一种特殊情况。但是在面向对象编程中,椭圆有一些圆没有的特性或行为。比如:椭圆的宽高比例可以不固定。如果这样的特性对你的程序很重要的话,椭圆就不可能是圆的对象,这将违背LSP原则(这样的子类,无法完全包含父类的所有属性)。
-
案例实践
public class WholesaleStore extends Store{
private int sellNum;
WholesaleStore(int sellNum){
this.sellNum = sellNum;
}
@Override
void sellOneProduct() {
if(sellNum<=2){
throw new IllegalArgumentException("最少批发三件");
}
}
}
class Store{
void sellOneProduct(){
System.out.println("一件代发");
}
}
上面这个案例符合里氏替换原则原则吗?
虽然WholesaleStore作为子类可以替换Store父类,但是子类已经改变了父类原有方法的初衷,如果父类支持一件代发,那么调用子类的方法时件数如果少于3件就会抛出异常。不符合里氏替换原则。
1.3.3、总结及建议
采用开闭原则必然会用到抽象和多态,而这离不开继承,而里氏替换原则对如何良好的基础提出了衡量依据,里氏替换原则使代码符合开闭原则的一个重要保证。
虽然继承实现了代码的复用,但是继承是侵入性的,只要继承,就必须拥有父类的所有属性和方法,这增强了耦合性,当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。然而在实际使用过程中却往往会出现滥用继承的现象,而里式替换原则可以很好的帮助我们在继承关系中进行父子类的设计。
实际开发中可以参考以下意见:
1) 子类可以实现父类的抽象方法,但是尽量少覆盖父类非抽象方法,即使是覆盖也不能改变父类方法原有的行为。
2)子类方法的实现要比父类方法更严格或相等。
1.4、接口隔离原则-ISP
1.4.1、定义
接口隔离原则(Interface Segregation Principle,简称ISP),是 Robert C.Martin 在2002年在一本叫《Agile Software Development: Principles, Patterns and Practices》书中提出的。
英文定义:Clients should not be forced to depend upon interfaces that they do not use
客户端不应该被强迫依赖它不需要的接口
The dependency of one class to another one should depend on the smallest possible interface
一个类对另一个类的依赖应该建立在最小的接口上
接口隔离原则告诉我们接口不应包含实现类不需要的方法,尽量细化接口。否则,这类接口我们称之为“胖”接口,实现胖接口的类要为那些不需要的方法提供空实现,使得结构变得复杂。
1.4.2、原则解析
接口隔离原则和单一职责原则都是为了提高类的内聚性、降低它们之间的耦合性。但是他们两者是不同的:
单一职责原则注重的是职责,而接口隔离原则注重的是对接口的依赖隔离;
单一原则主要是约束类,是针对程序中的实现和细节;接口隔离原则主要约束的是接口,主要针对抽象(接口就是抽象的一种约定)和程序整体的框架的构建。
我们现实生活中也有很多这样的例子:比如售票大厅的售票窗口,分为售票的、退票的窗口让不同需求的人排在不同的窗口,不仅效率高还节约时间。或者是银行的业务窗口,办理普通业务的、理财业务的等等。
-
案例实践
public class User implements IUser{
@Override
public void deleteUser() {
}
@Override
public void register(User user) {
}
@Override
public void login() {
}
@Override
public void sendMessage() {
}
}
interface IUser{
void register(User user);
void deleteUser();
void login();
void sendMessage();
}
上面这个案例符合接口隔离原则吗?
IUser接口作为用户接口,本身用户登录、注册和短信之间没有直接的关系。如果我们在其他模块也需要使用到短信发送功能呢?直接继承User类?那会把本身不想提供给其他模块的功能暴露了。正确的做法是需要把无关的功能剥离出去。
public class User implements IUser{
IMessage SendMessage;
@Override
public void deleteUser() {
}
@Override
public void register(User user) {
}
@Override
public void login() {
}
}
interface IUser{
void register(User user);
void deleteUser();
void login();
}
interface IMessage{
void sendMessage();
}
class SendMessage implements IMessage{
@Override
public void sendMessage() {
//......
}
}
1.4.3、总结及建议
大而全接口存在太多的不确定性,在接口设计中,我们应该遵循接口隔离原则。所以:
1)对接口来说,如果某个接口承担了与它无关的接口定义,则说该接口违反了接口隔离原则,可以把无关的接口剥离出去;
2)对公共的功能来说,应该细分功能点,按需添加,而不是定一个大而全的接口,让子类被迫实现。
3)它的应用不应该仅仅局限于接口、小到类、方法,大到系统层面,都可以遵循隔离原则,不需要暴露的类和方法也可以使用private、protected等权限隔离。
1.5、依赖倒置原则-DIP
1.5.1、定义
依赖倒置原则(Dependence Inversion Principle,DIP)是 Object Mentor 公司总裁罗伯特·马丁(Robert C.Martin)于 1996 年在 C++ Report 上发表的文章中提出来的。
英文定义:High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions。
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖具体细节,具体细节应该依赖抽象。
依赖倒置的核心思想:就是面向接口编程。
1.5.2、原则解析
首先我们来明确几个概念,例如:
public class A{
public void query(){
B b = new B();
b.query();
}
}
class B{
public void query(){
//...查询信息
}
}
高层模块:从代码层面来说是调用者,上面的A就是高层模块;
低层模块:从代码层面来说是被调用者,上面的B就是低层模块;
抽象:即抽象类或接口,两者都不能实例化;
细节:即具体的实现类,实现接口或者继承抽象类的类,可以被实例化;
-
那么什么是依赖倒置?
依赖正置:我们在学习UML关系的时候,就有一个依赖关系。类间的依赖是实实在在的(实现)类间的依赖,也是面向实现编程;上面的A依赖于B的查询,就是正常的依赖关系;
依赖倒置:其本质就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合,面向要求进行依赖,即我们常说的面向接口编程,这也是依赖倒置的核心。
-
看一个案例
《资本论》中都曾经运用了依赖倒置原则,在还没有货币的时候,人们以物换物来各取所需。假设你要买一台电脑,卖电脑的要你拿一头牛给他换,但是你并没有养牛,只会编程,于是你找到一位养牛户,说给他做个养牛APP来换一头牛,养牛户说换牛可以,但是你得用一条黄金项链来换,于是你又要去找等价黄金,这里就出现了一连串的对象依赖,从而造成了严重的耦合灾难。为了解决这个问题,最好的办法就是双方买卖都依赖于抽象,也就是货币。
还有我们现在的电脑提供了很多的插槽(接口),比如:我们可以自己更换硬盘、更换内存卡等等,要方便很多。
-
案例实践
public class Mike {
public static void main(String[] args) {
JavaCourse jc = new JavaCourse();
jc.study();
}
}
class JavaCourse{
void study(){
System.out.println("学习了java视频");
}
}
Mike非常爱学习,最近在疯狂的学习Java,很快Mike掌握了它。这里是main调用方,直接请求JavaCourse,main的箭头指向JavaCourse。
接着又开始学习Python。那我这从低层到高层的代码都要修改。过了一个月,这Mike又开始学习Vue。如此一来,我这系统自发布后频繁的改动,很不稳定。
为此,我回去学习了设计模式,准备对这个模块进行重构。
public class Mike{
ICourse courseJava;
ICourse pythonCourse;
public Mike(){
courseJava = new JavaCourse();
pythonCourse = new PythonCourse();
}
public void study(){
courseJava.study();
pythonCourse.study();
}
public static void main(String[] args) {
Mike mk = new Mike();
mk.study();
}
}
interface ICourse {
void study();
}
class JavaCourse implements ICourse {
@Override
public void study() {
System.out.println("学习Java课程");
}
}
class PythonCourse implements ICourse {
@Override
public void study() {
System.out.println("学习Python课程");
}
}
经过改造后,这里是main指向ICourse,有调用方main委托Mike调用接口去实现。此时JavaCourse的箭头是相反的指向ICourse。这就叫依赖倒置。
扩展:什么是依赖注入和控制反转
控制反转(Inversion of Control,IOC)是一种思想,就是把控制权反转给第三方。依赖注入(Dependency Injection,DI)是控制反转最典型的实现方法。实际上他们描述的是同一个概念,只是站在两个不同的角度去描述问题。把原本的耦合实体类交给第三方,并且可以由使用者传入不同的实现类,得到不同的结果。
什么是依赖注入,举个例子:
在原始的农耕时代,人类学会了磨制石斧。耕种者需要(发起调用者)需要斧头,就只能自己去磨一把石斧(被调用者)。就是调用者自己创建被调用者。
18世纪60年代,英国工业革命开始,蒸汽机的出现,将人类代入了蒸汽时代。
进入了工业化时代,工厂出现,斧子不再由普通人完成,而是在工厂里被生成出来,此时需要斧子的人(调用者),只要找到工厂,购买斧子,无需关心斧子的制造过程。这其实就是java的简单工厂设计模式。
后来人民公社出现了。
进入“按需分配”的社会,需要斧子的人不用再找工厂买,而只需要提供相应的票据,斧子就自然会到自己手里。这就对应了Spring的依赖注入,你提供什么票据,就给你生成什么东西。
什么是控制反转,举个例子:
一般,我们把请求服务的一方即发起方叫做客户端(Client),把提供服务的一方叫做服务端(Server)。我们一把由Client主动调用Server,这个叫控制,而当Server调用Client时,我就说是控制反转(或是叫回调)。控制反转一般着眼于流程的控制权,一般来说,程序的控制权属于Client,一旦控制权交到了Server手里,就是控制反转了。
1.5.3、总结及建议
依赖倒置原则的目的是通过要面向接口编程来降低类间的耦合性,所以在实际编程中尽量满足这个规则。
1)每个类尽量提供接口或者抽象类;
2)任何类都不应该从具体类派生。
扩展:依赖注入三种方式
spring运用了依赖注入的思想,即依赖类不由程序员实例化,而是通过spring容器帮我们new指定实例,并将实例注入到需要该对象的类中。
依赖注入有三种方式:构造注入、set注入、注解注入
构造注入
在类中,不用为属性设置setter方法,但需要生成该类带参的构造方法;
在配置文件中配置该类的bean,并且配置构造器,在配置构造器中用到了 <constructor-arg>节点,该节点有四个属性:
index:指该属性所对应的类型
type:指该属性所对应的类型
ref:引用的依赖对象
value:当注入的不是依赖对象,而是基本数据类型时,就用vale;
再回过头来看,我们上面的程序。我的main方法,调用Mike对象的study()方法,即完成了多门课程的学习,课程学习对象在内部就已经创建好了。但是在实际运用中,我们很难知道使用方需要什么,那我想提供一个更灵活的方式来选择,有没有办法由调用者自己决定学什么吗?
答案是有的。那就是依赖注入。
public class Mike{
ICourse course;
public Mike(ICourse course){
this.course = course;
}
public void study(){
course.study();
}
public static void main(String[] args) {
Mike mk = new Mike(new JavaCourse());
mk.study();
Mike mkp = new Mike(new PythonCourse());
mkp.study();
}
}
interface ICourse {
void study();
}
class JavaCourse implements ICourse {
@Override
public void study() {
System.out.println("学习Java课程");
}
}
class PythonCourse implements ICourse {
@Override
public void study() {
System.out.println("学习Python课程");
}
}
我们通过构造注入,把主动权交给调用方,由它决定调用谁,我程序内部就调用谁。中在spring中可以通过配置来完成:
<bean id="mike" class="xxx.Mike">
<constructor-arg ref="course"></constructor-arg>
...
</bean>
Set注入
set注入要求bean提供一个默认的构造函数,并且为需要注入的属性提供对应的settter方法。如果有带参构造,需显示的提供一个无参构造。
Spring先调用Bean的默认构造函数实例化Bean对象,然后通过反射的方式调用settter方法注入属性值。
public class Book {
private String name;
public String getName() {
System.out.println(name);
return name;
}
//set 方法
public void setName(String name) {
this.name = name;
}
}
public class BookTest {
public static void main(String[] args) {
//1、加载spring配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("book_config.xml");
//2、获取xml配置文件创建的对象
Book book = context.getBean("book", Book.class);
book.getName();
}
}
book_config.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
<bean id="book" class="com.cn.spring.Book">
<property name="name" value="Java设计模式"></property>
</bean>
</beans>
案例效果:
注解注入
使用注解的方式进行构造注入或者set注入。spring2.5开始提供了基于注解的配置。在java中可以使用@Resource或者@Autowired注解方式来注入。
-
@Resource:由J2EE提供默认安装名称来装配注入,只有当找不到与名称匹配的bean才会按照类型来装配注入;
-
@Autowired:默认是按照类型装配注入,如果想要按名称来装配,需要结合@Qualifier一起使用。
具体的这里就不再详细讲解了,这部分内容会在后续的spring文章中详细介绍。
1.6、迪米特法则-LOD
1.6.1、定义
迪米特法则(Law of Demeter,LOD)又叫作最少知识原则(Least Knowledge Principle,LKP),产生于 1987 年美国东北大学(Northeastern University)的一个名为迪米特(Demeter)的研究项目,要求每个方法只能给有限的对象发送消息,项目团队将这些规则称之为迪米特法则。
英文定义:Talk only to your immediate friends and not to strangers。
只与你的直接朋友交谈,不跟“陌生人”说话。
如果两个类不必彼此直接通信,那么这两个类就不应该发生直接的相互作用。如果其中一个类需要调用另外一个类的方法的话,可以通过第三者转发这个调用。
迪米特法则的初衷在于降低类之间的耦合,由于每个类尽量都减少了对其他类的依赖,因此很容易使得系统功能模块的独立,相互之间不存在或很少有依赖关系。
1.6.2、原则解析
迪米特法则中的“朋友”是指:同当前对象存在依赖、关联、聚合或组合关系的类或对象我们称之为朋友。而出现在成员变量、方法参数、方法返回值中的类或对象为直接的朋友;而出现在局部变量中的类或对象则不是直接的朋友,也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
现实生活中也有很多这样的案例,比如我们去买房子或者租房子,完全自己去找的话既费时又费力,如果我们找个中介,由中介帮我们联系,我们就能省去很多的事情。
-
案例实践
明星与经纪人的关系。
public class Agent {
private MediaCompany myCompany;
private Star myStar;
private Fans myFans;
public Agent(MediaCompany myCompany,Star myStar){
this.myCompany = myCompany;
this.myStar = myStar;
}
public Agent(Fans myFans,Star myStar){
this.myFans = myFans;
this.myStar = myStar;
}
public MediaCompany getMyCompany() {
return myCompany;
}
public void setMyCompany(MediaCompany myCompany) {
this.myCompany = myCompany;
}
public Star getMyStar() {
return myStar;
}
public void setMyStar(Star myStar) {
this.myStar = myStar;
}
public Fans getMyFans() {
return myFans;
}
public void setMyFans(Fans myFans) {
this.myFans = myFans;
}
public void meeting(){
System.out.println(myFans.getName()+"与"+myStar.getName()+"见面了");
}
public void business(){
System.out.println(myCompany.getName()+"与"+myStar.getName()+"洽谈合作");
}
public static void main(String[] args) {
Agent meet = new Agent(new Fans("赵丽颖粉丝"),new Star("赵丽颖"));
meet.meeting();
Agent bus = new Agent(new MediaCompany("分众传媒"),new Star("赵丽颖"));
bus.business();
}
}
class MediaCompany {
private String name;
MediaCompany(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Fans {
private String name;
Fans(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Star {
private String name;
Star(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
明星每天也有自己的事情要处理,面对粉丝、面对外界媒体,不可能都由明星自己亲自出马。所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
1.6.3、总结及建议
过分使用迪米特法则会产生大量的中介和传递类,反而会导致系统复杂度变大。所以采用迪米特法则是要反复权衡,既要做到结构清晰,又要高内聚低耦合。
外观模式和中介者模式都是迪米特法则的应用。
1.7、合成复用原则-CRP
1.7.1、定义
合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
英文定义:Favor object composition over class inheritance.
优先使用对象组合,而不是类继承。
合成聚合复用原则指在一个新对象中通过关联(组合或聚合)关系,使用原来已经存在的一些对象,作为新对象的一部分,新对象通过向这些对象委派相应的动作达到复用已有功能的目的。
1.7.2、原则解析
前面在介绍开闭原则的时候,面对扩展开发的问题,其实就已经提及到使用组合或继承的方式去扩展功能。
那为何要强调“要尽量使用合成和聚合,尽量不要使用继承”呢?
第一,继承复用破坏封装,它把超类的实现细节直接暴露给了子类,这违背了封装及信息隐藏的初衷;
第二,如果超类发生了改变,那么子类也要相应的改变,这就直接导致了类与类之间的高耦合,不利类的扩展、复用、维护等;
第三,从超类继承而来的实现是静态的,不会在运行时发生改变,没有足够的灵活性。
而合成和聚合的时候新对象和已有对象往往是通过接口或者抽象类进行的,就可以很好的避免上面的不足,而且可以让每一个新类都专注于实现自己的任务,符合单一职责。
-
案例实践
假设,我们汽车分为2种,一种是烧油的汽车、一种是电动的汽车;然后按颜色又可以分为白色汽车、黑色汽车,如果同时考虑这两种情况分别用继承和组合来实现。
继承:
这里的颜色是一个变化点,如果要增加新的颜色,那么子类越来越多而且还要修改代码,这违背了开闭原则。
组合:
1.7.3、总结及建议
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
组合与继承都是重要的复用方法。组合称为黑箱复用,继承称为白箱复用。
所以:
1)使用组合可以获得复用性与简单性更加的设计。
2)并不是继承一无是处,而是不要滥用继承,如果不是[is-a]的关系就少用继承。
1.8、设计原则总结
我们一共介绍了 7 种设计原则,它们分别为单一职责原则、开闭原则、里氏替换原则、接口隔离原则、依赖倒置原则、迪米特法则和合成复用原则。
设计原则是设计模式的基础。我们不必刻意去套用所有原则,要综合人力、时间成本等,在适当的场合下遵守设计原则,再迭代出设计模式,设计出更加灵活而优雅的代码结构。
下面用一句话去总结设计原则的精髓:
设计原则 | 一句话归纳 | 目的 |
---|---|---|
单一职责原则 | 一个类只干一件事,实现类要单一 | 结构清晰,分离出变化的原因 |
开闭原则 | 对扩展开放,对修改关闭 | 隔离变化,找到扩展点,降低维护风险 |
里氏替换原则 | 不要破坏继承体系,重写不应该影响父类方法的含义 | 防止继承泛滥 |
接口隔离原则 | 一个接口只干一件事,接口要精简单一 | 功能解耦,高聚合、低耦合 |
依赖倒置原则 | 面向接口编程 | 更利于代码结构的升级扩展 |
迪米特法则 | 减少对其他类的依赖,专业的事情可以移交给专业的对象来完成 | 只和朋友交流,不和陌生人说话,减少代码臃肿 |
合成复用原则 | 尽量使用组合或者聚合关系实现代码复用,少使用继承 | 降低代码耦合 |
关注我:获取更多知识!