开发者

How to extend family of objects (A has many B) with a more specific family of objects (A' <: A has many B' <: B)?

Sorry for the atrocious title, but I could not think of a better way of wording this. I will try to make this more clear with an example (which is not the problem that I am actually trying to solve, but I don't want to go into very problem-specific details).

Let's say I want to model forum-like discussions. In general, a topic represents something to be discussed (a message, a photo, etc), and comments represents the comments people make about that topic. So, at a very abstract level, we have:

Topic
  + title
  + comments: List<Comment>

Comment
  + text

Note: I am mixing Java-like syntax here because I am looking for a Java solution, but any solution that is suitable for a language with similar semantics than Java will probably be good.

Supose this simple model is enough for some general-purpose representation of a topic and its comments. But in some situations I'd like to have topics and comments with more information. For example, for a website where all comments are "authored" (i.e. they have an author... they cannot be anonymous), I'd like to have:

AuthoredTopic < Topic
  + author
  + comments: List<AuthoredComment>

AuthoredComment < Comment
  + author

So basically, I want to have more specialized types of topics, which have more specialized types of comments in them, but that they could still be seen as abstract topics and comments repectively. In a very basic way of seing it: an authored topic has an author, but it's still a topic, and its authored comments are still comments.

Now, before anyone has a seizure reading that pseudo-class-spec, the comments attribure is a read-only list (aka immutable =D). If it weren't, the design would be plain and simply broken, as one would be able to add any type of comment to an AuthoredTopic. But if it is immutable, then you could say that a List<AuthoredComment> is a List<Comment>, and therefore an AuthoredComment could be a valid subtype of Comment.

Now, I know Java doesn't have generic type's covariance, so one cannot say that a List<AuthoredComment> is a subtype List<Comment>, which is a shame, and is probably also the reason why I'm finding quite complicated to design/implement this.

So, how do I get around th开发者_Go百科is?

Am I approaching this from a completely wrong way?

To make things worse (or more interesting let's say), there might be further subclassing of topics and comments. For example:

YouTubeVideoTopic < AuthoredTopic
  + video
  + votes
  + comments: List<YouTubeComment>

YouTubeComment < AuthoredComment
  + votes

Finally, while Topic and AuthoredTopic might end up being interfaces or abstract classes, it'd be really nice if for concrete classes like YouTubeVideoTopic one could add comments to them (its addComment method should receive a YouTubeComment argument).

I will now explain the two approaches I've tried for solving this, none of them really successfull or convincing.

Approach 1: have multiple methods to return comments

The base topic class has an abstract getComments: List<Comment> method, while the AuthoredTopic class add an abstract getAuthoredComments: List<AuthoredComment> method, and finally the leaf classes like YouTubeVideoTopic have a concrete getYouTubeComments: List<YouTubeComment> while also implement their parents' get*Comments methods.

Code:

abstract class Topic {
    private String title;            

    public Topic(String title) {
        this.title = title;
    }

    public abstract List<Comment> getComments();

    // Other getters...
}

class Comment {
    private String text;

    // Constructor & getters
}

abstract class AuthoredTopic extends Topic {

    private String author;

    public AuthoredTopic(String title, String author) {
        super(title);
        this.author = author;
    }

    public abstract List<AuthoredComment> getAuthoredComments();

    @Override
    public List<Comment> getComments() {
        return Collections.<Comment> unmodifiableList(getAuthoredComments());
    }
}

class AuthoredComment extends Comment {
    private String author;
}

class YouTubeVideoTopic extends AuthoredTopic {

    int votes;
    String video; // Yeah.. 'video' is a string for now...
    List<YouTubeComment> comments = new ArrayList<YouTubeComment>();

    public YouTubeVideoTopic(String title, String author) {
        super(title, author);
    }

    public List<YouTubeComment> getYouTubeComments() {
        return Collections.unmodifiableList(comments);
    }

    @Override
    public List<AuthoredComment> getAuthoredComments() {
        return Collections.<AuthoredComment> unmodifiableList(comments);
    }

    public void addComment(YouTubeComment comment) {
        comments.add(comment);
    }
}

class YouTubeComment extends AuthoredComment {
    int votes;
}

It gets the job done, but as you can see it's a quite ugly solution. Not very DRY. The leaf classes like YouTubeVideoTopic have to implement quite a lot of stuff. Also, nested Topic subclasses end up having multiple get*Comments that are just noise in the design.

Approach 2: Add a generic type parameter

This approach I find much easier to implement:

class Topic<CommentT extends Comment> {

    private String title;
    private List<CommentT> comments = new ArrayList<CommentT>();

    public Topic(String title) {
        this.title = title;
    }

    public List<CommentT> getComments() {
        return Collections.unmodifiableList(comments);
    }

    public void addComment(CommentT comment) {
        comments.add(comment);
    }
}

class Comment {
    private String text;

    // Constructor & getters
}

class AuthoredTopic<CommentT extends AuthoredComment> extends
        Topic<CommentT> {

    String author;

    public AuthoredTopic(String title, String author) {
        super(title);
        this.author = author;
    }

    public String getAuthor() {
        return author;
    }

}

class AuthoredComment extends Comment {
    private String author;
}

class YouTubeVideoTopic extends AuthoredTopic<YouTubeComment> {

    int votes;
    String video; // Yeah.. 'video' is a string for now...

    public YouTubeVideoTopic(String title, String author) {
        super(title, author);
    }
}

class YouTubeComment extends AuthoredComment {
    int votes;
}

But, the downside of it is that the type parameters "leak" into the code that uses this clases, even if a wildcard can make the character overhead pretty small:

List<Topic<?>> t = getAllTopics();

Topic should not have a type parameter. A topic simply have comments. It doesn't matter, at the topic level, what the type of its comments is as long as they are Comments; it does not even matter if their type is homogeneous (some subclass of Topic, like YouTubeVideoTopic might only have YouTubeComments, but that does not matter at the base topic level).


This is called parallel hierarchies. The c2 wiki has a good discussion. Google search brings up good stuff too.

I agree that generic based approach is cleaner.


Just have the getComments method return List<? extends Comment>. Like so:

public class Topic {
    public List<? extends Comment> getComments() {
        return new ArrayList<Comment>();
    }
}

public class Comment {

}

public class AuthoredTopic extends Topic {

    @Override
    public List<? extends Comment> getComments() {
        return new ArrayList<AuthoredComment>();
    }
}

public class AuthoredComment extends Comment {

}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