S.O.L.I.D、GRASP 和其他面向对象设计基本原则
学习 SOLID、GRASP 和其他核心面向对象设计 OOD 原则,以独立于语言的简单方式给其他开发人员留下深刻印象
引言
我将从一个陈词滥调开始。
软件代码应具备以下质量
- 可维护性
- 可扩展性
- 模块化
- 等等。
当你被问及任何特定代码是否具备上述质量特征时,你可能会发现自己处于困境。
一种有用的技术是查看任何软件的开发时间线。如果软件代码在其生命周期中易于维护、扩展和模块化,那么它就具备上述质量特征。
我曾编写过难以阅读、难以扩展和腐烂的软件代码。直到开发六个月后发生变化,我才意识到这一点。因此,开发时间线对于理解质量因素至关重要。
但是这种技术(查看开发时间线)只能通过回顾过去来应用,而我们希望在未来某个时候拥有高质量的软件。
了解质量的高级开发人员没有这个问题。当他们看到自己的代码具备初级开发人员梦寐以求的质量因素时,他们会感到自豪。
因此,高级开发人员和专家提炼出了一套原则,初级开发人员可以应用这些原则来编写高质量的代码,并在其他开发人员面前炫耀: )。如果你想编写自己的原则,这里有一个从自己的经验中提取原则的指南。
在这篇文章中,我将介绍 SOLID 原则。这些原则由 Uncle Bob (Robert C. Martin) 提出。我还将介绍 Craig Larman 发布的 GRASP (General Responsibility Assignment Software Principles) 和其他基本的面向对象设计原则。我包含了来自我个人经验的例子,因此你不会找到任何“动物”或“鸭子”的例子。
所示的代码示例更接近 Java 和 C#,但它们对任何了解面向对象编程基础知识的开发人员都有帮助。
以下是本文涵盖的原则的完整列表
- 单一职责原则 (SOLID)
- 高内聚 (GRASP)
- 低耦合 (GRASP)
- 开闭原则 (SOLID)
- 里氏替换原则 (SOLID)
- 接口隔离原则 (SOLID)
- 依赖倒置原则 (SOLID)
- 面向接口编程,而不是面向实现编程
- 好莱坞原则
- 多态性 (GRASP)
- 信息专家 (GRASP)
- 创建者 (GRASP)
- 纯虚构 (GRASP)
- 控制器 (GRASP)
- 优先使用组合而非继承
- 间接性 (GRASP)
- 不要重复自己
这篇文章很长,我希望你完整阅读。一种简单的方法是你可以收藏这篇文章(需要登录)。这样你就可以稍后继续阅读。此外,我已经制作了这篇文章的 PDF 版本,你可以从本文顶部下载。
单一职责原则
SRP 说
引用一个类应该只有一个职责。
一个类通过其函数或契约(以及数据成员辅助函数)来履行其职责。
以下面这个示例类为例
Class Simulation{
Public LoadSimulationFile()
Public Simulate()
Public ConvertParams()
}
这个类处理两个职责。首先,这个类加载模拟数据;其次,它执行模拟算法(使用 Simulate
和 ConvertParams
函数)。
一个类通过一个或多个函数来履行职责。在上面的示例中,加载模拟数据是一个职责,执行模拟是另一个职责。加载模拟数据需要一个函数(即 LoadSimulationFile
)。执行模拟需要其余两个函数。
我们怎么知道我的类有多少个职责?将“变更原因”类比为职责。因此,查找一个类所有可能发生变更的原因。如果一个类有多个变更原因,那么它就不遵循单一职责原则。
在上面的示例类中,这个类不应该包含 LoadSimulationFile
函数(或加载模拟数据职责)。如果我们为加载模拟数据创建一个单独的类,那么这个类就不会违反 SRP。
一个类只能有一个职责。你会如何设计一个遵循如此严格规则的软件?
让我们考虑另一个与 SRP 密切相关的原则,它被称为高内聚。高内聚给你一个主观的尺度,而不是像 SRP 那样的客观尺度。
非常低内聚意味着一个类承担了许多职责。例如,一个类负责超过 10 个职责。
低内聚意味着一个类承担大约 5 个职责,中等内聚意味着一个类承担 3 个职责。高内聚意味着一个类承担单一职责。
因此,经验法则是,在设计时,力求高内聚。
这里应该讨论的另一个原则是低耦合。该原则指出,应该分配职责,使类之间的依赖关系保持低。
再次考虑上面的示例类。在应用 SRP 和高内聚原则后,我们决定创建一个单独的类来处理模拟文件。这样,我们创建了两个相互依赖的类。
看起来应用高内聚会导致我们违反另一个原则,即低耦合。这种程度的耦合是允许的,因为目标是最小化耦合,而不是消除耦合。在对象协同完成任务的面向对象设计中,一定程度的耦合是正常的。
另一方面,考虑一个连接到数据库、通过 HTTP 处理远程客户端和处理屏幕布局的 GUI 类。这个 GUI 类依赖于太多的类。这个 GUI 类明显违反了低耦合原则。如果不涉及所有相关类,就无法重用这个 GUI 类。对数据库组件的任何更改都会导致 GUI 类的更改。
开闭原则
开闭原则说
引用一个软件模块(它可以是一个类或方法)应该对扩展开放,对修改关闭。
简单来说,你不能更新你已经为项目编写的代码,但你可以向项目添加新代码。
有两种方法可以应用开闭原则。你可以通过继承或通过组合来应用此原则。
这是使用继承应用开闭原则的示例
Class DataStream{
Public byte[] Read()
}
Class NetworkDataStream:DataStream{
Public byte[] Read(){
//Read from the network
}
}
Class Client {
Public void ReadData(DataStream ds){
ds.Read();
}
}
在此示例中,客户端从网络流读取数据(ds.Read()
)。如果我想扩展客户端类的功能以从另一个流(例如 PCI 数据流)读取数据,那么我将添加另一个 DataStream
类的子类,如下面所示
Class PCIDataStream:DataStream{
Public byte[] Read(){
//Read data from PCI
}
}
在这种情况下,客户端代码将正常运行,不会出现任何错误。客户端类知道基类,我可以传递 DataStream
的任何两个子类之一的对象。这样,客户端无需知道底层子类即可读取数据。这是在不修改任何现有代码的情况下实现的。
我们可以使用组合来应用此原则,还有其他方法和设计模式可以应用此原则。其中一些方法将在本文中讨论。
我们是否必须将此原则应用于我们编写的每一段代码?答案是否定的。这是因为大多数代码都不会改变。你必须在那些你怀疑将来会改变的代码段中战略性地应用此原则。
在前面的例子中,我从我的领域经验中得知会有不止一个流。因此,我应用了开闭原则,以便它可以在不修改的情况下处理未来的变化。
里氏替换原则
LSP 说
引用派生类必须可以替换它们的基类。
看待这个定义的另一种方式是,抽象(接口或abstract
类)对于客户端来说应该足够。
为了阐述,让我们考虑一个例子,下面是一个接口的列表
Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}
此代码表示数据采集设备抽象。数据采集设备根据其接口类型进行区分。数据采集设备可以使用 USB 接口、网络接口 (TCP 或 UDP)、PCI Express 接口或任何其他计算机接口。
IDevice
的客户端无需知道它们正在使用哪种设备。这为程序员提供了巨大的灵活性,可以在不更改依赖于 IDevice
接口的代码的情况下适应新设备。
让我们回顾一下历史,当时只有两个实现 IDevice
接口的具体类,如下所示
public class PCIDevice:IDevice {
public void Open(){
// Device specific opening logic
}
public void Read(){
// Reading logic specific to this device
}
public void Close(){
// Device specific closing logic.
}
}
public class NetWorkDevice:IDevice{
public void Open(){
// Device specific opening logic
}
public void Read(){
// Reading logic specific to this device
}
public void Close(){
// Device specific closing logic.
}
}
这三个方法(open
、read
和 close
)足以处理来自这些设备的数据。后来,需要添加另一个基于 USB 接口的数据采集设备。
USB 设备的问题在于,当你打开连接时,来自上一个连接的数据会留在缓冲区中。因此,在第一次调用 USB 设备的 read
时,返回的是上一会话的数据。这种行为损坏了该特定采集会话的数据。
幸运的是,基于 USB 的设备驱动程序提供了一个刷新功能,可以清除基于 USB 的采集设备中的缓冲区。我如何将此功能实现到我的代码中,以便代码更改保持最小?
一个天真的解决方案是通过识别你是否正在调用 USB
对象来更新代码
public class USBDevice:IDevice{
public void Open(){
// Device specific opening logic
}
public void Read(){
// Reading logic specific to this device<br>
}
public void Close(){
// Device specific closing logic.
}
public void Refresh(){
// specific only to USB interface Device
}
}
//Client code...
Public void Acquire(IDevice aDevice){
aDevice.Open();
// Identify if the object passed here is USBDevice class Object.
if(aDevice.GetType() == typeof(USBDevice)){
USBDevice aUsbDevice = (USBDevice) aDevice;
aUsbDevice.Refresh();
}
// remaining code….
}
在这个解决方案中,客户端代码直接使用具体类和接口(或抽象)。这意味着抽象对于客户端来说不够,无法履行其职责。
另一种说法是,基类无法履行所需的行为(刷新行为),但派生类具有此行为。因此派生类与基类不兼容,因此派生类无法替换。因此,此解决方案违反了里氏替换原则。
在上面的示例中,客户端依赖于更多实体(IDevice
和 USBDevice
),并且一个实体中的任何更改都会导致其他实体中的更改。因此,违反 LSP 会导致类之间的依赖。
一个遵循 LSP 的解决方案是什么?我这样更新了 Interface
Public Interface IDevice{
Void Open();
Void Refresh();
Void Read();
Void Close();
}
现在 IDevice
的客户端是
Public void Acquire(IDevice aDevice)
{
aDevice.open();
aDevice.refresh();
aDevice.acquire()
//Remaining code...
}
现在客户端不依赖于 IDevice
的具体实现。因此,在此解决方案中,我们的接口(IDevice
)对客户端来说是足够的。
从面向对象分析的角度来看,LSP 原则还有另一个角度。简而言之,在 OOA 期间,我们考虑了可能成为我们软件一部分的类及其层次结构。
当我们考虑类和层次结构时,我们可能会提出违反 LSP 的类。
让我们考虑矩形和正方形的经典例子,这个例子被多次错误引用。从一开始看,正方形似乎是矩形的专业化版本,一个愉快的设计师会画出以下继承层次结构。
Public class Rectangle{
Public void SetWidth(int width){}
Public void SetHeight(int height){}
}
Public Class Square:Rectangle{
//
}
接下来发生的是,你不能用 square
对象替换 rectangle
对象。由于 Square
继承自 Rectangle
,因此它继承了其方法 setWidth()
和 setHeight()
。Square
对象的客户端可以将其 width
和 height
更改为不同的维度。但 square
的 width
和 height
始终相同,因此软件的正常行为失败了。
只有通过根据不同的使用场景和条件来查看类,才能避免这种情况。因此,当你独立设计类时,你的假设可能会失败。就像 Square
和 Rectangle
的情况一样,is-a 关系在初始分析时看起来足够好,但当我们查看不同条件时,这种 is-a 关系未能实现软件的正确行为。
接口隔离原则
接口隔离原则 (ISP) 说
引用客户端不应该被迫依赖他们不使用的接口。
再次考虑前面的例子
Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}
有三个类实现了这个接口:USBDevice
、NetworkDevice
和 PCIDevice
。这个接口足以与网络和 PCI 设备配合使用。但是 USB 设备需要另一个函数(Refresh()
)才能正常工作。
类似于 USB 设备,将来可能会有其他设备也需要刷新功能才能正常工作。因此,IDevice
更新如下所示
Public Interface IDevice{
Void Open();
Void Refresh();
Void Read();
Void Close();
}
问题是现在每个实现 IDevice
的类都必须为 refresh
函数提供定义。
例如,我必须将以下代码行添加到 NetworkDevice
类和 PCIDevice
类才能使用此设计
public void Refresh()
{
// Yes nothing here… just a useless blank function
}
因此,IDevice
代表一个胖接口(函数过多)。这种设计违反了接口隔离原则,因为胖接口导致不必要的客户端依赖于它。
有很多方法可以解决这个问题,但我将使用我的领域特定知识来解决这个问题。
我知道 refresh
在 open
函数之后直接调用。因此,我将刷新逻辑从 IDevice
的客户端移动到特定的具体类。在我们的例子中,我将刷新逻辑的调用移动到 USBDevice
类,如下所示
Public Interface IDevice{
Void Open();
Void Read();
Void Close();
}
Public class USBDevice:IDevice{
Public void Open{
// open the device here…
// refresh the device
this.Refresh();
}
Private void Refresh(){
// make the USb Device Refresh
}
}
通过这种方式,我减少了 IDevice
类中的函数数量,使其不再臃肿。
依赖倒置原则
此原则是其他原则的概括。上面讨论的 LSP 和 OCP 原则替代了依赖倒置原则。
在跳到 DIP 的教科书定义之前,让我介绍一个与 DIP 密切相关的原则,它将有助于理解 DIP。
该原则是
引用“面向接口编程,而不是面向实现编程”
这个很简单。考虑下面的例子
Class PCIDevice{
Void open(){}
Void close(){}
}
Static void Main(){
PCIDevice aDevice = new PCIDevice();
aDevice.open();
//do some work
aDevice.close();
}
上面的例子违反了“面向接口编程原则”,因为我们正在使用具体类PCIDevice
的引用。下面的列表遵循这个原则
Interface IDevice{
Void open();
Void close();
}
Class PCIDevice implements IDevice{
Void open(){ // PCI device opening code }
Void close(){ // PCI Device closing code }
}
Static void Main(){
IDevice aDevice = new PCIDevice();
aDevice.open();
//do some work
aDevice.close();
}
因此,遵循这个原则非常容易。依赖倒置原则与此原则相似,但 DIP 要求我们再多一步。
DIP 说
引用高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
你很容易理解“两者都应该依赖于抽象”这句话,因为它表示每个模块都应该面向接口编程。但是高层模块和低层模块是什么呢?
要理解第一部分,我们必须了解什么是高层模块和低层模块?
请看下面的代码
Class TransferManager{
public void TransferData(USBExternalDevice usbExternalDeviceObj,SSDDrive ssdDriveObj){
Byte[] dataBytes = usbExternalDeviceObj.readData();
// work on dataBytes e.g compress, encrypt etc..
ssdDriveObj.WrtieData(dataBytes);
}
}
Class USBExternalDevice{
Public byte[] readData(){
}
}
Class SSDDrive{
Public void WriteData(byte[] data){
}
}
在这段代码中,有三个类。TransferManager
类代表一个高层模块。这是因为它在一个函数中使用了两个类。因此,其他两个类是低层模块。
高层模块函数 (TransferData
) 定义了数据如何从一个设备传输到另一个设备的逻辑。任何控制逻辑并在此过程中使用低层模块的模块都称为高层模块。
在上面的代码中,高层模块直接(没有任何抽象)使用低层模块,因此违反了依赖倒置原则。
违反此原则会导致软件难以更改。例如,如果要添加其他外部设备,则必须更改更高层模块。因此,你的更高层模块将依赖于更低层模块,这种依赖将使代码难以更改。
如果你理解了上面“面向接口编程”的原则,解决方案很简单。以下是清单
Class USBExternalDevice implements IExternalDevice{
Public byte[] readData(){
}
}
Class SSDDrive implements IInternalDevice{
Public void WriteData(byte[] data){
}
}
Class TransferManager implements ITransferManager{
public void Transfer(IExternalDevice externalDeviceObj, IInternalDevice internalDeviceObj){
Byte[] dataBytes = externalDeviceObj.readData();
// work on dataBytes e.g compress, encrypt etc..
internalDeviceObj.WrtieData(dataBytes);
}
}
Interface IExternalDevice{
Public byte[] readData();
}
Interface IInternalDevice{
Public void WriteData(byte[] data);
}
Interface ITransferManager {
public void Transfer(IExternalDevice usbExternalDeviceObj,SSDDrive IInternalDevice);
}
在上面的代码中,高层模块和低层模块都依赖于抽象。此代码遵循依赖倒置原则。
好莱坞原则
此原则与依赖倒置原则相似。此原则说
引用别给我们打电话,我们会给你打电话
这意味着高层组件可以以它们不相互依赖的方式指挥低层组件(或调用它们)。
此原则可以防御依赖腐烂。依赖腐烂发生在每个组件都依赖于所有其他组件时。换句话说,依赖腐烂是当依赖发生在每个方向(向上、横向、向下)时。好莱坞原则限制我们只在一个方向上建立依赖。
与依赖倒置原则的区别在于,DIP 给了我们一个通用指南:“高层和低层组件都应该依赖抽象,而不是具体类”。另一方面,好莱坞原则规定了高层组件和低层组件如何在不创建依赖的情况下进行交互。
多态
什么——多态性是一种设计原则?但我们已经了解到多态性是面向对象编程的基本特性。
是的,提供多态性特性是任何面向对象语言的基本要求,即派生类可以通过父类引用。
它也是 GRASP 中的一个设计原则。此原则提供了关于如何在面向对象设计中使用此 OOP 语言特性的指南。
此原则限制了运行时类型识别 (RTTI) 的使用。我们在 C# 中通过以下方式实现 RTTI
if(aDevice.GetType() == typeof(USBDevice)){
//This type is of USBDEvice
}
在 Java 中,RTTI 是通过使用函数 getClass()
或 instanceOf()
来实现的。
if(aDevice.getClass() == USBDevice.class){
// Implement USBDevice
Byte[] data = USBDeviceObj.ReadUART32();
}
如果您在项目中编写了此类代码,那么现在是重构该代码并使用多态性原则对其进行改进的时候了。
请看下面的图表
在这里,我在接口中通用化了 read
方法,并将设备特定的实现委托给它们的类(例如,USBDevice
中的 ReadUART32()
)。
现在我只使用 read
方法。
//RefactoreCode
IDevice aDevice = dm.getDeviceObject();
aDevice.Read();
getDeviceObject()
的实现将从何而来?我们将在创建者原则和信息专家原则中讨论这一点,在那里你将学习如何将职责分配给类。
信息专家
这是一个简单的 GRASP 原则,它提供了关于将职责分配给类的指导。
它说将职责分配给拥有履行该职责所需信息的类。
考虑以下类
在我们的场景中,模拟以全速(每秒 600 次循环)执行,而用户显示以降低的速度更新。在这里,我必须分配一个职责,是显示下一帧还是不显示。
哪个类应该处理这个职责?我有两个选择,要么是 simulation
类,要么是 SpeedControl
类。
现在 SpeedControl
类拥有关于当前序列中已显示哪些帧的信息,因此根据信息专家原则,SpeedControl
应该承担此职责。
创建者
创建者是一个 GRASP 原则,有助于决定哪个类应该负责创建类的新实例。
对象创建是一个重要的过程,有一个原则来决定谁应该创建类的实例是有用的。
根据 Larman 的说法,如果以下任何条件为 true
,则类“B
”应该被赋予创建另一个类“A
”的职责。
- B 包含 A
- B 聚合 A
- B 拥有 A 的初始化数据
- B 记录 A
- B 密切使用 A
在我们的多态性示例中,我使用了信息专家和创建者原则,将创建 Device
对象(dm.getDeviceObject()
)的职责赋予了 DeviceManager
类。这是因为 DeviceManager
拥有创建 Device
对象所需的信息。
纯虚构
为了理解纯虚构,前提是你必须理解面向对象分析 (OOA)。
简而言之,面向对象分析是一个过程,通过它你可以识别问题领域中的类。例如,银行系统的领域模型包含 Account
、Branch
、Cash
、Check
、Transaction
等类。
在银行示例中,领域类需要存储有关客户的信息。为了做到这一点,一个选项是将数据存储职责委托给领域类。此选项将降低领域类的内聚性(多于一个职责)。最终,此选项违反了 SRP 原则。
另一个选择是引入一个不代表任何领域概念的类。在银行示例中,我们可以引入一个类“PersistenceProvider
”。这个类不代表任何领域实体。这个类的目的是处理数据存储功能。因此,“PersistenceProvider
”是一个纯虚构。
控制器 (Controller)
当我开始开发时,我用 Java 的 Swing 组件编写了大部分程序,并且将大部分逻辑写在了监听器后面。
然后我了解了领域模型。因此,我将我的逻辑从监听器转移到领域模型。但我直接从监听器调用领域对象。这在 GUI 组件(监听器)和领域模型之间创建了依赖。控制器设计原则有助于最小化 GUI 组件和领域模型类之间的依赖。
控制器有两个目的。控制器的第一个目的是封装系统操作。系统操作是用户想要实现的目标,例如购买产品或将商品添加到购物车。然后通过在软件对象之间调用一个或多个方法调用来完成此系统操作。控制器的第二个目的是在 UI 和领域模型之间提供一个层。
UI 允许用户执行系统操作。控制器是 UI 层之后的第一个对象,它处理系统操作请求,然后将职责委托给底层领域对象。
例如,这是 MAP
类,它代表我们某个软件代码中的控制器。
从 UI,我们将“移动光标”的职责委托给此控制器,然后此控制器调用底层领域对象以移动光标。
通过使用控制器原则,你将能够灵活地插入另一个用户界面,例如命令行界面或 Web 界面。
优先使用组合而非继承
主要有两种面向对象编程工具可以扩展现有代码的功能。第一种是继承。
第二种方法是组合。在编程中,通过拥有对另一个对象的引用,你可以扩展该对象的功能。如果使用组合,添加一个新类,创建它的对象,然后使用它的引用来扩展代码。
组合的一个非常有用的特性是行为可以在运行时设置。另一方面,使用继承,你只能在编译时设置行为。这将在下面的示例中展示。
当我还是新手并使用继承来扩展行为时,我设计的类是这样的
最初,我只知道处理传入的数据流,并且有两种类型的数据(Stream A 和 Stream B)。几周后,我了解到应该处理数据的字节序。因此,我提出了如下所示的类设计
后来,需求中又添加了一个变量。这次,我必须处理数据的极性。想象一下我需要添加多少个类?对于 streamA
、streamB
、带字节序的 Stream
等等,有两种类型的极性。类的爆炸!现在我将不得不维护大量的类。
现在,如果我用组合来处理相同的问题,类设计将如下所示
我添加新类,然后通过它们的引用在我的代码中使用它们,请参阅下面的列表
clientData.setPolarity(new PolarityOfTypeA); // or clientData.setPolarity(new PolarityOfTypeB)
clientData.FormatPolarity;
clientData.setEndianness(new LittleEndiannes());// setting the behavior at run-time
clientData.FormatStream();
因此,我可以根据我想要的行为提供类的实例。此功能减少了类的总数,并最终解决了可维护性问题。因此,优先使用组合而不是继承将减少可维护性问题,并提供在运行时设置行为的灵活性。
间接性
这个原则回答了一个问题
如何让对象以它们之间的联系保持松散的方式进行交互?
解决方案是
将交互的职责赋予一个中间对象,以便不同组件之间的耦合保持低。
例如
一个软件应用程序使用不同的配置和选项。为了将领域代码与配置解耦,添加了一个特定类,如下面所示
Public Configuration{
public int GetFrameLength(){
// implementation
}
public string GetNextFileName(){
}
// Remaining configuration methods
}
通过这种方式,如果任何领域对象想要读取某个配置设置,它将询问 Configuration
类对象。因此,主代码与配置代码解耦。
如果你读过纯虚构原则,这个 Configuration
类就是一个纯虚构的例子。但间接的目的是创建解耦。另一方面,纯虚构的目的是保持领域模型清晰,只代表领域概念和职责。
许多软件设计模式,如适配器、外观和观察者,都是间接原则的特例。
不要重复自己 (DRY)
不要重复自己意味着不要尝试一遍又一遍地编写相同的代码。这个想法是,如果你一遍又一遍地编写几行代码,那么你应该将它们组合在一个函数中,然后调用该函数。
最大的好处是,现在如果你想更新那些特定的代码行,你只需在一个地方更新即可。否则,你将不得不搜索所有编写代码的地方。
我一直犹豫是否应用此原则。这是因为在旧的编程书中,我读到编写单独的函数会导致处理器额外工作。例如,当你调用一个函数时,汇编语言中总是有一个额外的调用,这称为“JUMP
”调用。
此 jump
调用会产生额外的执行成本。现在,如果函数在一个循环中执行 100 万次,那意味着你将有 100 万条额外的指令需要处理器执行。
嗯。代价高昂!!
这阻碍了我很长一段时间。对此也有解决方案。现在编译器已经非常优化,它们不会跳转到函数。相反,当你调用一个函数时,这些编译器只是用实际的代码行替换函数调用。因此,当处理器运行时,没有额外的“JUMP
”成本。
其他一切也都由编译器处理。所以尽管随意使用 DRY 原则,但请确保你的编译器足够聪明: )。
轮到你了
请在评论中告诉我,你在这篇文章中喜欢什么,以及你在学习和应用面向对象设计原则时遇到的最大挑战是什么。
我在 CodeProject 上还有一篇相关文章