添加新字段(and/or 方法)是否会破坏 OCP(开闭原则)?

Does adding new fields (and/or methods) break OCP (open closed principle)?

假设我有一个 XML 文件结构要导入数据库:

<Flight>
    <FlightName>FN 7777</FlightName>
    <Passengers>
        <American>
            <FirstName>Michael</FirstName>
            <LastName>Smith</LastName>
        </American>
        <American>
            <FirstName>Jack</FirstName>
            <LastName>Brown</LastName>
        </American>
        <German>
            <FirstName>Hans</FirstName>
            <LastName>Schaefer</LastName>
        </German>
        <Ukranian>
            <FirstName>Sergei</FirstName>
            <LastName>Osipenko</LastName>
            <CanSpeakRussian>true</CanSpeakRussian>
        </Ukranian>
    </Passengers>
</Flight>

根据最初的要求,我创建了这个 class 结构:

public class Flight
{
    public Flight()
    {
        Passengers = new List<IPassenger>();
    }
    public string FlightNr { get; set; }
    public List<IPassenger> Passengers { get; set; }
    public void SomeMethod()
    {
         ...
    }

}

public interface IPassenger
{
    string FirstName { get; set; }
    string LastName { get; set; }
}

public class German : IPassenger
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class American : IPassenger
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Ukranian : IPassenger
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public bool CanSpeakRussian { get; set; }
}

在项目的第二个版本中,我们有 3 个新需求:

  1. 航班必须有一个目的地节点
  2. 所有乘客都必须有护照号码
  3. 所有美国乘客必须有 SSN(社会安全号码)

这些需求的XML结构是:

<Flight>
    <FlightName>FN 7777</FlightName>
    <Destination>Chicago</Destination>
    <Passengers>
        <American>
            <FirstName>Michael</FirstName>
            <LastName>Smith</LastName>
            <PassportNr>US123456</PassportNr>
            <SSN>123-45-6789</SSN>
        </American>
        <American>
            <FirstName>Jack</FirstName>
            <LastName>Brown</LastName>
            <PassportNr>US556699</PassportNr>
            <SSN>345-12-9876</SSN>
        </American>
        <German>
            <FirstName>Hans</FirstName>
            <LastName>Schaefer</LastName>
            <PassportNr>DE112233</PassportNr>
        </German>
        <Ukranian>
            <FirstName>Sergei</FirstName>
            <LastName>Osipenko</LastName>
            <CanSpeakRussian>true</CanSpeakRussian>
            <PassportNr>UK447788</PassportNr>
        </Ukranian>
    </Passengers>
</Flight>

问题一: 如果我更改代码结构如下,是否会破坏 SOLID 的开闭原则?

public class Flight
{
    public Flight()
    {
        Passengers = new List<IPassenger>();
    }
    public string FlightNr { get; set; }
    public string Destination { get; set; }
    public List<IPassenger> Passengers { get; set; }
}

public interface IPassenger
{
    string FirstName { get; set; }
    string LastName { get; set; }
    string PassportNr { get; set; }
}

public class German : IPassenger
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string PassportNr { get; set; }
}

public class American : IPassenger
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string PassportNr { get; set; }
    public string SSN { get; set; }
}

public class Ukranian : IPassenger
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public bool CanSpeakRussian { get; set; }
    public string PassportNr { get; set; }
}

问题2: 你认为我应该为这个结构使用 abstract class 以获得更好更短的代码吗?如果是这样,是否对可测试性有任何负面影响?

public class Flight
{
    public Flight()
    {
        Passengers = new List<Passenger>();
    }
    public string FlightNr { get; set; }
    public string Destination { get; set; }
    public List<Passenger> Passengers { get; set; }

}

public abstract class Passenger
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string PassportNr { get; set; }
}

public class German : Passenger
{
}

public class American : Passenger
{
    public string SSN { get; set; }
}

public class Ukranian : Passenger
{
    public bool CanSpeakRussian { get; set; }
}

引用 Robert Martin 的敏捷原则、模式和实践(必读):

Modules that conform to OCP have two primary attributes.

They are open for extension. This means that the behavior of the module can be extended. As the requirements of the application change, we can extend the module with new behaviors that satisfy those changes. In other words, we are able to change what the module does.

They are closed for modification. Extending the behavior of a module does not result in changes to the source, or binary, code of the module. The binary executable version of the module—whether in a linkable library, a DLL, or a .EXE file—remains untouched.

(请注意,当它说“模块”时,它并不一定意味着某种集合,还包括更小规模的东西,例如 classes 和编译单元。)

这里重要的是它是关于行为的。你所拥有的是一个数据结构的层次结构,或多或少没有任何行为。因此,就目前而言,很难说您的代码是否违反了 OCP,因为它并不完全适用。

违反 OCP 的行为通常伴随着一些基于类型的开关或条件。 Flight class 的任何行为是否取决于它拥有的乘客类型?如果是这样,它可以有不同的形状,例如:

if (passenger is Kefiristani) {
    performSuperStrictSecurityChecks(passenger);
}

或者将这段丑陋的代码移动到Passengerclass的performSecurityChecks方法中,然后在Flightclass中做[=19] =]

passenger.performSecurityChecks(); // non-virtual call

或者以 OOP 的方式来做,欢呼多态!

passenger.performSecurityChecks(); // virtual call

现在假设出现了一位新的 class 乘客 Tumbombalooni,这也需要超级严格的安全检查。在前两种情况下,您必须更改新 class 之外的一些代码,这正是它们 not closed 的意思。 在最后一种情况下,您无需更改任何内容。

第一个例子违反了所有事情的 SRP。它也可能在程序集级别违反 OCP,如果您的 classes 不是内部的,并且您的程序集 外部 的某个人可能会扩展它们,现在他们必须去改变你的使其工作的代码。如果它们是内部的,那么它是否违反OCP是值得商榷的,但违反SRP的情况更糟。

第二个例子肯定违反了OCP。扩展 class 不应强制派生 class 的作者更改基础 class 中的任何内容。事实上,他们可能做不到。

最后一个例子没有违反OCP。 class仍然是可扩展的,扩展不需要修改,只需要编写new代码。这就是 OCP 的意义所在。

回答您原来的问题:添加新字段(and/or 方法)会破坏 OCP 吗?不,它本身没有。但是,如果在派生 class 中添加或更改某些内容会迫使您对基础 class(es) 进行一些更改,那么 OCP 就会被破坏。

关于 OCP 的一件事是,几乎不可能永远不破坏它。对派生 class 的特定要求可能会迫使您更改基 class 中的某些内容。如果你提前计划好每一个可能的改变,你就会冒着过度设计一切的风险,然后一些你没有想到的事情来了,无论如何都会咬你一口。但是当它发生时,你可以用不同的方式处理它,最好只向基础 class 添加一个虚拟方法一次,然后在新的派生 classes 中重新实现这个方法,而不是下降一次又一次地陷入同一个陷阱,直到您的代码变得无法有效维护。