Course program← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF
7Методические материалы

OCP – the Open–Closed Principle

This lesson was translated automatically – it may contain errors.

Software entities should be open for extension but closed for modification.

Behaviour should be extendable – through inheritance or composition, say – without climbing into existing code and rewriting it for the sake of new scenarios.

Why it matters

Working code has already been checked: it went through tests, review and production. Every edit in it is a risk of breaking what used to work. Adding a new class carries no such risk.

What a violation looks like

The customer type is checked in three different places:

// A new customer type means editing three working methods at once
public decimal Discount(string customerType, decimal amount)
{
    if (customerType == "premium") return amount * 0.1m;

    return 0m;
}

public int OrderLimit(string customerType) => customerType == "premium" ? 100 : 10;

public string WelcomeMail(string customerType) =>
    customerType == "premium" ? "Thank you for staying with us!" : "Welcome!";
// A new customer type means editing three working functions at once
double Discount(const std::string& customer_type, double amount) {
    return customer_type == "premium" ? amount * 0.1 : 0;
}

int OrderLimit(const std::string& customer_type) {
    return customer_type == "premium" ? 100 : 10;
}

std::string WelcomeMail(const std::string& customer_type) {
    return customer_type == "premium" ? "Thank you for staying with us!" : "Welcome!";
}
// A new customer type means editing three working functions at once
func Discount(customerType string, amount float64) float64 {
	if customerType == "premium" {
		return amount * 0.1
	}

	return 0
}

func OrderLimit(customerType string) int {
	if customerType == "premium" {
		return 100
	}

	return 10
}

func WelcomeMail(customerType string) string {
	if customerType == "premium" {
		return "Thank you for staying with us!"
	}

	return "Welcome!"
}
# A new customer type means editing three working functions at once
def discount(customer_type: str, amount: float) -> float:
    return amount * 0.1 if customer_type == "premium" else 0

def order_limit(customer_type: str) -> int:
    return 100 if customer_type == "premium" else 10

def welcome_mail(customer_type: str) -> str:
    return "Thank you for staying with us!" if customer_type == "premium" else "Welcome!"
// A new customer type means editing three working functions at once
function discount(customerType: string, amount: number): number {
    return customerType === 'premium' ? amount * 0.1 : 0;
}

function orderLimit(customerType: string): number {
    return customerType === 'premium' ? 100 : 10;
}

function welcomeMail(customerType: string): string {
    return customerType === 'premium' ? 'Thank you for staying with us!' : 'Welcome!';
}
// A new customer type means editing three working methods at once
public BigDecimal discount(String customerType, BigDecimal amount) {
    return customerType.equals("premium") ? amount.multiply(new BigDecimal("0.1")) : BigDecimal.ZERO;
}

public int orderLimit(String customerType) {
    return customerType.equals("premium") ? 100 : 10;
}

public String welcomeMail(String customerType) {
    return customerType.equals("premium") ? "Thank you for staying with us!" : "Welcome!";
}
// A new customer type means editing three working functions at once
fun discount(customerType: String, amount: BigDecimal): BigDecimal =
    if (customerType == "premium") amount * BigDecimal("0.1") else BigDecimal.ZERO

fun orderLimit(customerType: String) = if (customerType == "premium") 100 else 10

fun welcomeMail(customerType: String) =
    if (customerType == "premium") "Thank you for staying with us!" else "Welcome!"

Then sales come in: a third customer type is needed – a partner, with their own discount, their own limit and their own letter. Here is what is wrong with that:

  • we edit what works – three tested methods have to be opened and extended, and their tests rewritten along with them;
  • the compiler stays silent – it does not know there are three type checks; miss one and the partner quietly gets the limit of an ordinary customer, which surfaces in production;
  • the branches multiply – in a year there are seven types, and every one of them lives in seven different ifs;
  • two teams in one file – whoever adds the partner and whoever adds the corporate customer meet in a merge conflict.

