Java: Strategy and Factory Pattern with Method Chaining

0
13
Java Programming

We will design a rules engine in Java and utilize design patterns. Let us examine the business case and the hierarchy.

Example of a Simplified Decision Tree:

  1. Line of Business: Engineering
    • Location: North America
      • Job Code: Software Engineer
        • Job Grade: Senior
          • Salary Range: $120,000 – $150,000
        • Job Grade: Mid-level
          • Salary Range: $90,000 – $120,000
      • Job Code: Product Manager
        • Job Grade: Senior
          • Salary Range: $130,000 – $160,000
    • Location: Europe
      • Job Code: Software Engineer
        • Job Grade: Senior
          • Salary Range: €100,000 – €130,000
        • Job Grade: Mid-level
          • Salary Range: €70,000 – €100,000
    • Location: Asia
      • Job Code: Software Engineer
        • Job Grade: Senior
          • Salary Range: $80,000 – $110,000
  2. Line of Business: Marketing
    • Location: North America
      • Job Code: Marketing Specialist
        • Job Grade: Senior
          • Salary Range: $90,000 – $120,000
        • Job Grade: Mid-level
          • Salary Range: $60,000 – $90,000
    • Location: Europe
      • Job Code: Marketing Specialist
        • Job Grade: Senior
          • Salary Range: €70,000 – €100,000

To implement this example in Java using POJO classes, we’ll create a few classes:

  1. SalaryRange – Represents the salary range with min and max values.
  2. SalaryOffer – Represents an offer made for a specific location, job code, and job grade.
  3. SalaryDeterminationService – Contains the logic to determine if a salary offer falls within the specified salary range based on the provided criteria.

Step 1: Create the POJO Classes

1. SalaryRange.java

public class SalaryRange {
    private double minSalary;
    private double maxSalary;
    private String location;
    private String jobCode;
    private String jobGrade;

    // Constructor
    public SalaryRange(double minSalary, double maxSalary, String location, String jobCode, String jobGrade) {
        this.minSalary = minSalary;
        this.maxSalary = maxSalary;
        this.location = location;
        this.jobCode = jobCode;
        this.jobGrade = jobGrade;
    }

    // Getters and Setters
    public double getMinSalary() {
        return minSalary;
    }

    public void setMinSalary(double minSalary) {
        this.minSalary = minSalary;
    }

    public double getMaxSalary() {
        return maxSalary;
    }

    public void setMaxSalary(double maxSalary) {
        this.maxSalary = maxSalary;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public String getJobCode() {
        return jobCode;
    }

    public void setJobCode(String jobCode) {
        this.jobCode = jobCode;
    }

    public String getJobGrade() {
        return jobGrade;
    }

    public void setJobGrade(String jobGrade) {
        this.jobGrade = jobGrade;
    }

    @Override
    public String toString() {
        return "SalaryRange [minSalary=" + minSalary + ", maxSalary=" + maxSalary +
                ", location=" + location + ", jobCode=" + jobCode + ", jobGrade=" + jobGrade + "]";
    }
}

2. SalaryOffer.java

public class SalaryOffer {
    private double offeredSalary;
    private String location;
    private String jobCode;
    private String jobGrade;

    // Constructor
    public SalaryOffer(double offeredSalary, String location, String jobCode, String jobGrade) {
        this.offeredSalary = offeredSalary;
        this.location = location;
        this.jobCode = jobCode;
        this.jobGrade = jobGrade;
    }

    // Getters and Setters
    public double getOfferedSalary() {
        return offeredSalary;
    }

    public void setOfferedSalary(double offeredSalary) {
        this.offeredSalary = offeredSalary;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public String getJobCode() {
        return jobCode;
    }

    public void setJobCode(String jobCode) {
        this.jobCode = jobCode;
    }

    public String getJobGrade() {
        return jobGrade;
    }

    public void setJobGrade(String jobGrade) {
        this.jobGrade = jobGrade;
    }

    @Override
    public String toString() {
        return "SalaryOffer [offeredSalary=" + offeredSalary + 
                ", location=" + location + ", jobCode=" + jobCode + ", jobGrade=" + jobGrade + "]";
    }
}

Step 2: Create the Service Class

SalaryDeterminationService.java

public class SalaryDeterminationService {

