Open closed principle is the most simple one in the list of software design principles I understand. "Open for extension, close for modification" - the idea seems quite straightforward. Let's create an example about validation.
We have a service which provides the feature to send message to recipients. The message's type could be SMS, Email, and Telegram, etc. Here is the structure of these message types.
Obviously, each message type has its own validation logic. SMSMessage must have valid recipient's phone number, EmailMessage must have valid receipient's email address, for example. We need a class to handle the validation and it might look like this.
@Autowired
@Qualifier("Email")
private MessageValidator emailValidator;
@Autowired
@Qualifier("SMS")
private MessageValidator smsValidator;
@Autowired
@Qualifier("Telegram")
private MessageValidator telegramValidator;
@Override
public void processMessage(Message msg) {
boolean isValid = false;
if (msg instanceof SMSMessage) {
isValid = smsValidator.validate(msg);
} else if (msg instanceof EmailMessage) {
isValid = emailValidator.validate(msg);
} else if (msg instanceof TelegramMessage) {
isValid = telegramValidator.validate(msg);
}
//do other processing
}
When a new message type comes in, you have to change this class again to support the new type. This approach violate the open closed principle.
To deal with this, we use the ServiceLocatorFactoryBean provided by Spring framework to support the new validation without changing the existing code, we only need to create more classes to handle the validation logic of the new message type.
As you can see the code fragment below has been incredibly shortened.
@Autowired
private MessageValidatorFactory msgValidatorFactory;
@Override
public void processMessage(Message msg) {
boolean isValid = msgValidatorFactory.getMsgValidator(msg.getType()).validate(msg);
//do other processing
}
public interface MessageValidatorFactory {
MessageValidator getMsgValidator(String msgType);
}
@Configuration
public class BeanConfigs {
@Bean("validatorFactory")
public FactoryBean serviceLocatorFactoryBean() {
ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
factoryBean.setServiceLocatorInterface(MessageValidatorFactory.class);
return factoryBean;
}
}
The configuration merely help to lookup the bean by beanId such as
Email
, SMS
, Telegram
@Component("Email")
public class EmailMessageValidator implements MessageValidator {
@Override
public boolean validate(Message m) {
//logic for validation
return false;
}
}
@Component("SMS")
public class SMSMessageValidator implements MessageValidator {
@Override
public boolean validate(Message m) {
//logic for validation
return false;
}
}
@Component("Telegram")
public class TelegramMessageValidator implements MessageValidator {
@Override
public boolean validate(Message m) {
//logic for validation
return false;
}
}
By this approach, your code will look much cleaner and become easier to maintain, mitigate the impact to the old features when implementing new feature.
Cheers