Part E: Generating Medical Reports - Visitor Pattern
Document: Part E Analysis
Pattern: Visitor Pattern
Module: Medical Reports Generation System
Author: Ishara Lakshitha
Table of Contents
1. Overview & Problem Statement
Healthcare Domain Problems Addressed
Problem 1: Diverse Reporting Requirements
- Healthcare systems need multiple types of reports for different stakeholders
- Financial reports for billing departments and insurance companies
- Clinical reports for medical staff and patient care coordination
- Administrative reports for management and regulatory compliance
- Patient-specific reports for individualized care planning
Problem 2: Complex Data Aggregation
- Reports must combine data from multiple domain objects (Patients, Appointments, Bills)
- Different report types require different data processing algorithms
- Need to maintain data consistency across different report formats
- Complex calculations involving financial, clinical, and operational metrics
Problem 3: Extensible Report Framework
- New report types frequently required for changing business needs
- Regulatory requirements often demand new reporting formats
- Must support custom report generation without modifying existing domain objects
- Need to separate report generation logic from core business objects
Solution Approach
The Visitor Pattern provides a flexible reporting framework where:
- Domain Objects (PatientRecord, Appointment, MedicalBill) implement Visitable interface
- Report Visitors encapsulate specific report generation algorithms
- Data Processing is separated from domain object structure
- Extensibility allows adding new report types without modifying existing code
2. Visitor Pattern Implementation
2.1 Pattern Participants
Visitor Interface
/**
* The Visitor interface. It declares a set of visiting methods for each
* concrete Visitable element.
*/
public interface ReportVisitor {
void visit(PatientRecord patient);
void visit(Appointment appointment);
void visit(MedicalBill bill);
// Method to retrieve the final generated report
String getReport();
}
Element Interface
/**
* Defines the accept method that allows a visitor to perform an operation on an object.
* This is the "Element" interface in the Visitor pattern.
*/
public interface Visitable {
void accept(ReportVisitor visitor);
}
Concrete Elements
PatientRecord Implementation
public class PatientRecord implements Visitable {
private String patientId;
private String name;
private String medicalHistory;
private String treatmentPlans;
private InsurancePlan insurancePlan;
@Override
public void accept(ReportVisitor visitor) {
visitor.visit(this);
}
// Getters and other methods...
}
Appointment Implementation
public class Appointment implements Visitable {
private int appointmentId;
private String patientId;
private String doctorId;
private LocalDateTime appointmentDateTime;
private String reason;
private String status;
private String doctorNotes;
@Override
public void accept(ReportVisitor visitor) {
visitor.visit(this);
}
// Business methods and validation...
}
MedicalBill Implementation
public class MedicalBill implements Visitable {
private int billId;
private String patientId;
private String serviceDescription;
private double amount;
private double amountPaid;
private double insurancePaidAmount;
private String status;
@Override
public void accept(ReportVisitor visitor) {
visitor.visit(this);
}
// Financial calculation methods...
}
2.2 Concrete Visitor Implementations
Patient Summary Report Visitor
/**
* Patient Summary Report Visitor - Fixed to match actual database schema
* Provides comprehensive overview of patient's healthcare and financial information
*/
public class PatientSummaryReportVisitor implements ReportVisitor {
private final StringBuilder reportContent = new StringBuilder();
private final List<Appointment> appointments = new ArrayList<>();
private final List<MedicalBill> bills = new ArrayList<>();
private final Map<String, Integer> serviceCount = new HashMap<>();
private final Map<String, Integer> doctorVisits = new HashMap<>();
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private PatientRecord currentPatient;
private double totalBilled = 0;
private double totalCollected = 0;
private double totalOutstanding = 0;
@Override
public void visit(PatientRecord patient) {
this.currentPatient = patient;
reportContent.append(repeatString("=", 90)).append("\\n");
reportContent.append(" COMPREHENSIVE PATIENT SUMMARY REPORT\\n");
reportContent.append(repeatString("=", 90)).append("\\n");
reportContent.append("Patient: ").append(patient.getName())
.append(" (ID: ").append(patient.getPatientId()).append(")\\n");
reportContent.append(repeatString("=", 90)).append("\\n\\n");
}
@Override
public void visit(Appointment appointment) {
appointments.add(appointment);
// Track doctor visits
String doctorId = appointment.getDoctorId();
doctorVisits.put(doctorId, doctorVisits.getOrDefault(doctorId, 0) + 1);
}
@Override
public void visit(MedicalBill bill) {
bills.add(bill);
totalBilled += bill.getAmount();
totalCollected += bill.getTotalCollected();
totalOutstanding += bill.getRemainingBalance();
// Track service usage
String service = bill.getServiceDescription();
serviceCount.put(service, serviceCount.getOrDefault(service, 0) + 1);
}
@Override
public String getReport() {
generatePatientInformation();
generateHealthcareSummary();
generateFinancialSummary();
generateRecentActivity();
generateServiceUtilization();
generateDoctorRelationships();
generateHealthIndicators();
generateActionItems();
return reportContent.toString();
}
private void generatePatientInformation() {
reportContent.append("PATIENT INFORMATION\\n");
reportContent.append(repeatString("-", 50)).append("\\n");
reportContent.append("Name: ").append(currentPatient.getName()).append("\\n");
reportContent.append("Patient ID: ").append(currentPatient.getPatientId()).append("\\n");
reportContent.append("Medical History: ").append(currentPatient.getMedicalHistory()).append("\\n");
reportContent.append("Treatment Plans: ").append(currentPatient.getTreatmentPlans()).append("\\n");
if (currentPatient.getInsurancePlan() != null) {
reportContent.append("Insurance: ").append(currentPatient.getInsurancePlan().getPlanName())
.append(" (").append(currentPatient.getInsurancePlan().getCoveragePercent()).append("% coverage)\\n");
}
reportContent.append("\\n");
}
private void generateHealthcareSummary() {
reportContent.append("HEALTHCARE SUMMARY\\n");
reportContent.append(repeatString("-", 50)).append("\\n");
reportContent.append("Total Appointments: ").append(appointments.size()).append("\\n");
reportContent.append("Total Services: ").append(bills.size()).append("\\n");
reportContent.append("Unique Doctors Seen: ").append(doctorVisits.size()).append("\\n");
// Healthcare engagement indicator
if (appointments.size() > 20) {
reportContent.append("Healthcare Engagement: 🔴 High utilization\\n");
} else if (appointments.size() > 10) {
reportContent.append("Healthcare Engagement: 🟡 Regular patient\\n");
} else if (appointments.size() > 3) {
reportContent.append("Healthcare Engagement: 🟢 Moderate usage\\n");
} else {
reportContent.append("Healthcare Engagement: ⚪ Limited interaction\\n");
}
reportContent.append("\\n");
}
private void generateFinancialSummary() {
reportContent.append("FINANCIAL SUMMARY\\n");
reportContent.append(repeatString("-", 50)).append("\\n");
reportContent.append(String.format("Total Billed: $%.2f\\n", totalBilled));
reportContent.append(String.format("Total Collected: $%.2f\\n", totalCollected));
reportContent.append(String.format("Outstanding Balance: $%.2f\\n", totalOutstanding));
if (totalBilled > 0) {
double collectionRate = (totalCollected / totalBilled) * 100;
reportContent.append(String.format("Collection Rate: %.1f%%\\n", collectionRate));
}
reportContent.append("\\n");
}
// Additional helper methods for report generation...
private String repeatString(String str, int count) {
return str.repeat(Math.max(0, count));
}
}
Financial Report Visitor
/**
* Enhanced Financial Report Visitor - Fixed to match actual DB schema and MedicalBill class
*/
public class FinancialReportVisitor implements ReportVisitor {
private final StringBuilder reportContent = new StringBuilder();
private final List<MedicalBill> bills = new ArrayList<>();
private final Map<String, Double> serviceRevenue = new HashMap<>();
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private String patientName = "";
private String patientId = "";
private double totalBilled = 0;
private double totalPatientPaid = 0;
private double totalInsurancePaid = 0;
private double totalOutstanding = 0;
@Override
public void visit(PatientRecord patient) {
this.patientName = patient.getName();
this.patientId = patient.getPatientId();
reportContent.append(repeatString("=", 80)).append("\\n");
reportContent.append(" INDIVIDUAL PATIENT FINANCIAL SUMMARY\\n");
reportContent.append(repeatString("=", 80)).append("\\n");
reportContent.append("Patient: ").append(patient.getName())
.append(" (ID: ").append(patient.getPatientId()).append(")\\n");
reportContent.append("Generated: ").append(LocalDate.now().format(dateFormatter)).append("\\n");
reportContent.append(repeatString("=", 80)).append("\\n\\n");
}
@Override
public void visit(Appointment appointment) {
// Could track appointment revenue correlation if needed
}
@Override
public void visit(MedicalBill bill) {
bills.add(bill);
totalBilled += bill.getAmount();
totalPatientPaid += bill.getAmountPaid();
totalInsurancePaid += bill.getInsurancePaidAmount();
totalOutstanding += bill.getRemainingBalance();
// Track revenue by service type
String service = bill.getServiceDescription();
serviceRevenue.put(service, serviceRevenue.getOrDefault(service, 0.0) + bill.getAmount());
}
@Override
public String getReport() {
generateFinancialOverview();
generatePaymentBreakdown();
generateServiceRevenue();
generatePaymentStatus();
generateRecommendations();
return reportContent.toString();
}
private void generateFinancialOverview() {
reportContent.append("FINANCIAL OVERVIEW\\n");
reportContent.append(repeatString("-", 50)).append("\\n");
reportContent.append(String.format("Total Amount Billed: $%,.2f\\n", totalBilled));
reportContent.append(String.format("Patient Payments: $%,.2f\\n", totalPatientPaid));
reportContent.append(String.format("Insurance Payments: $%,.2f\\n", totalInsurancePaid));
reportContent.append(String.format("Total Collected: $%,.2f\\n", totalPatientPaid + totalInsurancePaid));
reportContent.append(String.format("Outstanding Balance: $%,.2f\\n", totalOutstanding));
reportContent.append("\\n");
}
// Additional financial analysis methods...
}
Service Utilization Report Visitor
/**
* Patient Service Utilization Visitor - Fixed to match actual database schema
* Analyzes patient's service usage patterns, appointments, and healthcare utilization
*/
public class PatientServiceUtilizationVisitor implements ReportVisitor {
private final StringBuilder reportContent = new StringBuilder();
private final Map<String, ServiceUtilization> serviceUsage = new HashMap<>();
private final Map<String, DoctorUtilization> doctorVisits = new HashMap<>();
private final List<Appointment> appointments = new ArrayList<>();
private final List<MedicalBill> bills = new ArrayList<>();
private String patientName = "";
private String patientId = "";
private double totalSpent = 0;
private double totalBilled = 0;
private int totalServices = 0;
@Override
public void visit(PatientRecord patient) {
this.patientName = patient.getName();
this.patientId = patient.getPatientId();
reportContent.append(repeatString("=", 90)).append("\\n");
reportContent.append(" PATIENT SERVICE UTILIZATION REPORT\\n");
reportContent.append(repeatString("=", 90)).append("\\n\\n");
}
@Override
public void visit(Appointment appointment) {
// Only process appointments for the current patient
if (appointment.getPatientId().equals(this.patientId)) {
appointments.add(appointment);
// Track doctor utilization
String doctorId = appointment.getDoctorId();
DoctorUtilization doctorUtil = doctorVisits.getOrDefault(doctorId,
new DoctorUtilization(doctorId));
doctorUtil.addAppointment(appointment);
doctorVisits.put(doctorId, doctorUtil);
}
}
@Override
public void visit(MedicalBill bill) {
// Only process bills for the current patient
if (bill.getPatientId().equals(this.patientId)) {
bills.add(bill);
totalServices++;
totalBilled += bill.getAmount();
totalSpent += bill.getTotalCollected();
String serviceName = bill.getServiceDescription();
ServiceUtilization utilization = serviceUsage.getOrDefault(serviceName,
new ServiceUtilization(serviceName));
utilization.addService(bill);
serviceUsage.put(serviceName, utilization);
}
}
@Override
public String getReport() {
generateUtilizationSummary();
generateAppointmentAnalysis();
generateServiceBreakdown();
generateDoctorUtilization();
generateUtilizationPatterns();
generateHealthcareValue();
generateRecommendations();
return reportContent.toString();
}
// Inner classes for data aggregation
private static class ServiceUtilization {
private final String serviceName;
private int usageCount = 0;
private double totalBilled = 0;
private double totalPaid = 0;
private double totalOutstanding = 0;
public ServiceUtilization(String serviceName) {
this.serviceName = serviceName;
}
public void addService(MedicalBill bill) {
usageCount++;
totalBilled += bill.getAmount();
totalPaid += bill.getTotalCollected();
totalOutstanding += bill.getRemainingBalance();
}
// Getters...
}
private static class DoctorUtilization {
private final String doctorId;
private final List<Appointment> appointments = new ArrayList<>();
private final Map<String, Integer> reasonCount = new HashMap<>();
private final Map<String, Integer> statusCount = new HashMap<>();
public DoctorUtilization(String doctorId) {
this.doctorId = doctorId;
}
public void addAppointment(Appointment appointment) {
appointments.add(appointment);
String reason = appointment.getReason();
if (reason != null && !reason.trim().isEmpty()) {
reasonCount.put(reason, reasonCount.getOrDefault(reason, 0) + 1);
}
String status = appointment.getStatus();
statusCount.put(status, statusCount.getOrDefault(status, 0) + 1);
}
// Getters and analysis methods...
}
}
3. UML Class Diagrams
3.0 Comprehensive Diagram