    // Method to check if the salary offer is within the range
    public boolean isOfferWithinRange(SalaryRange range, SalaryOffer offer) {
        if (offer.getLocation().equalsIgnoreCase(range.getLocation()) &&
            offer.getJobCode().equalsIgnoreCase(range.getJobCode()) &&
            offer.getJobGrade().equalsIgnoreCase(range.getJobGrade())) {
            
            double offeredSalary = offer.getOfferedSalary();
            return offeredSalary >= range.getMinSalary() && offeredSalary <= range.getMaxSalary();
        }
        return false;
    }

    // Method to determine and adjust the offer if needed
    public double determineSalaryOffer(SalaryRange range, SalaryOffer offer) {
        if (isOfferWithinRange(range, offer)) {
            return offer.getOfferedSalary(); // Offer is within range, return as is
        } else {
            // Adjust the salary to the minimum of the range if it's below
            if (offer.getOfferedSalary() < range.getMinSalary()) {
                return range.getMinSalary();
            }
            // Adjust the salary to the maximum of the range if it's above
            if (offer.getOfferedSalary() > range.getMaxSalary()) {
                return range.getMaxSalary();
            }
        }
        return offer.getOfferedSalary(); // Return original if no adjustment is needed
    }
}

Step 3: Testing the Implementation

Main.java

public class Main {
    public static void main(String[] args) {
        // Define a salary range
        SalaryRange range = new SalaryRange(60000, 80000, "New York", "ENG", "Senior");

        // Create a salary offer
        SalaryOffer offer = new SalaryOffer(75000, "New York", "ENG", "Senior");

        // Service to determine if the offer is within range
        SalaryDeterminationService service = new SalaryDeterminationService();

        // Check if the offer is within the salary range
        boolean isWithinRange = service.isOfferWithinRange(range, offer);
        System.out.println("Is offer within range? " + isWithinRange);

        // Determine the final salary offer
        double finalSalary = service.determineSalaryOffer(range, offer);
        System.out.println("Final Salary Offer: " + finalSalary);
    }
}

Output

If you run the above code, the output would be:

Is offer within range? true
Final Salary Offer: 75000.0

Explanation

  1. SalaryRange Class: Represents the range of salaries for a specific location, job code, and job grade.
  2. SalaryOffer Class: Represents a salary offer made for a specific job.
  3. SalaryDeterminationService: This service checks if a salary offer is within the specified range and adjusts it if necessary.
  • The isOfferWithinRange method checks if the offer’s salary falls within the range for the given location, job code, and job grade.
  • The determineSalaryOffer method ensures the offered salary is adjusted to the minimum or maximum of the range if it is outside the specified bounds.

This simple example shows how you can encapsulate salary range rules and logic in Java objects and services, providing a foundation for more complex business logic or integration with rule engines like Drools.

To implement specific overrides for HR employees in California (e.g., a 10% increase) while maintaining clean and manageable code as more exceptions arise, you can use the Strategy Pattern combined with Method Chaining. This allows you to define different adjustment strategies that can be applied based on various conditions, making it easy to add more rules or exceptions in the future.

Steps to Implement the Strategy Pattern with Method Chaining

