Do Java Sealed Classes Break the Open/Closed Principle (OCP) ?

Do Java Sealed Classes Break the Open/Closed Principle (OCP) ?

When Java rolled out sealed classes, many developers were intrigued. But with that intrigue came a few questions - one being, do sealed classes violate the Open/Closed Principle (OCP) from SOLID design principles?

After having several discussions with fellow developers on this topic, I've gained some fascinating insights. To explore this further, let's break it down with a practical example a banking system and consider how Domain-Driven Design (DDD) fits into the picture.

Setting the Scene: Banking System and Sealed Classes

Imagine you’re building a banking platform, and you’re dealing with the core concept of bank accounts. Specifically, let’s say you have a few basic account types: SavingsAccount, CheckingAccount, and FixedDepositAccount. These account types are well-established and regulated, meaning they don’t change much. You wouldn’t expect to randomly add a new account type without valid business reasons.

In Java, we can define these account types using sealed classes, like so:

public sealed class BankAccount permits SavingsAccount, CheckingAccount, FixedDepositAccount {}

public final class SavingsAccount extends BankAccount {}

public final class CheckingAccount extends BankAccount {}

public final class FixedDepositAccount extends BankAccount {}        

With this, we’ve created a controlled hierarchy where only these three account types are allowed to inherit from BankAccount. If someone tries to create a new subclass, the compiler will throw an error. Sounds like a great way to maintain control, right? But is this design a violation of OCP?


The Open/Closed Principle (OCP) and Why It Matters

If you’re familiar with the Open/Closed Principle (OCP), you know the rule:

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

This means that when you need to add new features or capabilities to a system, you should be able to extend the system without modifying existing code.

So, let’s say the bank wants to introduce a new account type, such as a LoanAccount. To add this, we’d have to modify the BankAccount class to include the new type in the permits clause:

public sealed class BankAccount permits SavingsAccount, CheckingAccount, FixedDepositAccount, LoanAccount {}        

By changing the base class to accommodate a new subclass, we’re clearly modifying existing code. That seems to contradict OCP, right?

But here’s the catch: whether or not this is a violation really depends on the context.


Sealed Classes and Domain-Driven Design (DDD)

In Domain-Driven Design (DDD), the goal is to model the system as closely as possible to the business domain. The system should reflect real-world concepts, and the design should respect the rules and constraints of the business.

In the case of our banking system, account types like SavingsAccount, CheckingAccount, and FixedDepositAccount are likely tightly regulated by business rules or regulatory bodies. For instance, there might be no room for a bank to arbitrarily allow new account types like CryptoAccount or StockAccount unless they undergo a formal approval process.

Here’s where sealed classes actually make a lot of sense.

1. Enforcing Business Constraints: By using a sealed class, we’re ensuring that only the types of accounts the business allows can exist in the system. This is a deliberate design choice to restrict inheritance, ensuring that developers can’t introduce unexpected or unapproved account types.

2. Aligning with DDD Principles: In DDD, the domain model should reflect the business’s actual rules. Sealed classes are a natural fit here because they enforce the business constraints at the code level. The account types are fixed, and the domain remains consistent with the rules of the business. So, when we use sealed classes, we're not violating OCP as much as we’re aligning our system with the business’s needs.


Sealed Classes vs. Open-Ended Design: A Flexible or Controlled Approach?

Now, if we were working on a system where account types should be flexible and open to extension, sealed classes might not be the best choice. For example, if the platform needed to support third-party integrations that could introduce new account types like CryptoAccount or InvestmentAccount then a more open design would be preferable.

In that case, using abstract classes or interfaces might be a better fit, like this:

public abstract class BankAccount {
    public abstract String getAccountType();
}        

With this design, developers (both internal and third-party) could freely create new account types without needing to modify the base class. This approach supports extensibility without forcing any changes to existing code, which is the essence of OCP.

But in a regulated banking system, where the types of accounts are strictly defined, sealed classes are actually a better fit because they help maintain the integrity of the system and prevent any unauthorized extensions.


Sealed Classes and DDD Building Blocks

In DDD, we often work with aggregates, which are domain objects that enclose a set of related entities. In the case of the BankAccount, you could view it as an aggregate root - the root of a set of related entities or values.

By using a sealed class for BankAccount, we create a clear boundary around the allowed account types. This ensures that only the valid account types can be part of the aggregate. Think of it as setting limits on what can exist within the business model - no rogue account types sneaking in.

This helps with maintaining data consistency and business logic integrity, which are core principles of DDD.


Pattern Matching and Sealed Classes: A Developer’s Dream

Another reason sealed classes are popular is their pattern matching capabilities in Java. Pattern matching is great for writing clear and concise logic, especially when working with different types of objects.

In our banking example, suppose we need to calculate interest for different types of accounts:

public double calculateInterest(BankAccount account) {
  return switch (account) {
  case SavingsAccount sa -> sa.getBalance() * 0.03;
  case FixedDepositAccount fda -> fda.getBalance() * 0.05;
  case CheckingAccount ca -> 0; // No interest for checking account 
};
}        

The best part about this is that Java ensures all cases are handled at compile-time. If a new account type is added and we forget to update the switch statement, the compiler will catch it, ensuring that we never miss a case. This kind of safety is invaluable, especially in a domain as sensitive as banking.


So, Do Sealed Classes Break OCP?

Here’s the conclusion: It depends on the context.

In a regulated domain like banking, sealed classes align well with the goals of Domain-Driven Design. They help enforce business rules, maintain a controlled domain model, and ensure that only valid account types are part of the system. From this perspective, they don’t violate OCP --they actually make the system more predictable and business-aligned.

However, if the system is meant to be more open-ended or supports a large number of third-party extensions, sealed classes may impose unnecessary restrictions. In those cases, it would be better to use a more flexible inheritance structure that allows for easy extension without modification.

So, sealed classes don’t inherently break OCP. They just reinterpret OCP for scenarios where business rules and regulatory compliance are paramount. Sealed classes provide safety and clarity in systems where the scope of changes needs to be tightly controlled. It’s a design decision that protects the integrity of the domain, and in many cases, that’s exactly what we want.


I’d be interested to hear your perspective on this.

要查看或添加评论,请登录

Kedar Patil的更多文章

社区洞察

其他会员也浏览了