(You can find the High Res images in the Github Project Repo)
3.1 Visitor Pattern Structure
┌─────────────────────────────────────┐
│ <<interface>> │
│ ReportVisitor │
├─────────────────────────────────────┤
│ + visit(patient: PatientRecord) │
│ + visit(appointment: Appointment) │
│ + visit(bill: MedicalBill) │
│ + getReport(): String │
└─────────────────────────────────────┘
▲
┌───────────┼───────────┐
│ │ │
┌───────────────┐ ┌─────────────┐ ┌──────────────────┐
│PatientSummary │ │Financial │ │ServiceUtilization│
│ReportVisitor │ │ReportVisitor│ │Visitor │
├───────────────┤ ├─────────────┤ ├──────────────────┤
│- reportContent│ │- bills: List│ │- serviceUsage │
│- appointments │ │- totalBilled│ │- doctorVisits │
│- bills: List │ │- totalPaid │ │- appointments │
│- serviceCount │ └─────────────┘ └──────────────────┘
│- doctorVisits │
└───────────────┘
┌─────────────────────────────────────┐
│ <<interface>> │
│ Visitable │
├─────────────────────────────────────┤
│ + accept(visitor: ReportVisitor) │
└─────────────────────────────────────┘
▲
┌───────────┼───────────┐
│ │ │
┌───────────────┐ ┌─────────────┐ ┌──────────────┐
│PatientRecord │ │Appointment │ │MedicalBill │
├───────────────┤ ├─────────────┤ ├──────────────┤
│- patientId │ │- appointmentId│ │- billId │
│- name │ │- patientId │ │- patientId │
│- medicalHist. │ │- doctorId │ │- amount │
│- insurance │ │- dateTime │ │- amountPaid │
├───────────────┤ │- reason │ │- status │
│+ accept() │ │- status │ ├──────────────┤
└───────────────┘ ├─────────────┤ │+ accept() │
│+ accept() │ │+ getTotalColl│
└─────────────┘ │ ected() │
└──────────────┘
3.2 Report Generation Flow
ReportController
│
▼
generateReport(visitor)
│
├─→ patient.accept(visitor) ─→ visitor.visit(patient)
│ │
├─→ appointments.forEach(apt → │
│ apt.accept(visitor)) ─→ visitor.visit(appointment)
│ │
└─→ bills.forEach(bill → │
bill.accept(visitor)) ─→ visitor.visit(bill)
│
▼
visitor.getReport()
│
▼
Generated Report
4. Detailed Code Analysis
4.1 Report Controller Integration
/**
* ReportController manages report generation workflow
* Demonstrates how the Visitor pattern integrates with the controller layer
*/
public class ReportController {
private final ReportPanel view;
private final BillingDAO billingDAO;
private final SchedulingDAO schedulingDAO;
private final PatientDAO patientDAO;
private final IUser currentUser;
private PatientRecord currentPatient;
private void generatePatientSpecificReport(ReportVisitor visitor) {
processPatientData(visitor);
processPatientAppointments(visitor);
processPatientBills(visitor);
}
private void processPatientData(ReportVisitor visitor) {
// Process patient data
currentPatient.accept(visitor);
}
private void processPatientAppointments(ReportVisitor visitor) {
// Get appointments for the patient
List<Appointment> appointments = schedulingDAO.getAppointmentsByPatientId(
currentPatient.getPatientId());
for (Appointment appointment : appointments) {
appointment.accept(visitor);
}
}
private void processPatientBills(ReportVisitor visitor) {
// Get bills for the patient within date range
List<MedicalBill> bills = getBillsForPatientWithFilters(currentPatient.getPatientId());
for (MedicalBill bill : bills) {
bill.accept(visitor);
}
}
private void generatePatientSummaryReport() {
if (currentPatient == null) {
showError("Please select a patient first.");
return;
}
PatientSummaryReportVisitor visitor = new PatientSummaryReportVisitor();
generatePatientSpecificReport(visitor);
String report = visitor.getReport();
view.displayReport(report);
JOptionPane.showMessageDialog(view,
"Patient Summary Report generated successfully!",
"Report Generated", JOptionPane.INFORMATION_MESSAGE);
}
private void generateFinancialReport() {
if (currentPatient == null) {
showError("Please select a patient first.");
return;
}
FinancialReportVisitor visitor = new FinancialReportVisitor();
generatePatientSpecificReport(visitor);
String report = visitor.getReport();
view.displayReport(report);
JOptionPane.showMessageDialog(view,
"Financial Report generated successfully!",
"Report Generated", JOptionPane.INFORMATION_MESSAGE);
}
}
4.2 Complex Data Aggregation Examples
Patient Summary Report Data Processing
public class PatientSummaryReportVisitor implements ReportVisitor {
@Override
public void visit(PatientRecord patient) {
this.currentPatient = patient;
// Initialize report header with patient information
generateReportHeader(patient);
}
@Override
public void visit(Appointment appointment) {
appointments.add(appointment);
// Track doctor-patient relationships
String doctorId = appointment.getDoctorId();
doctorVisits.put(doctorId, doctorVisits.getOrDefault(doctorId, 0) + 1);
// Analyze appointment patterns
analyzeAppointmentPatterns(appointment);
}
@Override
public void visit(MedicalBill bill) {
bills.add(bill);
// Financial aggregation
totalBilled += bill.getAmount();
totalCollected += bill.getTotalCollected();
totalOutstanding += bill.getRemainingBalance();
// Service utilization tracking
String service = bill.getServiceDescription();
serviceCount.put(service, serviceCount.getOrDefault(service, 0) + 1);
// Payment analysis
analyzePaymentPatterns(bill);
}
private void generateHealthIndicators() {
reportContent.append("HEALTH INDICATORS\\n");
reportContent.append(repeatString("-", 50)).append("\\n");
// Service diversity analysis
if (serviceCount.size() > 10) {
reportContent.append("Service Diversity: 🔴 High - complex healthcare needs\\n");
} else if (serviceCount.size() > 5) {
reportContent.append("Service Diversity: 🟡 Moderate - varied healthcare needs\\n");
} else {
reportContent.append("Service Diversity: 🟢 Focused - specific healthcare needs\\n");
}
// Appointment compliance calculation
long completedAppointments = appointments.stream()
.filter(apt -> "Completed".equalsIgnoreCase(apt.getStatus()) ||
"Scheduled".equalsIgnoreCase(apt.getStatus()))
.count();
if (appointments.size() > 0) {
double complianceRate = (double) completedAppointments / appointments.size() * 100;
reportContent.append(String.format("Appointment Compliance: %.1f%%", complianceRate));
if (complianceRate >= 90) {
reportContent.append(" 🟢 Excellent\\n");
} else if (complianceRate >= 75) {
reportContent.append(" 🟡 Good\\n");
} else {
reportContent.append(" 🔴 Needs Improvement\\n");
}
}
}
}
Service Utilization Analysis
public class PatientServiceUtilizationVisitor implements ReportVisitor {
@Override
public void visit(MedicalBill bill) {
if (bill.getPatientId().equals(this.patientId)) {
bills.add(bill);
totalServices++;
totalBilled += bill.getAmount();
totalSpent += bill.getTotalCollected();
// Track service utilization patterns
String serviceName = bill.getServiceDescription();
ServiceUtilization utilization = serviceUsage.getOrDefault(serviceName,
new ServiceUtilization(serviceName));
utilization.addService(bill);
serviceUsage.put(serviceName, utilization);
}
}
private void generateServiceBreakdown() {
reportContent.append("SERVICE UTILIZATION BREAKDOWN\\n");
reportContent.append(repeatString("-", 60)).append("\\n");
reportContent.append(String.format("%-25s %8s %10s %10s %12s\\n",
"Service", "Count", "Billed", "Paid", "Outstanding"));
reportContent.append(repeatString("-", 60)).append("\\n");
serviceUsage.values().stream()
.sorted((a, b) -> Double.compare(b.getTotalBilled(), a.getTotalBilled()))
.forEach(service -> {
reportContent.append(String.format("%-25s %8d $%9.2f $%9.2f $%11.2f\\n",
truncateString(service.getServiceName(), 25),
service.getUsageCount(),
service.getTotalBilled(),
service.getTotalPaid(),
service.getTotalOutstanding()));
});
reportContent.append("\\n");
}
private void generateHealthcareValue() {
reportContent.append("HEALTHCARE VALUE ANALYSIS\\n");
reportContent.append(repeatString("-", 50)).append("\\n");
// Value indicators
if (appointments.size() > 10 && serviceUsage.size() > 5) {
reportContent.append("🟢 High healthcare engagement\\n");
} else if (appointments.size() > 5) {
reportContent.append("🟡 Regular healthcare usage\\n");
} else {
reportContent.append("🔴 Low healthcare utilization\\n");
}
// Cost efficiency analysis
double costPerAppointment = appointments.size() > 0 ? totalBilled / appointments.size() : 0;
if (costPerAppointment > 500) {
reportContent.append("Higher cost utilization pattern\\n");
} else if (costPerAppointment > 200) {
reportContent.append("Moderate cost utilization pattern\\n");
} else {
reportContent.append("Lower cost utilization pattern\\n");
}
}
}
4.3 Multi-Domain Data Correlation
/**
* Doctor Revenue Performance Visitor demonstrates complex multi-domain analysis
* Correlates appointment data with billing information for performance metrics
*/
public class DoctorRevenuePerformanceVisitor implements ReportVisitor {
@Override
public void visit(Appointment appointment) {
String doctorId = appointment.getDoctorId();
DoctorPerformance performance = doctorPerformance.getOrDefault(doctorId,
new DoctorPerformance(doctorId));
performance.addAppointment(appointment);
doctorPerformance.put(doctorId, performance);
totalAppointments++;
// Create mapping for later bill correlation
appointmentToBillMapping.put(appointment.getPatientId() + "_" +
appointment.getAppointmentDateTime().toLocalDate(), doctorId);
}
@Override
public void visit(MedicalBill bill) {
totalBills++;
totalSystemRevenue += bill.getAmount();
totalSystemCollected += bill.getTotalCollected();
// Find associated doctor through appointment correlation
String doctorId = findDoctorForBill(bill.getPatientId());
if (doctorId != null) {
DoctorPerformance performance = doctorPerformance.get(doctorId);
if (performance != null) {
performance.addRevenueBill(bill);
}
} else {
// Handle unassigned bills
DoctorPerformance unassigned = doctorPerformance.getOrDefault("UNASSIGNED",
new DoctorPerformance("UNASSIGNED"));
unassigned.addRevenueBill(bill);
doctorPerformance.put("UNASSIGNED", unassigned);
}
}
private String findDoctorForBill(String patientId) {
// Logic to correlate bills with appointments based on patient and timing
return appointmentToBillMapping.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(patientId))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
}
// Enhanced DoctorPerformance class with comprehensive metrics
private static class DoctorPerformance {
private final String doctorId;
private final List<Appointment> appointments = new ArrayList<>();
private final List<MedicalBill> bills = new ArrayList<>();
private final Map<String, Integer> appointmentStatusBreakdown = new HashMap<>();
private double totalBilled = 0;
private double totalPatientPaid = 0;
private double totalInsurancePaid = 0;
private double totalCollected = 0;
private double totalOutstanding = 0;
public void addAppointment(Appointment appointment) {
appointments.add(appointment);
String status = appointment.getStatus();
appointmentStatusBreakdown.put(status,
appointmentStatusBreakdown.getOrDefault(status, 0) + 1);
}
public void addRevenueBill(MedicalBill bill) {
bills.add(bill);
totalBilled += bill.getAmount();
totalPatientPaid += bill.getAmountPaid();
totalInsurancePaid += bill.getInsurancePaidAmount();
totalCollected = totalPatientPaid + totalInsurancePaid;
totalOutstanding += bill.getRemainingBalance();
}
// Performance calculation methods
public double getRevenuePerAppointment() {
return appointments.size() > 0 ? totalBilled / appointments.size() : 0;
}
public double getCollectionRate() {
return totalBilled > 0 ? (totalCollected / totalBilled) * 100 : 0;
}
}
}
5. Report Generation Workflows
5.1 Patient-Specific Report Generation
/**
* Complete workflow for generating patient-specific reports
*/
public void generatePatientReport(String reportType, String patientId) {
// Step 1: Retrieve patient data
PatientRecord patient = patientDAO.getPatientById(patientId);
if (patient == null) {
throw new IllegalArgumentException("Patient not found: " + patientId);
}
// Step 2: Create appropriate visitor
ReportVisitor visitor = createVisitor(reportType);
// Step 3: Process patient data
patient.accept(visitor);
// Step 4: Process related appointments
List<Appointment> appointments = schedulingDAO.getAppointmentsByPatientId(patientId);
appointments.forEach(appointment -> appointment.accept(visitor));
// Step 5: Process related bills
List<MedicalBill> bills = billingDAO.getBillsByPatientId(patientId);
bills.forEach(bill -> bill.accept(visitor));
// Step 6: Generate final report
String report = visitor.getReport();
// Step 7: Display or save report
displayReport(report, reportType);
}
private ReportVisitor createVisitor(String reportType) {
switch (reportType) {
case "PATIENT_SUMMARY":
return new PatientSummaryReportVisitor();
case "FINANCIAL":
return new FinancialReportVisitor();
case "SERVICE_UTILIZATION":
return new PatientServiceUtilizationVisitor();
case "PAYMENT_HISTORY":
return new PatientPaymentHistoryVisitor();
default:
throw new IllegalArgumentException("Unknown report type: " + reportType);
}
}
5.2 System-Wide Report Generation
/**
* Workflow for generating comprehensive system reports
*/
public void generateSystemWideReport(String reportType, LocalDate startDate, LocalDate endDate) {
ReportVisitor visitor = createSystemVisitor(reportType);
// Process all patients
List<PatientRecord> patients = patientDAO.getAllPatients();
patients.forEach(patient -> patient.accept(visitor));
// Process appointments within date range
List<Appointment> appointments = schedulingDAO.getAppointmentsByDateRange(startDate, endDate);
appointments.forEach(appointment -> appointment.accept(visitor));
// Process bills within date range
List<MedicalBill> bills = billingDAO.getBillsByDateRange(startDate, endDate);
bills.forEach(bill -> bill.accept(visitor));
// Generate comprehensive report
String report = visitor.getReport();
displayReport(report, reportType);
}
private ReportVisitor createSystemVisitor(String reportType) {
switch (reportType) {
case "COMPREHENSIVE_FINANCIAL":
return new ComprehensiveFinancialSummaryVisitor();
case "DOCTOR_PERFORMANCE":
return new DoctorRevenuePerformanceVisitor();
case "SYSTEM_UTILIZATION":
return new SystemUtilizationReportVisitor();
default:
throw new IllegalArgumentException("Unknown system report type: " + reportType);
}
}
5.3 Conditional Report Processing
/**
* Advanced report generation with conditional processing
*/
public class ConditionalReportVisitor implements ReportVisitor {
private final Predicate<PatientRecord> patientFilter;
private final Predicate<Appointment> appointmentFilter;
private final Predicate<MedicalBill> billFilter;
public ConditionalReportVisitor(Predicate<PatientRecord> patientFilter,
Predicate<Appointment> appointmentFilter,
Predicate<MedicalBill> billFilter) {
this.patientFilter = patientFilter;
this.appointmentFilter = appointmentFilter;
this.billFilter = billFilter;
}
@Override
public void visit(PatientRecord patient) {
if (patientFilter.test(patient)) {
// Process patient data
processPatientData(patient);
}
}
@Override
public void visit(Appointment appointment) {
if (appointmentFilter.test(appointment)) {
// Process appointment data
processAppointmentData(appointment);
}
}
@Override
public void visit(MedicalBill bill) {
if (billFilter.test(bill)) {
// Process bill data
processBillData(bill);
}
}
// Usage example:
public static ConditionalReportVisitor createHighValuePatientReport() {
return new ConditionalReportVisitor(
patient -> hasHighValueBilling(patient),
appointment -> appointment.getStatus().equals("Completed"),
bill -> bill.getAmount() > 1000.0
);
}
}
6. Usage Scenarios
6.1 Scenario 1: Comprehensive Patient Summary
// Generate complete patient summary for P001
String patientId = "P001";
PatientRecord patient = patientDAO.getPatientById(patientId);
// Create visitor for comprehensive summary
PatientSummaryReportVisitor visitor = new PatientSummaryReportVisitor();
// Process patient information
patient.accept(visitor);
// Process all patient appointments
List<Appointment> appointments = schedulingDAO.getAppointmentsByPatientId(patientId);
appointments.forEach(appointment -> appointment.accept(visitor));
// Process all patient bills
List<MedicalBill> bills = billingDAO.getBillsByPatientId(patientId);
bills.forEach(bill -> bill.accept(visitor));
// Generate final report
String report = visitor.getReport();
// Expected report sections:
// - Patient Information (demographics, insurance)
// - Healthcare Summary (appointments, engagement level)
// - Financial Summary (billing, payments, outstanding)
// - Service Utilization (most used services)
// - Doctor Relationships (frequency of visits)
// - Health Indicators (compliance, service diversity)
// - Action Items (recommendations)
6.2 Scenario 2: Financial Analysis Report
// Generate detailed financial report for patient
FinancialReportVisitor financialVisitor = new FinancialReportVisitor();
// Process the same data through financial lens
patient.accept(financialVisitor);
appointments.forEach(appointment -> appointment.accept(financialVisitor));
bills.forEach(bill -> bill.accept(financialVisitor));
String financialReport = financialVisitor.getReport();
// Expected financial analysis:
// - Total amounts (billed, collected, outstanding)
// - Payment breakdown (patient vs insurance)
// - Service revenue analysis
// - Payment status categorization
// - Collection rate calculations
// - Financial recommendations
6.3 Scenario 3: Multi-Patient Service Utilization
// Analyze service utilization across multiple patients
PatientServiceUtilizationVisitor utilizationVisitor = new PatientServiceUtilizationVisitor();
List<String> patientIds = Arrays.asList("P001", "P002", "P003");
for (String pid : patientIds) {
PatientRecord pt = patientDAO.getPatientById(pid);
pt.accept(utilizationVisitor);
// Process appointments and bills for each patient
schedulingDAO.getAppointmentsByPatientId(pid)
.forEach(apt -> apt.accept(utilizationVisitor));
billingDAO.getBillsByPatientId(pid)
.forEach(bill -> bill.accept(utilizationVisitor));
}
String utilizationReport = utilizationVisitor.getReport();
// Expected utilization analysis:
// - Service usage patterns per patient
// - Doctor utilization metrics
// - Cost per service analysis
// - Healthcare value indicators
// - Utilization recommendations
6.4 Scenario 4: Doctor Performance Analysis
// Generate doctor performance report across all patients
DoctorRevenuePerformanceVisitor performanceVisitor = new DoctorRevenuePerformanceVisitor();
// Process all patients, appointments, and bills
List<PatientRecord> allPatients = patientDAO.getAllPatients();
allPatients.forEach(patient -> patient.accept(performanceVisitor));
List<Appointment> allAppointments = schedulingDAO.getAllAppointments();
allAppointments.forEach(appointment -> appointment.accept(performanceVisitor));
List<MedicalBill> allBills = billingDAO.getAllBills();
allBills.forEach(bill -> bill.accept(performanceVisitor));
String performanceReport = performanceVisitor.getReport();
// Expected performance metrics:
// - Revenue per doctor
// - Appointment volume by doctor
// - Collection rates by doctor
// - Patient load analysis
// - Performance rankings
// - Revenue efficiency metrics
7. Benefits & Trade-offs
7.1 Visitor Pattern Benefits
Separation of Concerns
- Report generation logic completely separated from domain objects
- Domain objects remain focused on core business functionality
- Different report types can be developed independently
- Easy to modify report algorithms without affecting domain model
Extensibility
- New report types can be added without modifying existing domain classes
- Complex analytical reports can be implemented as new visitors
- Regulatory reporting requirements easily accommodated
- Custom reports for different stakeholders simple to implement
Code Reusability
- Same domain objects can be processed by multiple report visitors
- Common report utilities can be shared across visitor implementations
- Data aggregation patterns can be reused in different report contexts
- Report generation workflows are consistent and reusable
Type Safety
- Compile-time checking ensures all visitable types are handled
- Method overloading provides type-specific processing logic
- No need for runtime type checking or casting
- Clear contracts between visitors and domain objects
7.2 Healthcare Domain Specific Benefits
Regulatory Compliance
- Easy to implement new reporting requirements for changing regulations
- Audit trail capabilities built into report generation process
- Standardized report formats for regulatory submissions
- Historical reporting capabilities for compliance verification
Multi-Stakeholder Support
- Different report views for clinical staff, administrators, and patients
- Financial reports for billing departments and insurance companies
- Clinical reports for care coordination and quality improvement
- Administrative reports for operational management
Data Consistency
- Reports always based on current domain object state
- No data duplication between report storage and operational data
- Real-time report generation ensures data accuracy
- Consistent calculations across different report types
Performance Optimization
- Reports generated on-demand without pre-computation overhead
- Efficient data traversal patterns for large datasets
- Memory-efficient processing through visitor callbacks
- Scalable approach for enterprise healthcare systems
7.3 Trade-offs and Considerations
Complexity Management
- Large number of visitor classes for comprehensive reporting
- Complex interactions between different visitor implementations
- Need for careful design to avoid visitor explosion
- Testing complexity increases with number of visitors
Performance Considerations
- Method dispatch overhead through visitor interface
- Memory usage can grow with complex report data structures
- Large datasets may require streaming or pagination approaches
- Report generation time scales with data volume
Maintenance Overhead
- Adding new domain object types requires updating all visitors
- Interface changes affect all visitor implementations
- Version compatibility issues between visitors and domain objects
- Documentation critical for understanding visitor relationships
Learning Curve
- Visitor pattern can be difficult for developers to understand initially
- Complex report logic may be harder to debug across multiple methods
- Requires good understanding of double dispatch mechanism
- Testing strategies need to account for visitor pattern structure
8. Testing & Validation
8.1 Unit Testing Individual Visitors
@Test
public void testPatientSummaryReportVisitor() {
// Arrange
PatientSummaryReportVisitor visitor = new PatientSummaryReportVisitor();
PatientRecord patient = new PatientRecord("P001", "John Doe");
patient.setMedicalHistory("Diabetes, Hypertension");
Appointment appointment = new Appointment("P001", "D001",
LocalDateTime.now(), "Regular checkup");
appointment.setStatus("Completed");
MedicalBill bill = new MedicalBill("P001", "Consultation", 150.00);
bill.setAmountPaid(30.00);
bill.setInsurancePaidAmount(120.00);
// Act
patient.accept(visitor);
appointment.accept(visitor);
bill.accept(visitor);
String report = visitor.getReport();
// Assert
assertNotNull(report);
assertTrue(report.contains("John Doe"));
assertTrue(report.contains("P001"));
assertTrue(report.contains("Total Appointments: 1"));
assertTrue(report.contains("Total Billed: $150.00"));
assertTrue(report.contains("Total Collected: $150.00"));
assertTrue(report.contains("Outstanding Balance: $0.00"));
}
@Test
public void testFinancialReportVisitor() {
// Arrange
FinancialReportVisitor visitor = new FinancialReportVisitor();
PatientRecord patient = new PatientRecord("P002", "Jane Smith");
MedicalBill bill1 = new MedicalBill("P002", "Surgery", 5000.00);
bill1.setAmountPaid(1000.00);
bill1.setInsurancePaidAmount(3000.00);
MedicalBill bill2 = new MedicalBill("P002", "Follow-up", 200.00);
bill2.setAmountPaid(50.00);
bill2.setInsurancePaidAmount(150.00);
// Act
patient.accept(visitor);
bill1.accept(visitor);
bill2.accept(visitor);
String report = visitor.getReport();
// Assert
assertTrue(report.contains("Total Amount Billed: $5,200.00"));
assertTrue(report.contains("Patient Payments: $1,050.00"));
assertTrue(report.contains("Insurance Payments: $3,150.00"));
assertTrue(report.contains("Total Collected: $4,200.00"));
}
@Test
public void testServiceUtilizationVisitor() {
// Arrange
PatientServiceUtilizationVisitor visitor = new PatientServiceUtilizationVisitor();
PatientRecord patient = new PatientRecord("P003", "Bob Wilson");
// Multiple appointments with same doctor
Appointment apt1 = new Appointment("P003", "D001", LocalDateTime.now(), "Checkup");
Appointment apt2 = new Appointment("P003", "D001", LocalDateTime.now().plusWeeks(2), "Follow-up");
// Multiple services
MedicalBill bill1 = new MedicalBill("P003", "Consultation", 150.00);
MedicalBill bill2 = new MedicalBill("P003", "Consultation", 150.00);
MedicalBill bill3 = new MedicalBill("P003", "Lab Test", 75.00);
// Act
patient.accept(visitor);
apt1.accept(visitor);
apt2.accept(visitor);
bill1.accept(visitor);
bill2.accept(visitor);
bill3.accept(visitor);
String report = visitor.getReport();
// Assert
assertTrue(report.contains("Total Services: 3"));
assertTrue(report.contains("Consultation")); // Most used service
assertTrue(report.contains("D001")); // Doctor utilization
assertTrue(report.contains("Total Billed: $375.00"));
}
8.2 Integration Testing with Multiple Visitors
@Test
public void testMultipleVisitorsOnSameData() {
// Arrange - Common test data
PatientRecord patient = createTestPatient();
List<Appointment> appointments = createTestAppointments();
List<MedicalBill> bills = createTestBills();
// Create different visitors
PatientSummaryReportVisitor summaryVisitor = new PatientSummaryReportVisitor();
FinancialReportVisitor financialVisitor = new FinancialReportVisitor();
PatientServiceUtilizationVisitor utilizationVisitor = new PatientServiceUtilizationVisitor();
// Act - Process same data with different visitors
List<ReportVisitor> visitors = Arrays.asList(
summaryVisitor, financialVisitor, utilizationVisitor);
visitors.forEach(visitor -> {
patient.accept(visitor);
appointments.forEach(apt -> apt.accept(visitor));
bills.forEach(bill -> bill.accept(visitor));
});
// Assert - Different reports generated from same data
String summaryReport = summaryVisitor.getReport();
String financialReport = financialVisitor.getReport();
String utilizationReport = utilizationVisitor.getReport();
assertNotEquals(summaryReport, financialReport);
assertNotEquals(financialReport, utilizationReport);
// Verify each report contains appropriate content
assertTrue(summaryReport.contains("COMPREHENSIVE PATIENT SUMMARY"));
assertTrue(financialReport.contains("FINANCIAL SUMMARY"));
assertTrue(utilizationReport.contains("SERVICE UTILIZATION"));
}
8.3 Performance Testing
@Test
public void testLargeDatasetReportGeneration() {
// Arrange - Large dataset
List<PatientRecord> patients = createTestPatients(1000);
List<Appointment> appointments = createTestAppointments(5000);
List<MedicalBill> bills = createTestBills(3000);
PatientSummaryReportVisitor visitor = new PatientSummaryReportVisitor();
// Act - Measure performance
long startTime = System.currentTimeMillis();
patients.forEach(patient -> patient.accept(visitor));
appointments.forEach(appointment -> appointment.accept(visitor));
bills.forEach(bill -> bill.accept(visitor));
String report = visitor.getReport();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// Assert - Performance within acceptable limits
assertTrue("Report generation should complete within 5 seconds",
duration < 5000);
assertNotNull(report);
assertTrue("Report should contain substantial content",
report.length() > 1000);
}
@Test
public void testMemoryUsageWithLargeReports() {
// Test memory efficiency of visitor pattern
Runtime runtime = Runtime.getRuntime();
long initialMemory = runtime.totalMemory() - runtime.freeMemory();
// Generate large report
generateLargeReport();
long finalMemory = runtime.totalMemory() - runtime.freeMemory();
long memoryUsed = finalMemory - initialMemory;
// Assert memory usage is reasonable
assertTrue("Memory usage should be under 100MB",
memoryUsed < 100 * 1024 * 1024);
}
8.4 Error Handling Testing
@Test
public void testVisitorWithNullData() {
PatientSummaryReportVisitor visitor = new PatientSummaryReportVisitor();
// Test null patient
assertThrows(NullPointerException.class, () -> {
PatientRecord nullPatient = null;
nullPatient.accept(visitor);
});
// Test patient with null fields
PatientRecord incompletePatient = new PatientRecord(null, "Test Name");
assertDoesNotThrow(() -> {
incompletePatient.accept(visitor);
String report = visitor.getReport();
assertNotNull(report);
});
}
@Test
public void testVisitorWithInvalidData() {
FinancialReportVisitor visitor = new FinancialReportVisitor();
// Test bill with negative amounts
MedicalBill invalidBill = new MedicalBill("P001", "Test Service", -100.00);
assertDoesNotThrow(() -> {
invalidBill.accept(visitor);
String report = visitor.getReport();
// Verify report handles invalid data gracefully
assertNotNull(report);
});
}
9. Conclusion
The Visitor pattern implementation for medical reports generation demonstrates a sophisticated approach to handling diverse reporting requirements in healthcare systems. The pattern successfully addresses the complex challenges of multi-domain data aggregation while maintaining clean separation between report generation logic and core business objects.
Key Achievements
- Flexible Report Framework: Successfully implements a comprehensive reporting system that can generate diverse report types from the same underlying data
- Domain Separation: Clean separation between business logic and reporting concerns, allowing independent evolution of both
- Extensible Architecture: Easy addition of new report types without modifying existing domain objects or other reports
- Complex Data Analysis: Sophisticated data aggregation and correlation across multiple domain objects (patients, appointments, bills)
- Performance Scalability: Efficient processing of large datasets through visitor callbacks and streaming approaches
Real-World Healthcare Impact
The Visitor pattern proves particularly valuable in healthcare reporting contexts where:
- Regulatory Compliance requires diverse reporting formats for different regulatory bodies and timeframes
- Multi-Stakeholder Needs demand different views of the same data for clinical staff, administrators, patients, and insurance companies
- Clinical Decision Support benefits from comprehensive patient summaries and utilization analysis
- Financial Management requires detailed billing analysis and revenue performance tracking
- Quality Improvement needs service utilization patterns and patient engagement metrics
Pattern Benefits Realized
The implementation successfully demonstrates how the Visitor pattern can:
- Enable complex analytical processing without cluttering domain objects with reporting logic
- Support real-time report generation from current operational data without data duplication
- Facilitate consistent data interpretation across different report types and stakeholders
- Provide type-safe report processing with compile-time verification of visitor completeness
- Maintain high performance even with large datasets through efficient visitor traversal patterns
Technical Excellence
The visitor implementation showcases several advanced techniques:
- Double Dispatch mechanism for type-specific processing
- Data Correlation across multiple domain boundaries for comprehensive analysis
- Conditional Processing for filtered and targeted report generation
- Memory Efficiency through streaming visitor patterns for large datasets
- Error Resilience with graceful handling of incomplete or invalid data
The medical reports generation system provides a robust foundation for healthcare analytics while maintaining the flexibility needed for evolving reporting requirements in dynamic healthcare environments.
Document Status: Part E Complete
Next: Part F - Security Considerations - Decorator, DAO