  1. Define an Interface for Salary Adjustment Strategies: Create an interface that defines the method for applying salary adjustments.
  2. Implement Different Strategy Classes: Create classes that implement the interface for each adjustment rule (e.g., bilingual bonus, California HR adjustment).
  3. Modify the SalaryAdjustmentService: Allow it to accept and apply different strategies in a maintainable way.
  4. Update the Main Class: Use the new strategy-based service to apply the adjustments.

Step 1: Define the Salary Adjustment Strategy Interface

Create an interface for salary adjustment strategies.

SalaryAdjustmentStrategy.java

public interface SalaryAdjustmentStrategy {
    void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange);
}

Step 2: Implement the Specific Adjustment Strategies

Now, implement the strategies for the bilingual increase and the California HR adjustment.

BilingualBonusAdjustment.java

public class BilingualBonusAdjustment implements SalaryAdjustmentStrategy {
    @Override
    public void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        if ("HR".equalsIgnoreCase(salaryOffer.getLineOfBusiness()) && salaryOffer.isBilingual()) {
            double adjustedSalary = salaryOffer.getOfferedSalary() * 1.07; // 7% increase
            System.out.println("Applying 7% bilingual increase for HR. Adjusted Salary: " + adjustedSalary);
            salaryOffer.setOfferedSalary(adjustedSalary);
        }
    }
}

CaliforniaHREmployeeAdjustment.java

public class CaliforniaHREmployeeAdjustment implements SalaryAdjustmentStrategy {
    @Override
    public void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        if ("HR".equalsIgnoreCase(salaryOffer.getLineOfBusiness()) &&
            "California".equalsIgnoreCase(salaryOffer.getLocation())) {
            double adjustedSalary = salaryOffer.getOfferedSalary() * 1.10; // 10% increase
            System.out.println("Applying 10% increase for California HR employee. Adjusted Salary: " + adjustedSalary);
            salaryOffer.setOfferedSalary(adjustedSalary);
        }
    }
}

Step 3: Modify the SalaryAdjustmentService

Modify the service class to accept a list of strategies and apply them.

SalaryAdjustmentService.java

import java.util.ArrayList;
import java.util.List;

public class SalaryAdjustmentService {
    private SalaryOffer salaryOffer;
    private SalaryRange salaryRange;
    private List<SalaryAdjustmentStrategy> strategies;

    public SalaryAdjustmentService(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        this.salaryOffer = salaryOffer;
        this.salaryRange = salaryRange;
        this.strategies = new ArrayList<>();
    }

    // Method to add a strategy
    public SalaryAdjustmentService addStrategy(SalaryAdjustmentStrategy strategy) {
        strategies.add(strategy);
        return this;
    }

    // Method to apply all strategies
    public SalaryAdjustmentService applyAdjustments() {
        for (SalaryAdjustmentStrategy strategy : strategies) {
            strategy.applyAdjustment(salaryOffer, salaryRange);
        }
        ensureSalaryWithinRange(); // Ensure salary is within range after adjustments
        return this;
    }

    // Method to ensure salary is within the range
    private void ensureSalaryWithinRange() {
        if (salaryOffer.getOfferedSalary() < salaryRange.getMinSalary()) {
            System.out.println("Offer is below the minimum range. Adjusting to minimum.");
            salaryOffer.setOfferedSalary(salaryRange.getMinSalary());
        } else if (salaryOffer.getOfferedSalary() > salaryRange.getMaxSalary()) {
            System.out.println("Offer is above the maximum range. Adjusting to maximum.");
            salaryOffer.setOfferedSalary(salaryRange.getMaxSalary());
        } else {
            System.out.println("Offer is within the range.");
        }
    }

    // Method to return the final adjusted offer
    public SalaryOffer getFinalOffer() {
        return salaryOffer;
    }
}

Step 4: Update the Main Class

In the Main class, you can now add the different strategies and apply them.

Main.java

public class Main {
    public static void main(String[] args) {
        // Define a salary range
        SalaryRange range = new SalaryRange(60000, 80000, "New York", "ENG", "Senior");

        // Create a salary offer for a bilingual HR employee in California
        SalaryOffer offer = new SalaryOffer(75000, "California", "ENG", "Senior", true, "HR");

        // Use the SalaryAdjustmentService with strategy pattern and method chaining
        SalaryOffer finalOffer = new SalaryAdjustmentService(offer, range)
            .addStrategy(new BilingualBonusAdjustment()) // Add bilingual bonus strategy
            .addStrategy(new CaliforniaHREmployeeAdjustment()) // Add California HR adjustment strategy
            .applyAdjustments()  // Apply all adjustments
            .getFinalOffer();    // Get the final adjusted offer

        // Output the final salary offer
        System.out.println("Final Salary Offer: " + finalOffer.getOfferedSalary());
    }
}

