您的位置:首页 > 编程语言 > Java开发

向对象设计的五个基本原则以及Java基本类型转换规则

2017-10-10 14:33 274 查看

概念

在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期引入的记忆术首字母缩略字,

指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得

更加可能。SOLID所包含的原则是通过引发编程者进行软件源代码的代码重构进行软件的代码异味清扫,从而使得软件清晰可读以及可扩展时可以应用

的指南。SOLID被典型的应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。

首字母指代概念
S单一功能原则认为对象应该仅具有一种单一功能的概念
O开闭原则认为软件体应该是对于扩展开放的,但是对于修改封闭的概念
L里氏替换原则认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”概念
I接口隔离原则认为“多个特定客户端接口要好于一个宽泛用途的接口”
D依赖反转原则认为一个方法应该遵从“依赖于抽象而不是一个实例”的概念

依赖注入所以该原则的一种实现方式
以上源自Wiki百科:SOLID(面向对象设计)
在我们的实际工作中,这五个原则互相关联和支持,可能实现一个业务功能把五个原则都用到了,也可能只用到了其中的三个

单一功能原则(S)

按照字面意思不难理解,一个对象有且仅有一种功能,同时也仅有一种功能需求的变更能够引发该对象实现的修改。
试想以下场景,如果我们有一个对象,里面大而全的实现了某业务领域内的全部功能,那么,当该领域内的需求发生变更时,我们是不是要对该对象内的代码进行修改?
再试想一下,如果我们将该业务领域内的需求进行细分,针对细分后的功能实现对应的功能(业务)对象,那么当该领域内的需求发生变更时,我们仅仅需要修改有变更需求对应的功能(业务)对象就可以?
可能有人会说,可是两种方式都是修改代码了呀?!是的,两种方式都对我们的实现进行了代码修改,这是不可避免的。

但换个角度想,此时我们修改的代码,对其他需求没有变更的功能(业务)对象来说,是不是已经没有了太大的关联?
因为实现了“单一功能原则(S)”,顺带也满足了“接口隔离(I)”原则,当然严格来说是面向接口,但面向实现也未尝不可。而此时,我们对特定业务/功能的修改不会影响到该领域内的其他实现,是不是也捎带手实现了“开闭原则(O)”?
其实,在开发过程中,有一个百试不爽的窍门就是“拆”,大拆小,直到不能再拆(原子级),遇到复杂的业务如是,遇到复杂的功能同样适用。不但有利于实现,同时也有利于测试。
延伸一下思路,无论是当下流行的微服务,还是一个方法只完成一个功能的普遍认知,是不是和单一功能原则有着相似的理念?
理论讲完,来点具体的代码实现验证一下
// C# 示例
// 做饭接口
public interface ICooking
{
//做饭接口方法
bool Cook();
}

//做中餐
public class ChineseCook: ICooking
{
public bool Cook()
{
Console.WriteLine("开始做鱼香肉丝")
Console.WriteLine("鱼香肉丝做好了,快来米西吧")
}
}

//做西餐
public class WesternCook: ICooking
{
public bool Cook()
{
Console.WriteLine("开始做蔬菜沙拉")
Console.WriteLine("蔬菜沙拉做好了, 快来eat吧")
}
}

例子写的可能不太恰当,但足够说明吃货脑子里不想别的。
如果哪天鱼香肉丝吃腻味了换成宫保鸡丁,是不是就只要修改ChineseCook的Cook实现就可以了?

又或者说如果哪天新增加了墨西哥菜,泰国菜,日本料理等,是不是只需要实现ICooking接口就可以了?
可能这个例子还不能对“拆”这个行为提供足够的支持,因为好处不够明显,但别忘了五个原则是互相关联和支持的,个原则是互相关联和支持的,原则是互相关联和支持的,是互相关联和支持的,互相关联和支持的,相关联和支持的,关联和支持的,联和支持的,相和支持的,支持的,持的,的...
上面的示例中,实现了单一功能原则(S)的同时,也实现了开闭原则(O),接口隔离原则(I)。

依赖反转原则(D)

请看如下代码示例:
// 厨师 - 依赖反转示例
public class Cooker
{
//这里只需要ICooking接口对象,不关心接口实现
public void Cook(ICooking cook)
{
cook.Cook();
}
}

// 食客 - 消费者
public class Diner
{
public static void main(string[] args)
{
Cooker cooker = new Cooker();
ICooking chineseCook = new ChineseCook(); //做中餐实例
ICooking westernCook = new WesternCook(); //做西餐实例
// 或许哪天增加日本料理
// ICooking japaneseCook = new JapaneseCook(); //日本料理实例

cooker.Cook(chineseCook); // 做中餐
cooker.Cook(westernCook); // 做西餐
cooker.Cook(japaneseCook); // 日本料理
}
}

代码说明:

以上示例,在ICooking接口的两个实现的基础上,实现了依赖反转/倒置(DIP)
我们来看一下定义:
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口
在此示例中:
高层次的模块 Cooker 不依赖于 ChineseCook 或 WesternCook, 而是依赖于 ICooking 接口
ICooking接口不依赖于具体的实现。而 ChineseCook 和 WesternCook 则依赖于 ICooking 接口
如果新增加了墨西哥菜,日本料理,只需要实现接口ICooking即可(开闭原则:面向修改关闭,面向扩展开放)

依赖倒置原则基于这样一个事实:
相对于实现的多变性,抽象的东西(接口)要稳定的多。以抽象为基础搭建起来的架构要比以实现为基础搭建起来的架构要稳定的多。

在不同的语言领域中,抽象和实现可能有着不同的实现。不一而论。在C#或者Java中,抽象指的是接口或者抽象类,实现则是具体的实现类。

依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

出自设计模式六大原则(3):依赖倒置原则

里氏替换原则(L)

定义:

派生类(子类)对象能够替换其基类(超类)对象被使用。

出自里氏替换原则
这个概念比较好理解,结合上面的示例,如果我们把接口替换成抽象类,该抽象类实现了默认的Cook方法(默认做一份老北京鸡肉卷):
// 父类,实现了一个默认的Cook方法
public class Cooking
{
public virtual Cook()
{
Console.WriteLine("开始做老北京鸡肉卷");
Console.WriteLine("老北京鸡肉卷做完了,快来啃吧!");
}
}

//做中餐(重载Cook方法)
public class ChineseCook: Cooking
{
public override bool Cook()
{
Console.WriteLine("开始做鱼香肉丝")
Console.WriteLine("鱼香肉丝做好了,快来米西吧")
}
}

//做西餐(重载Cook方法)
public class WesternCook: Cooking
{
public override bool Cook()
{
Console.WriteLine("开始做蔬菜沙拉")
Console.WriteLine("蔬菜沙拉做好了, 快来eat吧")
}
}

代码说明:
上例中,父类Cooking实现了Cook的默认方法(做一份老北京鸡肉卷),所以如果不继承该父类的话,服务员端上来的就是鸡肉卷,很显然,该类可以被子类替换。

补充Java基本类型转换规则:
1、基本数据类型的转换是指由系统根据转换规则自动完成,不需要程序员明确地声明不同数据类型之间的转换。转换在编译器执行,而不是等到运行期再执行。 

2、基本数据类型的转换在赋值、方法调用和算术运算三种情况下都会发生。在进行方法调用时,数据类型指调用方法向被调用方法传递参数,即实参和型参类型不一致,从而发生了类型转换。 

3、赋值和方法调用的基本数据类型转换规则一样。合法的基本类型转换原则是指从取值范围窄的类型向取值范围宽的类型转换,如果是从取值范围宽的类型向取值范围窄的类型转换,则会产生编译错误。 

4、具体规则: 

(1)布尔型和其它基本数据类型之间不能相互转换; 

(2)byte型可以转换为short、int、、long、float和double; 

(3)short可转换为int、long、float和double; 

(4)char可转换为int、long、float和double; 

(5)int可转换为long、float和double; 

(6)long可转换为float和double; 

(7)float可转换为double; 

也就是说,只能有取值窄的范围向宽范围转换,反之则不行。 

5、Java中无后缀数字型,文字型共有两种默认类型,无小数点的整数型文字值、默认类型为整型int,带有小数点的浮点数型文字值,默认类型为双精度double。 

6、在赋值语句中,默认类型为整型的无小数点整数型文字值作为右操作数时,可以赋值给取值范围比整型小的变量,前提是文字值对于的实际数值在变量类型的取值范围内。而默认类型为双精度的带有小数点的浮点数型文字值只能赋值给双精度型变量,不能赋值给单精度型变量。 

7、基本数据类型的转换在算术运算情况下,正对单操作数运算符和双操作数运算符的转换规则是不一样的。 

但操作数运算符算术运算时基本转换规则如下: 

(1)当运算符为取正运算符(+)。取负运算符(-)或按位取反运算符(~)时,如果操作数为byte、char或short,则先被转换为int,再参与运算。 

(2)当运算符为自动递增运算符(++)或自动递减运算符(--)时,如果操作数为byte,short或char,则不用先被转换为int,而是直接参与算术运算,且运算结果类型也不变。 

(3)如果操作数为int或long,则无论运算符为何种单操作数运算符,均不发生类型转换,且运算结果类型也不变。 

双操作数运算符算术运算时基本转换规则如下: 

(1)如操作数之一为double,则另一个操作数先被转化为double,再参与算术运算。 

(2)如两操作数均不为double,当操作数之一为float,则另一操作数先被转换为float,再参与运算。 

(3)如两操作数均不为double或float,当操作数之一为long,、则另一操作数先被转换为long,再参与算术运算。 

(4)如两操作数均不为double、float或long,则两操作数先被转换为int,再参与运算。 

(5)如采用+=、*=等缩略形式的运算符,系统会自动强制将运算结果转换为目标变量的类型。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