当前位置: 主页 > JAVA语言

java面向对象设计原则-教师资格证考试《单一责任原则》考前必看!

发布时间:2023-07-11 07:02   浏览次数:次   作者:佚名

单一责任原则

单一责任原则规定:

一个班级应该只有一个责任。

类使用其函数或契约(以及数据成员帮助函数)来履行其职责。

以下面的示例类为例:

ClassSimulation{

PublicLoadSimulationFile()

PublicSimulate()

PublicConvertParams()

}

这个类处理两个责任。首先,这个类正在加载模拟数据,其次,它正在执行模拟算法(使用Simulate和ConvertParams 职能)。

类使用一个或多个函数来履行职责。在上面的示例中,加载模拟数据是一项责任,而执行模拟则是另一项责任。加载模拟数据需要一个函数(即LoadSimationFile)。其余两个函数需要执行模拟。

我怎么知道我们班有多少责任?把“改变的理由”看作类似于责任。因此,寻找类更改的所有原因。如果有一个以上的原因改变一个类,那么这意味着这个类不遵循单一责任原则。

在上面的示例类中,这个类不应该包含LoadSimulationFile 函数(或加载模拟数据责任)。如果我们创建一个单独的类来加载模拟数据,那么这个类就不会违反SRP。

一个类只能有一个责任。你怎么能用这么严格的规则来设计软件呢?

让我们考虑另一个与SRP密切相关的原则:高内聚力。高内聚力给你一个主观的尺度,而不是一个客观的尺度,例如SRP。很低的凝聚力意味着一个班级正在履行许多责任。例如,一个类负责的责任超过10项。低衔接意味着一个班级大约完成5个责任,适度衔接意味着一个班级完成3个责任。高凝聚力意味着一个班级在履行一个单一的责任。因此,设计时的经验法则是争取高度的凝聚力。

这里应该讨论的另一个原则是低耦合。这一原则规定,人们应该分配责任,以使类之间的依赖性保持在较低水平。再次考虑上面的示例类。在应用SRP和高内聚原理后,我们决定建立一个单独的类来处理仿真文件。通过这种方式,我们创建了两个相互依赖的类。

似乎应用高内聚力使我们违背了低耦合的原则。这个级别的耦合被允许作为目标,以最小化耦合,但不允许零联轴器。在创建面向对象的设计时,某种程度的耦合是正常的,在这种设计中,任务是通过对象的协作来完成的。

另一方面,考虑一个GUI类,它连接到数据库,通过HTTP处理远程客户端,并处理屏幕布局。这个GUI类依赖于太多的类。这个GUI类显然违反了低耦合原则。如果不涉及所有相关类,则不能重用此类。对数据库组件的任何更改都会导致GUI类的更改。

开闭原理

“开放-封闭原则”规定:

软件模块(可以是类或方法)应该开放供扩展,但关闭以进行修改。

换句话说,您不能更新已经为项目编写的代码,但可以向项目添加新代码。

有两种方法可以应用开闭原理.您可以通过继承或组合应用此原则。

下面是使用继承应用开放关闭原则的示例:

ClassDataStream{

Publicbyte[]Read()

}

ClassNetworkDataStream:DataStream{

Publicbyte[]Read(){

//Read from the network

}

}

ClassClient{

PublicvoidReadData(DataStreamds){

ds.Read();

}

}

在本例中,客户端读取数据(ds.Read())来自网络流。如果我想扩展客户机类的功能以从另一个流(例如pci数据流)读取数据,那么我将加类的另一个子类。DataStream类,如下面的清单所示:

ClassPCIDataStream:DataStream{

Publcbyte[]Read(){

//Read data from PCI

}

}

在这种情况下,客户端代码将在没有任何错误的情况下工作。客户端类知道基类,并且我可以传递以下两个子类中任何一个的对象DataStream。这样,客户端就可以在不知道底层子类的情况下读取数据。这是在没有改性任何现存的密码。

面向对象设计原则的基本概念_java面向对象设计原则_java面向对象五大设计原则