Explanation of the Strategy Pattern Implementation

  1. SalaryAdjustmentStrategy Interface: This interface defines the contract for any salary adjustment strategy, allowing for easy extension in the future.
  2. Specific Strategies: Each adjustment logic (e.g., bilingual bonus, California HR adjustment) is encapsulated in its own class, making it easy to manage and extend. New rules can be added by creating new classes that implement the SalaryAdjustmentStrategy.
  3. SalaryAdjustmentService: This service now maintains a list of strategies and applies each one in turn. It also ensures that the final salary remains within the defined range.
  4. Main Class: The main class demonstrates how to create an offer and apply multiple adjustment strategies in a clean and maintainable manner.

Output

Running the Main class with the given input will produce output similar to:

Applying 7% bilingual increase for HR. Adjusted Salary: 80250.0
Applying 10% increase for California HR employee. Adjusted Salary: 88275.0
Offer is above the maximum range. Adjusting to maximum.
Final Salary Offer: 80000.0

Benefits of This Approach

  • Maintainability: New adjustment rules can be added as new classes without changing existing code, promoting the Open/Closed Principle.
  • Flexibility: Strategies can be easily combined or reordered by changing the order of addStrategy calls.
  • Separation of Concerns: Each adjustment logic is encapsulated in its own class, making it clear and easy to understand.

This design can handle multiple exceptions as they arise in the future, keeping the code clean and maintainable.

To ensure that the California rule (10% increase for HR employees in California) does not get overwritten by the default HR rule (7% increase for bilingual HR employees), you can implement a prioritization mechanism within the adjustment strategies. Here’s how you can structure it:

  1. Check for Specific Conditions First: Always check the more specific rules (like the California rule) before the more general rules (like the bilingual bonus). This way, if the conditions for the California adjustment are met, the adjustment is applied first.
  2. Flagging Adjustments: You can also use a flag or a condition to indicate if an adjustment has already been applied. If an adjustment is made, subsequent adjustments can check this flag to determine whether they should apply.
  3. Order of Strategy Execution: The order in which strategies are added to the SalaryAdjustmentService can be controlled to ensure that specific strategies are executed before more general ones.

Implementation Steps

1. Update the Salary Adjustment Strategies

Add logic in the applyAdjustment methods to prevent further adjustments if a specific adjustment has already been applied.

CaliforniaHREmployeeAdjustment.java

public class CaliforniaHREmployeeAdjustment implements SalaryAdjustmentStrategy {
    @Override
    public void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        if ("HR".equalsIgnoreCase(salaryOffer.getLineOfBusiness()) &&
            "California".equalsIgnoreCase(salaryOffer.getLocation())) {
            double adjustedSalary = salaryOffer.getOfferedSalary() * 1.10; // 10% increase
            System.out.println("Applying 10% increase for California HR employee. Adjusted Salary: " + adjustedSalary);
            salaryOffer.setOfferedSalary(adjustedSalary);
            // Indicate that this specific adjustment has been applied
            salaryOffer.setAdjustmentApplied(true); // Add a flag to indicate adjustment
        }
    }
}

BilingualBonusAdjustment.java

public class BilingualBonusAdjustment implements SalaryAdjustmentStrategy {
    @Override
    public void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        // Only apply if no other adjustment has been made
        if (!salaryOffer.isAdjustmentApplied() && 
            "HR".equalsIgnoreCase(salaryOffer.getLineOfBusiness()) && salaryOffer.isBilingual()) {
            double adjustedSalary = salaryOffer.getOfferedSalary() * 1.07; // 7% increase
            System.out.println("Applying 7% bilingual increase for HR. Adjusted Salary: " + adjustedSalary);
            salaryOffer.setOfferedSalary(adjustedSalary);
            salaryOffer.setAdjustmentApplied(true); // Indicate that this adjustment has been applied
        }
    }
}

2. Update the SalaryOffer Class

