Think of [c1 OR c2] AND [d1 OR d2] as a single requirement. Consider c1, c2, d1, d2 as requirements, say sub requirements or atomic requirements. Now they all have one thing in common. They can be checked if they are met. So let's introduce an interface for that:
public interface Requirement {
public boolean isMet();
}
Now atomic requirements can be implemented like
public class AtomicRequirement implements Requirement {
private final boolean met;
public AtomicRequirement(boolean isMet) {
this.met = isMet;
}
public boolean isMet() {
return true;
}
}
You can add compound requirements and build upon the Requirement interface.
public class AndRequirement implements Requirement {
private final Requirement r1, r2;
public AndRequirement(Requirement r1, Requirement r2) {
this.r1 = r1;
this.r2 = r2;
}
public boolean isMet() {
return r1.isMet() && r2.isMet();
}
}
Same goes for OR. You can do it with more complex combinators
public class AnyOfTheseRequirement implements Requirement {
private final List<Requirement> anyOfThese;
public AnyOfTheseRequirement(List<Requirement> anyOfThese) {
this.anyOfThese = anyOfThese;
}
public boolean isMet() {
for (Requirement r : anyofThese) {
if (r.isMet()) return true;
}
return false;
}
}
and of course
public class MakseeRequirement implements Requirement {
private final AndRequirement r;
public MakseeRequirement(Requirement c1, List<Requirement> dns) {
AnyOfTheseRequirement d = new AnyOfTheseRequirement(dns);
this.r = new AndRequirement(c1, d);
}
public boolean isMet() {
return r.isMet();
}
}
This approach is quite extendable but you can trim it down to the three actual types of requirements you got.