How to open it for extension

The axis of change is visible here: the customer type changes. So that is what we make the extension point – one interface for all three decisions:

// A new customer type is a new class, the old ones stay untouched
public interface ICustomerPolicy
{
    decimal Discount(decimal amount);
    int OrderLimit { get; }
    string WelcomeMail();
}

public class RegularCustomer : ICustomerPolicy
{
    public decimal Discount(decimal amount) => 0m;
    public int OrderLimit => 10;
    public string WelcomeMail() => "Welcome!";
}

public class PremiumCustomer : ICustomerPolicy
{
    public decimal Discount(decimal amount) => amount * 0.1m;
    public int OrderLimit => 100;
    public string WelcomeMail() => "Thank you for staying with us!";
}

// The partner arrives in a separate file – not a single line above changes
public class PartnerCustomer : ICustomerPolicy
{
    public decimal Discount(decimal amount) => amount * 0.25m;
    public int OrderLimit => 1000;
    public string WelcomeMail() => "Glad to work together!";
}
// A new customer type is a new class, the old ones stay untouched
class ICustomerPolicy {
public:
    virtual double Discount(double amount) const = 0;
    virtual int OrderLimit() const = 0;
    virtual std::string WelcomeMail() const = 0;
    virtual ~ICustomerPolicy() = default;
};

class RegularCustomer : public ICustomerPolicy {
public:
    double Discount(double) const override { return 0; }
    int OrderLimit() const override { return 10; }
    std::string WelcomeMail() const override { return "Welcome!"; }
};

class PremiumCustomer : public ICustomerPolicy {
public:
    double Discount(double amount) const override { return amount * 0.1; }
    int OrderLimit() const override { return 100; }
    std::string WelcomeMail() const override { return "Thank you for staying with us!"; }
};

// The partner arrives in a separate file – not a single line above changes
class PartnerCustomer : public ICustomerPolicy {
public:
    double Discount(double amount) const override { return amount * 0.25; }
    int OrderLimit() const override { return 1000; }
    std::string WelcomeMail() const override { return "Glad to work together!"; }
};
// A new customer type is a new implementation of the interface, the old ones stay untouched
type CustomerPolicy interface {
	Discount(amount float64) float64
	OrderLimit() int
	WelcomeMail() string
}

type RegularCustomer struct{}

func (RegularCustomer) Discount(float64) float64 { return 0 }
func (RegularCustomer) OrderLimit() int          { return 10 }
func (RegularCustomer) WelcomeMail() string      { return "Welcome!" }

type PremiumCustomer struct{}

func (PremiumCustomer) Discount(amount float64) float64 { return amount * 0.1 }
func (PremiumCustomer) OrderLimit() int                 { return 100 }
func (PremiumCustomer) WelcomeMail() string             { return "Thank you for staying with us!" }

// The partner arrives in a separate file – not a single line above changes
type PartnerCustomer struct{}

func (PartnerCustomer) Discount(amount float64) float64 { return amount * 0.25 }
func (PartnerCustomer) OrderLimit() int                 { return 1000 }
func (PartnerCustomer) WelcomeMail() string             { return "Glad to work together!" }
# A new customer type is a new class, the old ones stay untouched
class CustomerPolicy(Protocol):
    def discount(self, amount: float) -> float: ...
    def order_limit(self) -> int: ...
    def welcome_mail(self) -> str: ...

class RegularCustomer:
    def discount(self, amount: float) -> float:
        return 0

    def order_limit(self) -> int:
        return 10

    def welcome_mail(self) -> str:
        return "Welcome!"

class PremiumCustomer:
    def discount(self, amount: float) -> float:
        return amount * 0.1

    def order_limit(self) -> int:
        return 100

    def welcome_mail(self) -> str:
        return "Thank you for staying with us!"