You’ll need to add a flag in the SalaryOffer class to track if an adjustment has been applied.

SalaryOffer.java

public class SalaryOffer {
    private double offeredSalary;
    private String location;
    private String jobCode;
    private String jobGrade;
    private boolean isBilingual;
    private String lineOfBusiness;
    private boolean adjustmentApplied; // New flag to indicate if an adjustment has been made

    // Constructor
    public SalaryOffer(double offeredSalary, String location, String jobCode, String jobGrade, boolean isBilingual, String lineOfBusiness) {
        this.offeredSalary = offeredSalary;
        this.location = location;
        this.jobCode = jobCode;
        this.jobGrade = jobGrade;
        this.isBilingual = isBilingual;
        this.lineOfBusiness = lineOfBusiness;
        this.adjustmentApplied = false; // Initialize to false
    }

    // Getters and Setters...
    
    public boolean isAdjustmentApplied() {
        return adjustmentApplied;
    }

    public void setAdjustmentApplied(boolean adjustmentApplied) {
        this.adjustmentApplied = adjustmentApplied;
    }
    
    @Override
    public String toString() {
        return "SalaryOffer [offeredSalary=" + offeredSalary +
                ", location=" + location + ", jobCode=" + jobCode +
                ", jobGrade=" + jobGrade + ", isBilingual=" + isBilingual +
                ", lineOfBusiness=" + lineOfBusiness + "]";
    }
}

3. Update the Main Class to Demonstrate the Logic

In the Main class, you can add strategies to ensure the correct order is applied:

Main.java

public class Main {
    public static void main(String[] args) {
        // Define a salary range
        SalaryRange range = new SalaryRange(60000, 80000, "California", "ENG", "Senior");

        // Create a salary offer for a bilingual HR employee in California
        SalaryOffer offer = new SalaryOffer(75000, "California", "ENG", "Senior", true, "HR");

        // Use the SalaryAdjustmentService with strategy pattern and method chaining
        SalaryOffer finalOffer = new SalaryAdjustmentService(offer, range)
            .addStrategy(new CaliforniaHREmployeeAdjustment()) // Add California HR adjustment strategy first
            .addStrategy(new BilingualBonusAdjustment()) // Then add bilingual bonus strategy
            .applyAdjustments()  // Apply all adjustments
            .getFinalOffer();    // Get the final adjusted offer

        // Output the final salary offer
        System.out.println("Final Salary Offer: " + finalOffer.getOfferedSalary());
    }
}

How This Ensures Correct Behavior

  • Priority of Rules: By adding the CaliforniaHREmployeeAdjustment strategy first in the SalaryAdjustmentService, it is guaranteed to be executed before the BilingualBonusAdjustment. Thus, if the California condition is met, that adjustment will be applied before checking for the bilingual adjustment.
  • Adjustment Flag: The adjustmentApplied flag in the SalaryOffer class prevents subsequent adjustments from being applied if a more specific adjustment (like the California HR adjustment) has already been made. This ensures that once a specific adjustment is applied, other adjustments do not overwrite it.

Example Output

When you run this implementation with a salary offer for a bilingual HR employee in California, you might see the following output:

Applying 10% increase for California HR employee. Adjusted Salary: 82500.0
Offer is within the range.
Final Salary Offer: 82500.0

This approach allows you to maintain a clean and extensible system that easily accommodates additional rules or exceptions without fear of unintended overwriting of more specific adjustments.

To handle complex rule sets with specific overrides for different lines of business (LOBs) like Financial and HR, you can use a combination of the Strategy Pattern and Factory Pattern. This approach allows you to define LOB-specific rules and overrides in a modular way, and dynamically apply only the rules relevant to a particular LOB.

Key Concepts:

  1. Strategy Pattern: Allows you to define a family of algorithms (in this case, salary adjustments) and make them interchangeable. Each LOB can have its own set of strategies.
  2. Factory Pattern: Used to create the appropriate set of strategies for a given LOB. This ensures that only the relevant rules are applied based on the LOB.
  3. Chaining and Priority Management: Ensures that more specific rules (like the manager weekend rule) are applied after more general rules (like the weekend rule).

