Question

The Open-Closed Principle states:

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

I'm designing a domain right now and including quite a bit of behavior in my domain entities. I'm using domain events and injecting dependencies into methods so make sure my entities aren't coupled to outside influences. However, it occurrs to me that, if the client wants more functionality later on, I will have to violate OCP and crack open these domain entities to add the features. How can a behavior-rich domain entity live in harmony with the Open-Closed Principle?

Was it helpful?

Solution

It's useful to keep the open-closed principle (OCP) in mind when designing classes, but it's not always practical or desirable to make classes "closed for modification" right away. I think the single responsibility principle (SRP) is much more useful in practice -- as long as a class only does one thing, it is okay to modify it if the requirements for that one thing change.

Moreover, SRP leads to OCP over time; if you find yourself changing a class often, you would eventually refactor it so that the changing portion is isolated in a separate class, thus making the original class more closed.

OTHER TIPS

The answer is simple: Factory methods and interfaces + composition.

Open for extension means that you add new functionality using new sub classes.

To enable that you have to use factories to create the domain objects. I typically use my repositories as factories.

If you code against interfaces instead of concretes you'll can add new functionality easily:

  • IUser
  • IManager : IUser (adds some manager features)
  • ISupervisor : IUser (adds supervisor features)

The features themselves can be small classes which you include using composition:

public class ManagerSupervisor : User, IManager, ISupervior
(
    public ManagerSupervisor()
    {
        // used to work with the supervisor features.
        // without breaking Law Of Demeter
        _supervisor = new SuperVisorFeatures(this);

    }
)

This is a tricky one to explain without some concrete examples. I recommend you to read Robert Martin's book "Agile Software Development, Principles, Patterns, and Practices". This book is also where the Open-Close principle comes from.

Domain objects with rich behaviour does not conflict with Open-close principle. If they have no behaviour, you can't create reasonable extension. The key to apply open-close principle is to anticipate future changes and create new interfaces to fulfil roles and keep them single responsibility.

I am going to tell a story of applying the open-close principle in real code. Hopefully it helps.

I had a Sender class that send messages at the begining:

package com.thinkinginobjects;

public class MessageSender {

private Transport transport;

public void send(Message message) {
    byte[] bytes = message.toBytes();
    transport.sendBytes(bytes);
}
}

One day, I was asked to send messages in a batch of 10. A simple solution would be:

package com.thinkinginobjects;

public class MessageSenderWithBatch {

private static final int BATCH_SIZE = 10;

private Transport transport;

private List<Message> buffer = new ArrayList<Message>();

public void send(Message message) {
    buffer.add(message);
    if (buffer.size() == BATCH_SIZE) {
        for (Message each : buffer) {
            byte[] bytes = each.toBytes();
            transport.sendBytes(bytes);
        }
                    buffer.clear();
    }
}
}

However my experience told me this is may not be the end of the story. I anticipated that people will require different way of batching the messages. Hence I created a batch strategy and made my Sender to use it. Note that I was applying Open-close principle here. If I had new kind of batching strategy in the future, my code is open for extension (by adding a new BatchStrategy), but close to modification (by not modifying any existing code). However as Robert Martin stated in his book, when the code is open for some types of changes, it is also close to other type of changes. If someone want to notify a component after sent in the future, my code is not open for this type of change.

package com.thinkinginobjects;

public class MessageSenderWithStrategy {

private Transport transport;

private BatchStrategy strategy;

public void send(Message message) {
    strategy.newMessage(message);
    List<Message> messages = strategy.getMessagesToSend();

    for (Message each : messages) {
        byte[] bytes = each.toBytes();
        transport.sendBytes(bytes);
    }
    strategy.sent();
}
}

package com.thinkinginobjects;

public class FixSizeBatchStrategy implements BatchStrategy {

private static final int BATCH_SIZE = 0;
private List<Message> buffer = new ArrayList<Message>();

@Override
public void newMessage(Message message) {
    buffer.add(message);    
}

@Override
public List<Message> getMessagesToSend() {
    if (buffer.size() == BATCH_SIZE) {
        return buffer;
    } else {
        return Collections.emptyList();
    }
}

@Override
public void sent() {
    buffer.clear(); 
}

}

Jus to complete the story, I received an requirement a couple of days later to send batched messages every 5 seconds. My guess was right and I can accommodate the requirement by adding extensions instead of modifying my code:

package com.thinkinginobjects;

public class FixIntervalBatchStrategy implements BatchStrategy {

private static final long INTERVAL = 5000;

private List<Message> buffer = new ArrayList<Message>();

private volatile boolean readyToSend;

public FixIntervalBatchStrategy() {
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    executorService.scheduleAtFixedRate(new Runnable() {

        @Override
        public void run() {
            readyToSend = true;

        }
    }, 0, INTERVAL, TimeUnit.MILLISECONDS);
}

@Override
public void newMessage(Message message) {
    buffer.add(message);
}

@Override
public List<Message> getMessagesToSend() {
    if (readyToSend) {
        return buffer;
    } else {
        return Collections.emptyList();
    }
}

@Override
public void sent() {
    readyToSend = false;
    buffer.clear();
}
}
  • Disclaimer: The code example belongs to www.thinkingInObjects.com

You are on the right track with domain event pattern.

You won't have to crack open your classes. You just need to append additional, conditional event handlers.

A domain event - event handler relationship can be one to many, and the handler can fire based on the the concrete types of the domain events' arguments.

The simple answer is by making sure:

  1. It does the task as well as it can thereby not requiring you to modify it later should you find problems with it. Although open/closed principle does not cover bugs only change.
  2. It encourages replacement, i.e. It implements a sensible interface. The component should be switched out for another one rather than modified.
  3. Making it configurable, any hard-coded parameters that could be changed should be made configurable.
  4. Externalize code subject to change, use inversion of control to export any functionality that may need to be changed.

It is often far easier to implement open/close principle in heavy lifting components but much harder to apply the same principles around application flow control and parts of the application where business logic often requires the greatest change. Having workflow run by configuration is the ideal.

Domain driven design can be hard to reconcile with good programming design principles, the key part of DDD is the language and the rationale to keep the low-level concepts away from the business domain and keep the business model high-level in the language that the users and the business use, aka the "Ubiquitous Language" to prevent software/technical pollution of the business model.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top