1

Say I have a Group object which maintains a list of its Members:

private $members;

Now the class Member has a range of subclasses such as OrdinaryMember and GroupLeader. Every group has precisely one leader. The GroupLeader can provide a list of pending applicants to this group:

$groupLeader->getApplicantList();

However, the ::getApplicantList() method is unavailable on all of the OrdinaryMember instances.

How can a Group object obtain the list of pending applicants?

  • 1
    Downvoters please could you explain why voted? How could this be improved? – gen Jun 2 at 15:59
  • Could you add som detail about the programming language used please? The solution might differ for various programming languages. – πάντα ῥεῖ Jun 2 at 16:07
  • 3
    There are various tricks you could do, but your "problem" lacks specificity, to a painful degree, unfortunately, so answers will have to be invariably wide and with a rather "catch-all" style... Try to think of a better and more specific use case and we will be happy to try and provide some insight, to the extent possible. – Vector Zita Jun 2 at 16:33
  • 1
    @πάνταῥεῖ I was hoping to get a language agnostic answer, but I am currently trying to figure this out in PHP. – gen Jun 2 at 16:44
  • 2
    @gen Books on design patterns tend to be aimed at specific programming languages, paradigms, or families of programming languages. For example, the Gang-of-Four design patterns were designed around some of the popular programming languages at the time like Java and C++; as it turns out, many of the patterns in that book have now been baked into those languages or their standard libraries. If someone now wanted to use the GoF iterator pattern in C++ or Java, they would not follow the GoF book but just use what Java and C++ already provide. – Ben Cottrell Jun 2 at 18:02
6

This is largely a restatement of the @Telastyn answer, but might be clearer...

GroupLeader has special abilities, and special restrictions, there can be only one. It should have it's own field and setter. If you try to set more than one leader the code can react appropriately (replace the existing one or complain). You then access this field (or getter or delegating method) to access the applicant list. (Apologies if my PHP code is a bit off below...)

myGroup.$groupLeader.getApplicantList()    or

myGroup.getGroupLeader().getApplicantList()