Implementation Steps:

  1. Define the Salary Adjustment Strategy Interface: This remains the same as before.
  2. Create Specific Strategy Classes for Financial LOB:
    • A class for the weekend adjustment.
    • A class for the manager weekend adjustment.
  3. Create a Factory to Generate the Correct Set of Strategies: This factory will take the LOB as input and return the appropriate set of strategies.
  4. Integrate with the Salary Adjustment Service: Use the factory to apply only the relevant strategies for the given LOB.
  5. Use the Factory and Strategies in the Main Class: Apply the correct rules based on the LOB.

Step 1: Salary Adjustment Strategy Interface

This remains unchanged:

public interface SalaryAdjustmentStrategy {
    void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange);
}

Step 2: Create Financial LOB-Specific Strategies

WeekendBonusAdjustment.java

public class WeekendBonusAdjustment implements SalaryAdjustmentStrategy {
    @Override
    public void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        if ("FIN".equalsIgnoreCase(salaryOffer.getLineOfBusiness()) && salaryOffer.worksOnWeekends()) {
            double adjustedSalary = salaryOffer.getOfferedSalary() * 1.05; // 5% increase
            System.out.println("Applying 5% weekend bonus for Financial. Adjusted Salary: " + adjustedSalary);
            salaryOffer.setOfferedSalary(adjustedSalary);
            salaryOffer.setAdjustmentApplied(true);
        }
    }
}

ManagerWeekendBonusAdjustment.java

public class ManagerWeekendBonusAdjustment implements SalaryAdjustmentStrategy {
    @Override
    public void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        if ("FIN".equalsIgnoreCase(salaryOffer.getLineOfBusiness()) &&
            salaryOffer.worksOnWeekends() && salaryOffer.isManager()) {
            double adjustedSalary = salaryOffer.getOfferedSalary() * 1.08; // 8% increase
            System.out.println("Applying 8% weekend bonus for Financial Managers. Adjusted Salary: " + adjustedSalary);
            salaryOffer.setOfferedSalary(adjustedSalary);
            salaryOffer.setAdjustmentApplied(true);
        }
    }
}

Step 3: Create a Factory to Generate the Correct Set of Strategies

SalaryAdjustmentStrategyFactory.java

import java.util.ArrayList;
import java.util.List;

public class SalaryAdjustmentStrategyFactory {

    public static List<SalaryAdjustmentStrategy> getStrategiesForLOB(String lob) {
        List<SalaryAdjustmentStrategy> strategies = new ArrayList<>();
        
        switch (lob.toUpperCase()) {
            case "HR":
                strategies.add(new CaliforniaHREmployeeAdjustment());
                strategies.add(new BilingualBonusAdjustment());
                break;
            case "FIN":
                strategies.add(new ManagerWeekendBonusAdjustment()); // Specific manager rule first
                strategies.add(new WeekendBonusAdjustment());
                break;
            // Add other LOB-specific strategies here...
        }
        return strategies;
    }
}

Step 4: Integrate with the Salary Adjustment Service

Modify the SalaryAdjustmentService to accept a list of strategies directly.

SalaryAdjustmentService.java

import java.util.List;

public class SalaryAdjustmentService {
    private SalaryOffer salaryOffer;
    private SalaryRange salaryRange;
    private List<SalaryAdjustmentStrategy> strategies;

    public SalaryAdjustmentService(SalaryOffer salaryOffer, SalaryRange salaryRange, List<SalaryAdjustmentStrategy> strategies) {
        this.salaryOffer = salaryOffer;
        this.salaryRange = salaryRange;
        this.strategies = strategies;
    }

    // Method to apply all strategies
    public SalaryAdjustmentService applyAdjustments() {
        for (SalaryAdjustmentStrategy strategy : strategies) {
            strategy.applyAdjustment(salaryOffer, salaryRange);
        }
        ensureSalaryWithinRange(); // Ensure salary is within range after adjustments
        return this;
    }