我们可以使用组合来应用这个原则,还有其他的方法和设计模式来应用这个原则。本文将讨论这些方法中的一些。

您是否必须将此原则应用于您编写的每一段代码?没有。这是因为大部分代码不会改变。您只需要在那些您怀疑代码将来会发生变化的情况下,战略性地应用这个原则。

Liskov代换原理

Liskov替代原则规定:

派生类必须是它们的基类的替代类。

另一种看待这个定义的方法是抽象(接口或抽象类)应该是足够的为了一个客户。

为了详细说明,让我们考虑一个例子,下面是一个接口,其清单如下:

PublicInterfaceIDevice{

VoidOpen();

VoidRead();

VoidClose();

}

此代码表示数据采集设备的抽象。数据采集设备因其接口类型不同而不同。数据采集设备可以使用USB接口、网络接口(TCP或UDP)、PCI快速接口或任何其他计算机接口。然而,iDevice的客户端不需要知道他们使用的是哪种设备。这使程序员在不改变依赖iDevice接口的代码的情况下,能够灵活地适应新设备。

当只有两个实现iDevice接口的具体类时,让我们来了解一下历史,如下所示:

publicclassPCIDevice:IDevice{

publicvoidOpen(){

// Device specific opening logic

}

publicvoidRead(){

// Reading logic specific to this device

}

publicvoidClose(){

// Device specific closing logic.

}

}

publicclassNetWorkDevice:IDevice{

publicvoidOpen(){

// Device specific opening logic

}

publicvoidRead(){

// Reading logic specific to this device

}

publicvoidClose(){

// Device specific closing logic.

}

}

这三种方法(open,read,和close)足以处理来自这些设备的数据。后来,需要增加另一个基于USB接口的数据采集设备。

USB设备的问题是,当您打开连接时,来自前一个连接的数据保留在缓冲区中。因此,在对USB设备进行第一次读取调用时,返回来自上一会话的数据。该行为破坏了该特定获取会话的数据。

幸运的是,基于USB的设备驱动程序提供了刷新功能,以清除基于USB的采集设备中的缓冲区。如何将此特性实现到代码中,以使代码更改保持最小?

一个简单但不明智的解决方案是通过识别是否正在调用USB对象来更新代码:

publicclassUSBDevice:IDevice{

publicvoidOpen(){

// Device specific opening logic

}

publicvoidRead(){

java面向对象设计原则_面向对象设计原则的基本概念_java面向对象五大设计原则

// Reading logic specific to this device

}

publicvoidClose(){

// Device specific closing logic.

}

publicvoidRefresh(){

// specific only to USB interface Device

}

}

//Client code..

PublicvoidAcquire(IDeviceaDevice){

aDevice.Open();

// Identify if the object passed here is USBDevice class Object.

if(aDevice.GetType()==typeof(USBDevice)){

USBDeviceaUsbDevice=(USBDevice)aDevice;

aUsbDevice.Refresh();

}

// remaining code….

}

在这个解决方案中java面向对象设计原则,客户端代码直接使用具体的类以及接口(或抽象)。它的意思是抽象是不够让客户履行自己的职责。

另一种声明相同行为的方法是,基类不能满足所需的行为(刷新行为),但是派生类实际上具有这种行为。因此,派生类与基类不兼容,因此不能替换派生类。因此,这一解决方案违反了Liskov替代原则。

在上面的示例中,客户端依赖于更多的实体(iDevice和USB设备),一个实体中的任何更改都会导致其他实体中的更改。因此,违反LSP会导致类之间的依赖。

下面我给出一个解决这个问题的方法,这个问题遵循LSP:

PublicInterfaceIDevice{

VoidOpen();

VoidRefresh();

VoidRead();

VoidClose();

}

现在iDevice的客户端是:

PublicvoidAcquire(IDeviceaDevice)

{

aDevice.open();

aDevice.refresh();

aDevice.acquire()

//Remaining code..

}