# The partner arrives in a separate file – not a single line above changes
class PartnerCustomer:
    def discount(self, amount: float) -> float:
        return amount * 0.25

    def order_limit(self) -> int:
        return 1000

    def welcome_mail(self) -> str:
        return "Glad to work together!"
// A new customer type is a new class, the old ones stay untouched
interface CustomerPolicy {
    discount(amount: number): number;
    orderLimit(): number;
    welcomeMail(): string;
}

class RegularCustomer implements CustomerPolicy {
    discount(): number {
        return 0;
    }
    orderLimit(): number {
        return 10;
    }
    welcomeMail(): string {
        return 'Welcome!';
    }
}

class PremiumCustomer implements CustomerPolicy {
    discount(amount: number): number {
        return amount * 0.1;
    }
    orderLimit(): number {
        return 100;
    }
    welcomeMail(): string {
        return 'Thank you for staying with us!';
    }
}

// The partner arrives in a separate file – not a single line above changes
class PartnerCustomer implements CustomerPolicy {
    discount(amount: number): number {
        return amount * 0.25;
    }
    orderLimit(): number {
        return 1000;
    }
    welcomeMail(): string {
        return 'Glad to work together!';
    }
}
// A new customer type is a new class, the old ones stay untouched
public interface CustomerPolicy {
    BigDecimal discount(BigDecimal amount);
    int orderLimit();
    String welcomeMail();
}

public class RegularCustomer implements CustomerPolicy {
    public BigDecimal discount(BigDecimal amount) { return BigDecimal.ZERO; }
    public int orderLimit() { return 10; }
    public String welcomeMail() { return "Welcome!"; }
}

public class PremiumCustomer implements CustomerPolicy {
    public BigDecimal discount(BigDecimal amount) { return amount.multiply(new BigDecimal("0.1")); }
    public int orderLimit() { return 100; }
    public String welcomeMail() { return "Thank you for staying with us!"; }
}

// The partner arrives in a separate file – not a single line above changes
public class PartnerCustomer implements CustomerPolicy {
    public BigDecimal discount(BigDecimal amount) { return amount.multiply(new BigDecimal("0.25")); }
    public int orderLimit() { return 1000; }
    public String welcomeMail() { return "Glad to work together!"; }
}
// A new customer type is a new implementation, the old ones stay untouched
interface CustomerPolicy {
    fun discount(amount: BigDecimal): BigDecimal
    val orderLimit: Int
    fun welcomeMail(): String
}

class RegularCustomer : CustomerPolicy {
    override fun discount(amount: BigDecimal): BigDecimal = BigDecimal.ZERO
    override val orderLimit = 10
    override fun welcomeMail() = "Welcome!"
}

class PremiumCustomer : CustomerPolicy {
    override fun discount(amount: BigDecimal) = amount * BigDecimal("0.1")
    override val orderLimit = 100
    override fun welcomeMail() = "Thank you for staying with us!"
}

// The partner arrives in a separate file – not a single line above changes
class PartnerCustomer : CustomerPolicy {
    override fun discount(amount: BigDecimal) = amount * BigDecimal("0.25")
    override val orderLimit = 1000
    override fun welcomeMail() = "Glad to work together!"
}

There are no type checks left: the calling code asks the policy for a discount and a limit without knowing whose policy it is. Adding a partner means adding a file, not opening three working ones.

What following it gives

  • Working code is not touched – so it is not broken. A regression cannot appear in a file nobody edited.
  • New functionality can be built in parallel. Two teams add two new discount types and never meet in one file.
  • A rollback is cheap. Removing new behaviour means deleting a class, not hunting the diff for lines added to a shared method.
  • Past checks stay valid. The tests of the old customer types are not rewritten, and they can be trusted after the release.

Where the principle turns into a problem

For an entity to be open for extension, the axis along which the changes will go has to be guessed in advance. Guess wrong and you get an abstraction that gets in the way – and you edit the existing code anyway. That is why OCP works well on the second or third similar task, not on the first.