    // Method to ensure salary is within the range
    private void ensureSalaryWithinRange() {
        if (salaryOffer.getOfferedSalary() < salaryRange.getMinSalary()) {
            System.out.println("Offer is below the minimum range. Adjusting to minimum.");
            salaryOffer.setOfferedSalary(salaryRange.getMinSalary());
        } else if (salaryOffer.getOfferedSalary() > salaryRange.getMaxSalary()) {
            System.out.println("Offer is above the maximum range. Adjusting to maximum.");
            salaryOffer.setOfferedSalary(salaryRange.getMaxSalary());
        } else {
            System.out.println("Offer is within the range.");
        }
    }

    // Method to return the final adjusted offer
    public SalaryOffer getFinalOffer() {
        return salaryOffer;
    }
}

Step 5: Use the Factory and Strategies in the Main Class

In the Main class, you can now apply the relevant rules based on the LOB.

Main.java

public class Main {
    public static void main(String[] args) {
        // Define a salary range
        SalaryRange range = new SalaryRange(60000, 80000, "California", "ENG", "Senior");

        // Create a salary offer for a Financial Manager working on weekends
        SalaryOffer offer = new SalaryOffer(75000, "California", "ENG", "Senior", false, "FIN", true, true);

        // Get the appropriate strategies for the Financial LOB
        List<SalaryAdjustmentStrategy> strategies = SalaryAdjustmentStrategyFactory.getStrategiesForLOB(offer.getLineOfBusiness());

        // Use the SalaryAdjustmentService with the appropriate strategies
        SalaryOffer finalOffer = new SalaryAdjustmentService(offer, range, strategies)
            .applyAdjustments()  // Apply all adjustments
            .getFinalOffer();    // Get the final adjusted offer

        // Output the final salary offer
        System.out.println("Final Salary Offer: " + finalOffer.getOfferedSalary());
    }
}

Explanation of the Implementation

  1. Factory Pattern: The SalaryAdjustmentStrategyFactory is responsible for creating and returning the appropriate set of strategies based on the LOB. This ensures that only the relevant rules are applied, keeping the system efficient and maintainable.
  2. Chaining and Prioritization: Strategies are added in a specific order in the factory, ensuring that more specific rules (like the manager weekend bonus) are applied before more general rules (like the weekend bonus).
  3. Extendability: New rules for other LOBs can be easily added by implementing new strategies and updating the factory.
  4. Maintainability: This approach keeps the codebase clean and modular, allowing easy updates as new rules and exceptions are introduced.

Example Output

When you run this implementation with a salary offer for a Financial Manager working on weekends, the output might look like this:

Applying 8% weekend bonus for Financial Managers. Adjusted Salary: 81000.0
Offer is within the range.
Final Salary Offer: 81000.0

This implementation ensures that each LOB’s rules are applied correctly and that more specific rules take precedence over general rules, maintaining a clean and extensible code structure.

Precedence Management

To ensure that the manager’s adjustment takes precedence and is not overwritten by the general employee adjustment, you can follow these strategies:

  1. Order of Execution: Ensure that the manager adjustment strategy is executed before the general employee adjustment. This can be managed by the order in which strategies are applied in the SalaryAdjustmentService.
  2. Conditional Logic in the Strategies: Implement conditional checks within the general employee adjustment strategy to ensure it does not apply its adjustment if a more specific (like the manager) adjustment has already been applied.
  3. Adjustment Applied Flag: Use the adjustmentApplied flag within the SalaryOffer class to track if a specific adjustment has been applied. The general employee adjustment should check this flag before applying any changes.

Updated Strategy Classes

Here’s how you can implement these strategies:

1. ManagerWeekendBonusAdjustment.java

This strategy applies the manager-specific adjustment and sets the adjustmentApplied flag to true.