现在,客户端不依赖于iDevice的具体实现。因此,在这个解决方案中,我们的接口(IDevice)是足够的为了客户。

在面向对象分析的背景下,还可以从另一个角度来看待LSP原则。得到一个介绍和一个简单的方法应用面向对象的分析方法读取此.总之,在OOA期间,我们考虑的类及其层次结构可能是我们软件的一部分。

当我们考虑到类和层次结构时,我们可以想出违反LSP的类。

让我们考虑矩形和正方形的经典例子。从一开始就看上去,这个方块是矩形的专门版本,一个愉快的设计人员将绘制以下继承层次结构:

PublicclassRectangle{

PublicvoidSetWidth(intwidth){}

PublicvoidSetHeight(intheight){}

}

面向对象设计原则的基本概念_java面向对象五大设计原则_java面向对象设计原则

PublicClassSquare:Rectangle{

//

}

接下来发生的事情是,您不能用正方形对象代替矩形对象。因为正方形是从矩形继承的,所以它继承了它的方法。setWidth()和setHeight()。方形对象的客户端可以将其宽度和高度更改为不同的尺寸。但是正方形的宽度和高度总是相同的,因此软件的正常行为是有缺陷的。

这只能通过根据不同的使用场景和条件查看类才能避免。因此,当您单独设计类时,您的假设可能会失败。与正方形和矩形的情况一样,在最初的分析中,关系看起来足够好,但是当我们观察不同的条件时,这种关系无法与软件的正确行为一起工作。

界面分离原理

接口隔离原则(ISP)规定:

不应强迫客户端依赖于它们不使用的接口。

再次考虑前面的示例:

PublicInterfaceIDevice{

VoidOpen();

VoidRead();

VoidClose();

}

有三个类实现了这个接口:USBDevice,NetworkDevice,和PCIDevice。这个接口足够好用于网络和PCI设备。但是USB设备需要另一个功能(Refresh())工作正常。

类似于USB设备,可能会有另一个设备需要刷新功能才能正常工作。因此,iDevice被更新如下:

PublicInterfaceIDevice{

VoidOpen();

VoidRefresh();

VoidRead();

VoidClose();

}

现在的问题是,实现iDevice的每个类都必须提供刷新函数的定义。

例如,我必须将以下代码行添加到NetworkDevice阶级和PCIDevice 类处理此设计:

publicvoidRefresh()

{

// Yes nothing here… just a useless blank function

}

因此,iDevice表示一个FAT接口(太多函数)。此设计违反了接口隔离原则,因为FAT接口导致不必要的客户端依赖它。

解决这个问题有很多种方法,但我将在我们预先定义的面向对象解决方案范围内解决这个问题。

我知道REFRESH是在OPEN函数之后直接调用的。因此,我将刷新逻辑从iDevice的客户端移到特定的具体类。在我们的例子中,我将对刷新逻辑的调用移到USBDevice 类,如下所示:

PublicInterfaceIDevice{

VoidOpen();

VoidRead();

VoidClose();

}

PublicclassUSBDevice:IDevice{

PublicvoidOpen{

// open the device here…

// refresh the device

this.Refresh();

}

PrivatevoidRefresh(){

// make the USb Device Refresh

}

面向对象设计原则的基本概念_java面向对象设计原则_java面向对象五大设计原则

}

通过这种方式,我减少了iDevice类中的函数数量,使其更少。

依赖反演原理(DIP)

这一原则是上述其他原则的概括。

在跳到教科书对DIP的定义之前,让我介绍一个密切相关的原理,它将有助于理解DIP。

原则是:

编程到一个接口,而不是一个实现。

这很简单。考虑以下示例:

ClassPCIDevice{

Voidopen(){}

Voidclose(){}

}

StaticvoidMain(){

PCIDeviceaDevice=newPCIDevice();

aDevice.open();

//do some work

aDevice.close();

}

上面的例子违反了“程序到接口”的原则,因为我们使用的是具体的PCI设备的引用。下面的清单遵循这个原则:

