Design an event processing system for a SCADA system that handles different types of events (data changes, alarms, operator actions, system events) using all four OOP pillars. Show how abstraction, encapsulation, inheritance, and polymorphism work together in your design.
Model Answer
// ========== ABSTRACTION ==========
// Abstract base event class
public abstract class SCADAEvent {
protected final String eventId;
protected final LocalDateTime timestamp;
protected final EventSeverity severity;
protected final String source;
public SCADAEvent(String source, EventSeverity severity) {
this.eventId = UUID.randomUUID().toString();
this.timestamp = LocalDateTime.now();
this.severity = severity;
this.source = source;
}
// Abstract methods defining the event interface
public abstract EventType getType();
public abstract String getDescription();
public abstract Map getEventData();
// Common functionality for all events
public boolean requiresImmediateProcessing() {
return severity == EventSeverity.CRITICAL || severity == EventSeverity.HIGH;
}
public String getEventSummary() {
return String.format("[%s] %s: %s", timestamp, source, getDescription());
}
}
// ========== ENCAPSULATION ==========
// Event data is encapsulated within each event type
public class DataChangeEvent extends SCADAEvent {
private final String dataPointId;
private final Object oldValue;
private final Object newValue;
private final DataQuality quality;
// Private constructor - use factory method
private DataChangeEvent(String source, String dataPointId,
Object oldValue, Object newValue, DataQuality quality) {
super(source, calculateSeverity(oldValue, newValue, quality));
this.dataPointId = dataPointId;
this.oldValue = oldValue;
this.newValue = newValue;
this.quality = quality;
}
// Factory method encapsulates construction logic
public static DataChangeEvent create(String source, String dataPointId,
Object oldValue, Object newValue, DataQuality quality) {
validateDataPoint(dataPointId);
validateValues(oldValue, newValue);
return new DataChangeEvent(source, dataPointId, oldValue, newValue, quality);
}
// Encapsulated severity calculation
private static EventSeverity calculateSeverity(Object oldValue, Object newValue, DataQuality quality) {
if (quality == DataQuality.BAD) return EventSeverity.HIGH;
if (isSignificantChange(oldValue, newValue)) return EventSeverity.MEDIUM;
return EventSeverity.LOW;
}
// Getters provide controlled access to encapsulated data
@Override
public Map getEventData() {
Map data = new HashMap<>();
data.put("dataPointId", dataPointId);
data.put("oldValue", oldValue);
data.put("newValue", newValue);
data.put("quality", quality.toString());
data.put("changePercentage", calculateChangePercentage(oldValue, newValue));
return data;
}
@Override
public String getDescription() {
return String.format("Data point %s changed from %s to %s",
dataPointId, oldValue, newValue);
}
@Override
public EventType getType() {
return EventType.DATA_CHANGE;
}
// Private helper methods - implementation details hidden
private static boolean isSignificantChange(Object oldValue, Object newValue) {
// Complex significance calculation encapsulated here
if (oldValue instanceof Number && newValue instanceof Number) {
double oldNum = ((Number) oldValue).doubleValue();
double newNum = ((Number) newValue).doubleValue();
return Math.abs((newNum - oldNum) / oldNum) > 0.05; // 5% change
}
return !Objects.equals(oldValue, newValue);
}
}
// ========== INHERITANCE ==========
// Hierarchy of alarm events
public abstract class AlarmEvent extends SCADAEvent {
protected final String alarmId;
protected final AlarmType alarmType;
protected final LocalDateTime acknowledgeTime;
protected final String acknowledgedBy;
public AlarmEvent(String source, String alarmId, AlarmType type, EventSeverity severity) {
super(source, severity);
this.alarmId = alarmId;
this.alarmType = type;
this.acknowledgeTime = null;
this.acknowledgedBy = null;
}
@Override
public EventType getType() {
return EventType.ALARM;
}
// Common alarm functionality
public boolean isAcknowledged() {
return acknowledgeTime != null;
}
public abstract String getAlarmMessage();
public abstract List getRecommendedActions();
}
// Inherited specialized alarm types
public class EquipmentAlarmEvent extends AlarmEvent {
private final EquipmentType equipmentType;
private final String equipmentId;
private final EquipmentFailure failureMode;
public EquipmentAlarmEvent(String source, String alarmId,
EquipmentType equipmentType, String equipmentId,
EquipmentFailure failureMode) {
super(source, alarmId, AlarmType.EQUIPMENT,
calculateSeverityFromFailure(failureMode));
this.equipmentType = equipmentType;
this.equipmentId = equipmentId;
this.failureMode = failureMode;
}
@Override
public String getDescription() {
return String.format("%s %s has failure: %s",
equipmentType, equipmentId, failureMode);
}
@Override
public String getAlarmMessage() {
return String.format("EQUIPMENT ALARM: %s %s - %s",
equipmentType, equipmentId, failureMode.getDescription());
}
@Override
public List getRecommendedActions() {
return failureMode.getRecommendedActions();
}
@Override
public Map getEventData() {
Map data = super.getEventData();
data.put("equipmentType", equipmentType);
data.put("equipmentId", equipmentId);
data.put("failureMode", failureMode);
data.put("requiresMaintenance", failureMode.requiresImmediateMaintenance());
return data;
}
private static EventSeverity calculateSeverityFromFailure(EquipmentFailure failure) {
switch (failure.getCriticality()) {
case CRITICAL: return EventSeverity.CRITICAL;
case HIGH: return EventSeverity.HIGH;
case MEDIUM: return EventSeverity.MEDIUM;
default: return EventSeverity.LOW;
}
}
}
public class CommunicationAlarmEvent extends AlarmEvent {
private final String deviceAddress;
private final CommunicationProtocol protocol;
private final Duration outageDuration;
public CommunicationAlarmEvent(String source, String alarmId,
String deviceAddress, CommunicationProtocol protocol,
Duration outageDuration) {
super(source, alarmId, AlarmType.COMMUNICATION,
calculateSeverityFromOutage(outageDuration));
this.deviceAddress = deviceAddress;
this.protocol = protocol;
this.outageDuration = outageDuration;
}
// Overridden methods with communication-specific implementations
@Override
public String getDescription() {
return String.format("Communication loss with %s (%s) for %d minutes",
deviceAddress, protocol, outageDuration.toMinutes());
}
// Inherits common alarm behavior but provides specific implementation
}
// ========== POLYMORPHISM ==========
// Event processor using polymorphism
public class EventProcessor {
private List handlers = new ArrayList<>();
private EventQueue eventQueue = new PriorityEventQueue();
// Polymorphic event processing
public void processEvent(SCADAEvent event) {
// Single interface, multiple implementations
logEvent(event);
// Different processing based on event type (polymorphism)
if (event.requiresImmediateProcessing()) {
processImmediately(event);
} else {
eventQueue.add(event);
}
// Notify all handlers - each handles events differently
for (EventHandler handler : handlers) {
// Polymorphic call - each handler decides how to handle the event
handler.handleEvent(event);
}
// Update statistics based on event type
updateStatistics(event.getType(), event.getSeverity());
}
// Polymorphic logging
private void logEvent(SCADAEvent event) {
// Different log formats based on event type
String logEntry = event.getEventSummary();
EventLogger.log(logEntry, event.getSeverity(), event.getEventData());
}
// Polymorphic immediate processing
private void processImmediately(SCADAEvent event) {
switch (event.getType()) {
case ALARM:
handleAlarmImmediately((AlarmEvent) event); // Polymorphic
break;
case DATA_CHANGE:
handleDataChangeImmediately((DataChangeEvent) event); // Polymorphic
break;
case OPERATOR_ACTION:
handleOperatorActionImmediately((OperatorActionEvent) event); // Polymorphic
break;
case SYSTEM_EVENT:
handleSystemEventImmediately((SystemEvent) event); // Polymorphic
break;
}
}
}
// Polymorphic event handlers
public interface EventHandler {
void handleEvent(SCADAEvent event);
}
public class AlarmNotificationHandler implements EventHandler {
@Override
public void handleEvent(SCADAEvent event) {
// Only handles alarm events
if (event.getType() == EventType.ALARM) {
AlarmEvent alarm = (AlarmEvent) event;
sendNotification(alarm.getAlarmMessage(), alarm.getSeverity());
// Different notification methods based on alarm type
if (alarm instanceof EquipmentAlarmEvent) {
notifyMaintenanceTeam((EquipmentAlarmEvent) alarm);
} else if (alarm instanceof CommunicationAlarmEvent) {
notifyCommunicationTeam((CommunicationAlarmEvent) alarm);
}
}
}
}
public class DatabaseStorageHandler implements EventHandler {
@Override
public void handleEvent(SCADAEvent event) {
// Stores all events, but in different tables/structures
String tableName = getTableForEventType(event.getType());
Map data = event.getEventData();
// Each event type provides different data through getEventData()
database.insert(tableName, data);
// Additional processing based on event type
if (event instanceof DataChangeEvent) {
updateTrendData((DataChangeEvent) event);
}
}
}
public class RealTimeUpdateHandler implements EventHandler {
@Override
public void handleEvent(SCADAEvent event) {
// Updates different parts of UI based on event type
switch (event.getType()) {
case DATA_CHANGE:
updateDataDisplay((DataChangeEvent) event);
break;
case ALARM:
updateAlarmDisplay((AlarmEvent) event);
break;
case OPERATOR_ACTION:
updateOperatorLog((OperatorActionEvent) event);
break;
}
}
}
// Event factory using polymorphism
public class EventFactory {
public static SCADAEvent createDataChange(String source, String dataPointId,
Object oldValue, Object newValue) {
return DataChangeEvent.create(source, dataPointId, oldValue, newValue, DataQuality.GOOD);
}
public static SCADAEvent createEquipmentAlarm(String source, String equipmentId,
EquipmentType type, EquipmentFailure failure) {
return new EquipmentAlarmEvent(source, generateAlarmId(), type, equipmentId, failure);
}
// More factory methods...
// Client code uses the factory, dealing only with SCADAEvent abstraction
public void processValueChange(String dataPointId, Object newValue) {
Object oldValue = getCurrentValue(dataPointId);
SCADAEvent event = EventFactory.createDataChange("RTU1", dataPointId, oldValue, newValue);
eventProcessor.processEvent(event); // Polymorphic processing
}
}
This design integrates all four OOP pillars:
- Abstraction:
SCADAEvent defines the essential interface for all events
- Encapsulation: Each event type encapsulates its specific data and construction logic
- Inheritance: Specialized event types inherit common functionality from base classes
- Polymorphism: Event processors and handlers work with the abstract
SCADAEvent interface, with different behavior for each concrete event type
Together, these pillars create a flexible, maintainable event processing system that can easily accommodate new event types and processing requirements.