public class ManagerWeekendBonusAdjustment implements SalaryAdjustmentStrategy {
    @Override
    public void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        if ("FIN".equalsIgnoreCase(salaryOffer.getLineOfBusiness()) &&
            salaryOffer.worksOnWeekends() && salaryOffer.isManager()) {
            double adjustedSalary = salaryOffer.getOfferedSalary() * 1.08; // 8% increase
            System.out.println("Applying 8% weekend bonus for Financial Managers. Adjusted Salary: " + adjustedSalary);
            salaryOffer.setOfferedSalary(adjustedSalary);
            salaryOffer.setAdjustmentApplied(true); // Flag that this adjustment has been applied
        }
    }
}

2. WeekendBonusAdjustment.java

This strategy applies the general employee adjustment, but only if no other adjustment (like the manager adjustment) has already been applied.

public class WeekendBonusAdjustment implements SalaryAdjustmentStrategy {
    @Override
    public void applyAdjustment(SalaryOffer salaryOffer, SalaryRange salaryRange) {
        // Only apply if no other adjustment has been made
        if (!salaryOffer.isAdjustmentApplied() &&
            "FIN".equalsIgnoreCase(salaryOffer.getLineOfBusiness()) && salaryOffer.worksOnWeekends()) {
            double adjustedSalary = salaryOffer.getOfferedSalary() * 1.05; // 5% increase
            System.out.println("Applying 5% weekend bonus for Financial employees. Adjusted Salary: " + adjustedSalary);
            salaryOffer.setOfferedSalary(adjustedSalary);
            salaryOffer.setAdjustmentApplied(true);
        }
    }
}

Factory Pattern: Control the Order of Strategies

In the factory that generates the strategies, make sure the manager-specific strategy is added before the general employee strategy:

public class SalaryAdjustmentStrategyFactory {

    public static List<SalaryAdjustmentStrategy> getStrategiesForLOB(String lob) {
        List<SalaryAdjustmentStrategy> strategies = new ArrayList<>();
        
        switch (lob.toUpperCase()) {
            case "HR":
                strategies.add(new CaliforniaHREmployeeAdjustment());
                strategies.add(new BilingualBonusAdjustment());
                break;
            case "FIN":
                strategies.add(new ManagerWeekendBonusAdjustment()); // Add this first
                strategies.add(new WeekendBonusAdjustment());        // Then this
                break;
            // Add other LOB-specific strategies here...
        }
        return strategies;
    }
}

How It Works

  • Priority through Order: By placing the ManagerWeekendBonusAdjustment strategy first in the list, it is executed before the WeekendBonusAdjustment. This ensures that if the employee is a manager, the manager-specific adjustment is applied first.
  • Conditional Logic with Flags: The WeekendBonusAdjustment checks the adjustmentApplied flag. If the manager adjustment has already been applied, the general employee adjustment will not be executed.

Example Usage

Here’s how you would use these strategies in the Main class:

public class Main {
    public static void main(String[] args) {
        // Define a salary range
        SalaryRange range = new SalaryRange(60000, 80000, "California", "ENG", "Senior");

        // Create a salary offer for a Financial Manager working on weekends
        SalaryOffer offer = new SalaryOffer(75000, "California", "ENG", "Senior", false, "FIN", true, true);

        // Get the appropriate strategies for the Financial LOB
        List<SalaryAdjustmentStrategy> strategies = SalaryAdjustmentStrategyFactory.getStrategiesForLOB(offer.getLineOfBusiness());

        // Use the SalaryAdjustmentService with the appropriate strategies
        SalaryOffer finalOffer = new SalaryAdjustmentService(offer, range, strategies)
            .applyAdjustments()  // Apply all adjustments
            .getFinalOffer();    // Get the final adjusted offer

        // Output the final salary offer
        System.out.println("Final Salary Offer: " + finalOffer.getOfferedSalary());
    }
}

Expected Output

When running the above code, you might see something like this:

Applying 8% weekend bonus for Financial Managers. Adjusted Salary: 81000.0
Offer is within the range.
Final Salary Offer: 81000.0

This output confirms that the manager-specific adjustment was applied first, and the general employee adjustment was skipped due to the adjustmentApplied flag being set. This ensures that more specific rules always take precedence over general rules, maintaining the intended logic in a clean and maintainable way.