InterfaceIDevice{

Voidopen();

Voidclose();

}

ClassPCIDeviceimplementsIDevice{

Voidopen(){// PCI device opening code }

Voidclose(){// PCI Device closing code }

}

StaticvoidMain()

{

IDeviceaDevice=newPCIDevice();

aDevice.open();

//do some work

aDevice.close();

}

因此,遵循这一原则是非常容易的。依赖反演原理类似于这个原理,但是DIP要求我们再做一步。

迪普说:

高级模块不应依赖于低级别模块.两者都应依赖于抽象。

您可以很容易地理解这句话,正如它所说的,“两者都应该依赖于抽象”。每个模块都应该编程到一个接口。。但是什么是高级模块和低级模块呢?

为了理解第一部分,我们必须了解什么是高级模块和低级别模块。

请参阅下列代码:

ClassTransferManager{

publicvoidTransferData(USBExternalDeviceusbExternalDeviceObj,SSDDrivessdDriveObj){

Byte[]dataBytes=usbExternalDeviceObj.readData();

// work on dataBytes e.g compress, encrypt etc..

ssdDriveObj.WrtieData(dataBytes);

java面向对象设计原则_java面向对象五大设计原则_面向对象设计原则的基本概念

}

}

ClassUSBExternalDevice{

Publicbyte[]readData(){

}

}

ClassSSDDrive{

PublicvoidWriteData(byte[]data){

}

}

在这段代码中,有三个类。TransferManager类表示一个高级模块.这是因为它在其一个函数中使用两个类。因此,另外两个类是低级模块。

高级模块功能(TransferData)根据数据如何从一个设备传输到另一个设备来定义逻辑。任何控制逻辑的模块,并在此过程中使用低级模块,都称为高级模块。

在上面的代码中,高级模块直接(没有任何抽象)使用低级模块,因此违反了依赖反转原则。

违反这一原则使软件难以更改。例如,如果要添加其他外部设备,则必须更改较高级别的模块。因此,您的高级模块将依赖于较低级别的模块,而这种依赖将使代码难以更改。

如果您理解上面的原则:“编程到接口”,那么解决方案很容易。以下是清单:

ClassUSBExternalDeviceimplementsIExternalDevice{

Publicbyte[]readData(){

}

}

ClassSSDDriveimplementsIInternalDevice{

PublicvoidWriteData(byte[]data){

}

}

ClassTransferManagerimplementsITransferManager{

publicvoidTransfer(IExternalDeviceexternalDeviceObj,IInternalDeviceinternalDeviceObj){

Byte[]dataBytes=externalDeviceObj.readData();

// work on dataBytes e.g compress, encrypt etc..

internalDeviceObj.WrtieData(dataBytes);

}

}

InterfaceIExternalDevice{

Publicbyte[]readData();

}

InterfceIInternalDevice{

PublicvoidWriteData(byte[]data);

}

InterfaceITransferManager{

publicvoidTransfer(IExternalDeviceusbExternalDeviceObj,SSDDriveIInternalDevice);

}

在上面的代码中,高级模块和低级模块都依赖于抽象。此代码遵循依赖反转原则。

好莱坞原则

这一原理类似于依赖反演原理。这项原则规定:

别打电话给我们,我们会打给你的。

这意味着高级别组件可以以一种方式指定低级别组件(或调用它们),这样两个组件都不依赖于另一个组件。

这个原则有助于防止依赖腐烂。当每个组件依赖于每个其他组件时,就会发生依赖关系腐烂。换句话说java面向对象设计原则,依赖腐烂是当依赖发生在每个方向(向上,横向,向下)。好莱坞的原则只允许我们在一个方向上建立依赖关系。

依赖反转原则和好莱坞原则的区别在于,DIP给了我们一个总体指导:“高级和低级组件都应该依赖抽象,而不是具体的类。”另一方面,好莱坞原则规定了高层次组件和低层组件如何在不创建依赖关系的情况下进行交互。