or, if you prefer to follow the Law of Demeter (I don't) but this has an advantage if no leader has been set you can return an empty list:

myGroup.getApplicantList()

In addition, the groupLeader can also be in your list of Members. However, here it is just treated like any other generic Member, with (I presume) fields or methods for name, birthday, favorite food, etc...

| improve this answer | |
  • 1
    Thank you for your answer – gen Jun 2 at 20:35
3

A sound separation of concerns should relate applicants directly with the group to avoid unfortunate situations:

  • no leader was assigned to the group, hence no one can apply
  • a leader is removed from the group and all applicants are lost
  • several leaders on the group: what to do with applicants that are accepted in the group by only one of the leader ?

Of course, if you can assume that the group has one leader, or that several leaders are able to coordinate, and if there are founded reasons not to manage applicants at the group level, you can pursue your approach.

The leader has a slightly different interface. The best in this case is to translate the structural assumptions into a structural reality, i.e maintain a group property that refers to the group leader:

  • if your types have reference semantics you can refer to the leader (property of the group) and conveniently keep the leader in the list of members.
  • if your types have value semantic, use a leader property, don’t duplicate the leader in the members, and every time you iterate through the members, add a special case for the leader (ugly solution, I agree).
  • I you don’t like any of the above, define a method getLeader() that will always find the leader in the members and return a reference to it. This seems more elegant, but requires you to properly guarantee the invariant of having one single leader in all your operations related to the group members.

You can also extend this solution to cope with several leaders: instead of finding one leader, find the subset of all leaders, apply applicanList() to each, and merge the results.

If the example was badly chosen and it’s not really about group leaders and applicants, but just a group property that emerges from the members you’d better directly consider the getLeader() and merge the results.

If your languages doesn’t easily allow such a type based member selection (i.e doesn’t support dynamic cast nor reflection), a workaround could be to add a decorator to the non-leaders and handle all the members as leaders, with the insurance that no harm will occur when getApplicantList is invoked for a non member.

| improve this answer | |
  • 1
    This is a really good answer, one that addresses few problems I could anticipate and many more that I couldn't! Thank you for posting it – gen Jun 3 at 9:33
1

You give the group a field/property for its one leader. Then it has the right type and you can access the method. That though violates the Law of Demeter. Why is the applicant list on the leader rather than the group itself? Certainly the leader needs to know what group someone is applying to?

| improve this answer | |
  • Thanks for your answer. Why is the applicant list on the leader? It's just an example I made up on the fly to illustrate the problem I am trying to solve. It might be illogical, but the use case is still valid I think. You have an object that maintains references to a list of other objects that have a common ancestor. There are specialized objects and you need to retrieve something from one of them. How do you identify the object you need? – gen Jun 2 at 16:01
  • 2
    @gen - You should not. It is an OO anti-pattern. It violates the Liskov Substitution Principle, making your code fragile and hard to maintain. Where possible, you should adjust your design to not require it. – Telastyn Jun 2 at 16:17
  • Well, I'm trying to find the OO way... I'm not sure how this would violate Liskov though. Any Group can act on all of its members according to the contract set out by the Member superclass, without knowing the exact subtype. – gen Jun 2 at 16:47
  • 1
    @gen - “leverage additional capabilities” is the essence of LSP violations. – Telastyn Jun 2 at 17:08
  • 1
    @gen - you mean like my answer recommends? – Telastyn Jun 2 at 17:48
1

There are a few basic options. I think it is best though to see them written out in code.

So saying for sake of argument this is how your classes are written - maybe with Member being an interface for you or similar and maybe written in a dynamic language instead of a static one.

class Member {}

class OrdinaryMember extends Member {}

class GroupLeader extends Member {
    public List<Member> getApplicantList() {
        return Collections.emptyList();
    }
}

There are a few categories of solutions

1 - The Runtime Dynamism Solutions

1.1 - Actually check that you have an instance of what you care about

For languages with a "nominal" type system or where the concrete "class" of an object is preserved as runtime accessible data, the simplest approach is just to check that you have the exact subclass you were expecting.

List<Member> = ...;
for (final var member : members) {
    if (member instanceof GroupLeader leader) {
        ... leader.getApplicantList();
    }
}

This has the pro of narrowing down on exactly the capability you want, but comes with the downside of locking you into using just the GroupLeader class, and maybe even deeper subclasses, for this functionality.

1.2 - Check for the capabilities rather than for a concrete type

If you are in a language that is duck typed and allows for you to inspect available methods on an object, you can check an object directly to see if it would respond to the method name you want to call on it. This is similar to the previous solution, but doesn't lock you in to any particular class hierarchy for valid members

A good example of a language with this capability would be ruby, which would look something like this.

if member.respond_to?(:get_applicant_list) then
    applicants = member.get_applicant_list
end

2 - The Static Solutions

2.1 - Add a method to the super class

Basically, if you have a situation where not every member responds to the method you want, but there is some sensible "no-op" implementation, you can just do that. In this case that would mean adding an implementation that returns an empty list for all members since they wouldn't have any applicants. This has the downside of "polluting" the interface of the "Member" to include things that every member probably doesn't care about and might not make logical sense.

class Member {
    public List<Member> getApplicantList() {
        return Collections.emptyList();
    }
}

class OrdinaryMember extends Member {}

class GroupLeader extends Member {
    @Override
    public List<Member> getApplicantList() {
        return // some logic
    }
}

2.2 Make a sealed hierarchy

Effectively, the issue with an instance check is that you don't provide a path for new code to implement the "getApplicantList" part without being a direct subclass and in the situation where you are in a static language and can't fix that by checking for capabilities - one approach to mitigate that issue is to make the "Member" types a "sealed" hierarchy.

This means that, either by a pattern like the visitor pattern or a language feature, you restrict the subclasses that are allowed to be created to a known set. This means that if you are maintaining all of the code and you need to add a new type to the hierarchy, you will have the guard rails of warnings or errors telling you to handle that situation specially. For instance

sealed abstract class Member permits OrdinaryMember, GroupLeader {}

class OrdinaryLeader extends Member {}

class GroupLeader extends Member {
    public List<Member> getApplicantList() {
        return Collections.emptyList();
    }
}

Which could then be used with some language construct that tries to ensure "exhaustiveness" in order to minimize the refactor cost of adding a new thing that can give an applicant list without direct subclassing.

for (final var member : members) {
    switch (member) {
        case GroupLeader leader -> { ... leader.getApplicantList(); ... }
        case OrdinaryMember member -> { ... }
    }
}

3 - Stop what you are doing and do something else.

3.1 - Move this to another object.

Your group leader member maybe doesn't need to track the applicants itself. In fact, it might not need to be any different than an ordinary member. What you can do is have a "group" object just store a single group leader and have the group itself keep track of applicants, not the leader.

class Group {
   private Member leader;
   private List<Member> acceptedMembers;
   private List<Member> pendingMembers;
}

And maybe also you can have some sort of lookup of groups by their leader, and so on. Essentially, you might be able to change other aspects of the design to sidestep this issue.

What do I think you should do?

Approach 3.1: In your case, your group class has just a singular list of members, but it can logically only have one leader. This is a hint to me that the design of the class can be improved. Instead of joining everything in one big list, have a singular field for the leader and seperate lists for the other members.

It is also suspicious that a member, which is owned by a group, has a method that tells you info about the group that owns it. That dependency seems logically cyclical. Its probably a better idea to let the Group class take care of that functionality.

| improve this answer | |
  • Thank you for taking the time to write this up! – gen Jun 3 at 12:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.