从广义上讲,所有编程语言都可以分为两种范式:
命令式/面向对象编程- 遵循逐行执行的指令序列。
声明式/ 函数式编程——不是顺序的,而是更关注程序的目的。整个程序就像一个函数,还有子函数,每个子函数执行特定的任务。
作为一名初级开发人员,我以一种艰难的方式(阅读:我紧张地盯着 1000 行代码)意识到这不仅仅是关于编写功能代码,而且还包括语义简单和灵活的代码。
虽然在这两种范例中都有多种编写干净代码的最佳实践,但我将讨论有关面向对象编程范例的 SOLID 设计原则。
S——单一职责
O——开闭原则
L — 里氏替换原则
I——接口隔离
D——依赖倒置
难以掌握这些概念的主要原因不是因为它们的技术深度深不可测,而是因为它们是抽象的准则,被概括为在面向对象编程中编写干净的代码。让我们看一些高级类图来深入了解这些概念。
这些不是准确的类图,而是帮助理解类中存在哪些方法的基本蓝图。
让我们在整篇文章中考虑一个咖啡馆的例子。
一个类应该只有一个改变的理由
考虑这个处理咖啡馆收到的在线订单的类。
它出什么问题了?
这个单一的类负责多个功能。如果您必须添加其他付款方式怎么办?如果您有多种发送确认的方式怎么办?在负责处理订单的类中更改支付逻辑不是很好的设计。它导致高度不灵活的代码。
更好的方法是将这些特定功能分离到具体类中并调用它们的实例,如下图所示。
实体应该对扩展开放,对修改关闭。
在咖啡厅,您必须从选项列表中为您的咖啡选择调味品,并且有一个类可以处理这个问题。
咖啡馆决定添加一种新的调味品,黄油。请注意价格将如何根据所选调味品而变化,价格计算逻辑在 Coffee 类中。我们不仅每次都必须添加一个新的调味品类,这可能会在主类中创建可能的代码更改,而且每次处理逻辑的方式也不同。
更好的方法是创建一个调味品接口,该接口可以反过来拥有覆盖其方法的子类。而主类可以只使用调味品接口传递参数并获取每个订单的数量和价格。
这有两个好处:
1. 您可以动态更改您的订单以使用不同甚至多种调味品(咖啡加摩卡和巧克力听起来很美妙)。
2. Condiments 类将与Coffee 类有has-a 关系,而不是is-a。所以你的咖啡可以有摩卡/黄油/牛奶,而不是你的咖啡是摩卡/黄油/牛奶咖啡。
每个子类或派生类都应该可以替代其基类或父类。
这意味着子类应该可以直接替换父类;它必须具有相同的功能。我发现很难理解这个,因为它听起来像是一些复杂的数学公式。但我会在这篇文章中尽量把它说清楚。
想想咖啡馆里的员工。有咖啡师、经理和服务员。它们都具有相似的功能。
因此,我们可以创建一个具有名称、位置、getName、getPostion、takeOrder()、serve() 的基础 Staff 类。
每个具体类 Waiter、Barista 和 Manager 都可以从中派生并覆盖相同的方法以根据职位需要实现它们。
在此示例中,通过确保 Staff 的任何派生类都可以与基 Staff 类互换使用而不影响代码的正确性,使用了Liskov 替换原则 (LSP) 。
例如,Waiter 类扩展了 Staff 类并覆盖了 takeOrder 和 serveOrder 方法以包含特定于服务员角色的附加功能。然而,更重要的是,尽管在功能上存在差异,但任何需要 Staff 类对象的代码也可以正确地使用 Waiter 类对象。
public class Cafe { public void serveCustomer (Staff staff) { staff.takeOrder(); staff.serveOrder(); } } public class Main { public static void main (String[] args) { Cafe cafe = new Cafe(); Staff staff1 = new Staff( "John" , "Staff" ); Waiter waiter1 = new Waiter( "Jane" ); restaurant.serveCustomer(staff1); // Works correctly with Staff object
restaurant.serveCustomer(waiter1); // Works correctly with Waiter object
} }
这里类 Cafe 中的方法 serveCustomer() 将 Staff 对象作为参数。 serveCustomer() 方法调用 Staff 对象的 takeOrder() 和 serveOrder() 方法来为客户提供服务。
在 Main 类中,我们创建了一个 Staff 对象和一个 Waiter 对象。然后,我们两次调用 Cafe 类的 serveCustomer() 方法 - 一次使用 Staff 对象,一次使用 Waiter 对象。
因为 Waiter 类派生自 Staff 类,所以任何需要 Staff 类对象的代码也可以正确地使用 Waiter 类对象。在这种情况下,Cafe 类的 serveCustomer() 方法可以与 Staff 对象和 Waiter 对象一起正常工作,即使 Waiter 对象具有特定于服务员角色的附加功能。
不应强迫类依赖于它们不使用的方法。
所以咖啡馆里就有了这个非常多功能的自动售货机,它可以提供咖啡、茶、零食和苏打水。
它有什么问题?技术上没什么。如果您必须为分配咖啡等任何功能实现接口,则还需要实现其他用于茶、苏打水和零食的方法。这是不必要的,并且这些功能与其他功能无关。这些功能中的每一个功能之间的内聚性都非常低。
什么是凝聚力?它是决定接口中方法相互关联程度的一个因素。
就自动售货机而言,这些方法几乎不相互依赖。我们可以隔离这些方法,因为它们的内聚性很低。
现在,任何旨在实现一件事的接口都必须只实现所有功能通用的 takeMoney() 。这将接口中不相关的功能分开,从而避免在接口中强行实现不相关的功能。
高级模块不应该依赖于低级模块。细节必须依赖于抽象。
考虑咖啡馆的空调(冷却器)。如果你像我一样,那里总是很冷。让我们看看遥控器和空调等级。
在这里,remoteControl 是依赖于低级组件 AC 的高级模块。如果我得到投票,我也想要一个加热器 :P 所以为了有一个通用的温度调节,而不是冷却器,让我们将远程和温度控制分开。但是remoteControl类和AC是紧耦合的,是具体的实现。为了解耦依赖性,我们可以创建一个接口,它的功能只有 increaseTemp() 和 decreaseTemp(),范围是 45-65 F。
如您所见,高级模块和低级模块都依赖于一个接口,该接口抽象了升高或降低温度的功能。
具体类 AC 实现了适用温度范围内的方法。
现在我可能可以获得我想要在称为加热器的不同具体类中实现不同温度范围的加热器。
高级模块 remoteControl 只需要担心在运行时调用正确的方法。