开闭原则指出,软件实体(类、模块等)应该对扩展开放,但对修改关闭。这意味着什么,为什么它是良好面向对象设计的重要原则?
开闭原则指出,软件实体(类、模块等)应该对扩展开放,但对修改关闭。这意味着什么,为什么它是良好面向对象设计的重要原则?
这意味着你应该将新代码放到新的类/模块中。只有在修复错误时才应修改现有代码。 新类可以通过继承重用现有代码。
开闭原则旨在减轻引入新功能时的风险。由于不修改现有代码,因此可以确保它不会被破坏。 它降低了维护成本并增加了产品的稳定性。
具体而言,这是关于在面向对象编程(OOP)中设计的一种“圣杯”,即使实体的代码不需要重写(有时甚至无需重新编译),通过个体设计或参与架构支持未来无法预见的变化。
实现这种目标的方法包括多态/继承(Polymorphism/Inheritance)、组合(Composition)、控制反转(Inversion of Control,也称为DIP)、面向切面编程(Aspect-Oriented Programming)、模式,如策略模式(Strategy)、访问者模式(Visitor)、模板方法模式(Template Method)以及OOAD中的许多其他原则、模式和技术。
** 请参阅六个“包原则”:REP、CCP、CRP、ADP、SDP、SAP
比DaveK的更具体,通常意味着如果想要添加额外的功能或更改类的功能,则创建一个子类而不是更改原始类。这样,使用父类的任何人都不必担心以后的更改。基本上,它都是关于向后兼容性的。
另一个非常重要的面向对象设计原则是通过方法接口实现松散耦合。如果您要进行的更改不会影响现有接口,那么更改是相当安全的,例如使算法更加有效率。当然,面向对象的原则也需要适度运用常识 :)
Bike
和 Car
两个不同的类,因为维修 Bike
的策略与维修 Car
的策略不同。Garage
类接受各种类型的车辆进行维修。
僵化性问题
观察以下代码,看看当引入新功能时,Garage
类如何展现出僵化性的迹象:class Bike {
public void service() {
System.out.println("Bike servicing strategy performed.");
}
}
class Car {
public void service() {
System.out.println("Car servicing strategy performed.");
}
}
class Garage {
public void serviceBike(Bike bike) {
bike.service();
}
public void serviceCar(Car car) {
car.service();
}
}
Truck
或Bus
)时,就需要修改Garage
以定义一些新方法,例如serviceTruck()
和serviceBus()
。这意味着Garage
类必须知道所有可能的车辆类型,例如Bike
、Car
、Bus
、Truck
等等。因此,它违反了开闭原则,因为它对修改是开放的。同时,它也不支持扩展,因为要扩展新功能,我们需要修改类。
抽象
为解决上述代码中的刚性问题,我们可以使用开闭原则。这意味着我们需要剥离Garage
类的实现细节,使其变得简单。换句话说,我们应该为每种具体类型(如Bike
和Car
)抽象出服务策略的实现细节。
为了抽象各种类型车辆的服务策略的实现细节,我们使用一个名为Vehicle
的接口,并在其中有一个抽象方法service()
。
多态
同时,我们也希望Garage
类接受各种形式的车辆,例如Bus
、Truck
等,而不仅仅是Bike
和Car
。为此,开闭原则使用多态(多种形式)。
为了使Garage
类接受多种形式的Vehicle
,我们将其方法的签名更改为service(Vehicle vehicle) { }
以接受接口Vehicle
而不是实际实现,如Bike
、Car
等。我们还从该类中删除多个方法,因为一个方法将接受多种形式。
interface Vehicle {
void service();
}
class Bike implements Vehicle {
@Override
public void service() {
System.out.println("Bike servicing strategy performed.");
}
}
class Car implements Vehicle {
@Override
public void service() {
System.out.println("Car servicing strategy performed.");
}
}
class Garage {
public void service(Vehicle vehicle) {
vehicle.service();
}
}
对修改关闭
从上面的代码可以看到,现在Garage
类已经变成了对修改关闭。因为它不需要知道各种类型车辆维修策略的实现细节,只需将新的Vehicle
接口的派生类扩展并传递给Garage
就行了!我们不需要修改Garage
类里的任何代码。
另一个对修改关闭的实体是Vehicle
接口。我们不需要更改接口来扩展软件功能。
对扩展开放
Garage
类现在变成了对扩展开放状态,支持不同类型的Vehicle
,而无需进行修改。
我们的Vehicle
接口是对扩展开放的,因为我们可以通过扩展Vehicle
接口并提供适用于该特定车辆的服务策略的新实现来引入任何新车。
策略设计模式
您有没有注意到我多次使用“策略”这个词?这也是策略设计模式的一个例子。我们可以通过扩展Vehicle
接口来为不同类型的车辆实现不同的服务策略。例如,维修Truck
与维修Bus
有不同的策略。因此,我们在不同的派生类中实现这些策略。
策略模式允许我们的软件随着时间推移而变得灵活。每当客户端更改其策略时,只需为其派生一个新类并将其提供给现有组件即可,无需更改其他内容!开闭原则在实现此模式中扮演了重要角色。
就是这样!希望对您有所帮助。
开闭原则是面向对象编程中非常重要的一个原则,它是SOLID原则之一。
按照这个原则,一个类应该对扩展开放,对修改关闭。让我们理解一下为什么。
class Rectangle {
public int width;
public int lenth;
}
class Circle {
public int radius;
}
class AreaService {
public int areaForRectangle(Rectangle rectangle) {
return rectangle.width * rectangle.lenth;
}
public int areaForCircle(Circle circle) {
return (22 / 7) * circle.radius * circle.radius;
}
}
interface Shape{
int area();
}
class Rectangle implements Shape{
public int width;
public int lenth;
@Override
public int area() {
return lenth * width;
}
}
class Cirle implements Shape{
public int radius;
@Override
public int area() {
return (22/7) * radius * radius;
}
}
class AreaService {
int area(Shape shape) {
return shape.area();
}
}
软件实体应该对扩展开放,但对修改关闭
这意味着任何类或模块都应该以一种可用于现有情况、可以扩展,但永远不会修改的方式编写
var juiceTypes = ['Mango','Apple','Lemon'];
function juiceMaker(type){
if(juiceTypes.indexOf(type)!=-1)
console.log('Here is your juice, Have a nice day');
else
console.log('sorry, Error happned');
}
exports.makeJuice = juiceMaker;
现在,如果你想添加另一种果汁类型,你必须编辑模块本身,这样做会破坏开放封闭原则。
var juiceTypes = [];
function juiceMaker(type){
if(juiceTypes.indexOf(type)!=-1)
console.log('Here is your juice, Have a nice day');
else
console.log('sorry, Error happned');
}
function addType(typeName){
if(juiceTypes.indexOf(typeName)==-1)
juiceTypes.push(typeName);
}
function removeType(typeName){
let index = juiceTypes.indexOf(typeName)
if(index!==-1)
juiceTypes.splice(index,1);
}
exports.makeJuice = juiceMaker;
exports.addType = addType;
exports.removeType = removeType;
这是解决“脆弱基类问题”的答案,该问题指出对基类的看似无害的修改可能会对依赖于先前行为的继承者产生意想不到的后果。因此,您必须小心地封装您不希望被依赖的内容,以便派生类将遵守基类定义的契约。一旦继承者存在,您必须非常小心地更改基类中的内容。
开闭原则的基本思想是:当我们添加新功能时,尽量不要修改现有代码。这基本上意味着现有代码应该开放扩展,关闭修改(除非现有代码中存在错误)。如果在添加新功能时修改现有代码,则需要再次测试现有功能。
让我通过以AppLogger util类为例来解释这一点。
假设我们有一个需求,将应用程序范围内的错误记录到名为Firebase的在线工具中。因此,我们创建以下类并在1000处使用它,以记录API错误、内存不足等:
open class AppLogger {
open fun logError(message: String) {
// reporting error to Firebase
FirebaseAnalytics.logException(message)
}
}
fun logError(message: String, origin: String) {
if (origin == "Payment") {
//report to both Firebase and Instabug
FirebaseAnalytics.logException(message)
InstaBug.logException(message)
} else {
// otherwise report only to Firebase
FirebaseAnalytics.logException(message)
}
}
这种方法的问题在于它违反了单一职责原则,该原则规定一个方法只应该完成一项任务。换句话说,一个方法只应该有一个变化的原因。使用这种方法会有两个原因可能导致方法发生变化(if和else块)。
更好的方法是通过继承现有的Logger类来创建一个新的Logger类,如下所示。
class InstaBugLogger : AppLogger() {
override fun logError(message: String) {
super.logError(message) // This uses AppLogger.logError to report to Firebase.
InstaBug.logException(message) //Reporting to Instabug
}
}
遵循OCP的另一个经验法则是使基类针对派生类提供的功能具有抽象性。如Scott Meyers所说,'将非叶子类变为抽象类'。
这意味着在基类中有未实现的方法,并且只在没有子类的类中实现这些方法。这样,基类的客户端就不能依赖于基类中的特定实现,因为没有这种实现。
开放封闭原则是指在不改变已有的稳定和经过测试的功能的情况下,容易添加新功能,从而节省时间和金钱。通常,采用多态性,例如使用接口,是实现这一目标的好工具。