3

Suppose I'm writing some C++ code to visualize "Foo" objects. I have two ways of getting a "Foo": computing it from data, or from taking the pieces of a precomputed "Foo" and building a new "Foo".

Now, once a "Foo" is computed it's guaranteed to be good for visualization, but changing it may break this assumption. Therefore, I've decided to represent "Foos" in my code by a Foo class that has no mutating methods: once it is constructed and initialized, it doesn't change.

But there's a second way to make a "Foo": build it from a precomputed "Foo"'s components. I've come up with several methods of building a Foo from precomputed data:

Method 1: Constructor/Static methods

Perhaps the most obvious method would be to add a new constructor or a static method to Foo, call itfromPrecomputed, that would read the components of the precomputed Foo and make a new Foo object, checking that it is valid. To explain why I'd like to shy away from this, I have to complicate my example: Let's say that one component of a "Foo" is a collection of "Bars". Now, in terms of implementation, sometimes a "Bar" is represented as a std::vector<std::vector<Bar> >, sometimes as a Bar array[][2], sometimes as a std::vector<std::pair<Bar,Bar> >, and so on... I could have the user reorganize their data into a standardized form and have a single constructor for this standard, but this might require the user to perform an extra copy. I don't want to provide a static method for each format: readPrecomputedFormatA, readPrecomputedFormatB, and so on: this clutters the API.

Method 2: Make Foo mutable

If I exposed the addBar(Bar) method of Foo, then I could allow the user to iterate over their collection of "Bars" in their own way. This, however, makes Foo mutable. So I could compute a Foo that makes sense for visualization, then use addBar to add a Bar that makes the Foo no longer a "Foo". Not good.

Method 3: Make a friend "builder" class

I make a class called FooBuilder which has the addBar(Bar) method exposed. I make FooBuilder a friend of Foo and add a constructor to Foo that takes a FooBuilder. On calling this constructor, it checks to make sure that FooBuilder contains a valid "Foo" object, then swaps its empty representation of a Foo with what is inside the FooBuilder. Everybody is happy.

The only "messiness" about method #3 is that it requires a friendship, but it's worth it to maintain encapsulation I think. But this has got me thinking: is this an established pattern? Or is there another, better way of doing this that I don't know about?

2
  • Why should Foo be aware of how Bar is represented?
    – mouviciel
    Commented Jan 21, 2014 at 14:30
  • @mouviciel In what I'm writing, a Foo is a special type of graph. I want to allow the user to build a precomputed graph, which means specifying the edges it contains. Internally, Foo has a representation of an edge object and a container for storing edges, but the user just has to specify a 2d array of integers. But there are many different ways that the user can represent a 2d array, and I don't want to force the user to make a copy of his/her data to conform to my special standard. It isn't as simple as having an addEdge(int,int) method, since the tree shouldn't be mutable.
    – jme
    Commented Jan 21, 2014 at 14:46

1 Answer 1

9

What you consider FooBuilder and Foo is actually part of a well-established Builder pattern. The other approach you mention with creating a Foo instance based on an existing instance is called a Prototype pattern. Both of these are well-known object creation alternatives and several books (most notably "Design Patterns", aka GoF) have been written describing them. (not sure I'm clear on your second mutable example, so comparing #1 to #3 here)

Each pattern obviously has its own advantages and drawbacks and only you can decide which one is better for your specific situation. For example, I would typically use builders when there's a lot of different ways of initializing Foo. For example, right now I'm working on a query builder class and many clients use it differently to build query objects. Different clients want different fields to be returned, some clients want only last 10 rows, some want sorting while others don't. Builder is perfect as it can look something like this:

qb = QueryBuilder()
query = qb.fields("name", "address")
          .sort("zip", "asc")
          .limit(10)
          .build()

So multiple setters, each returning instance of the builder itself so you can daisy-chain basically serve as a very flexible constructor. Making one class a friend of another class has it's time and a place and potentially this could be one of those places.

Alternatively, you could define Foo as having one constructor that takes a rather complex set of params, or potentially you could define another class FooData, that FooBuilder could initialize and pass into Foo creation. Then again, maybe Foo and FooBuilder being friends in this case isn't such a bad thing. Just keep in mind that when one class is a friend of another one, you are expanding encapsulation boundary, which could be just as bad as putting more and more code all within one class (i.e. more code knows about internals)

At the end of the day, you could be thinking and considering all these different options and it will all boil down to 60/40. If there's no clear winner, you can always let a coin pick the winner and just go with it. Most likely either one of the design choices would work just fine and by going through the motions and seeing your code in action you will learn valuable lesson for future. Sometimes when I have design decisions and can't decide between A and B, I could end up picking B and then if I ever come across a similar situation I would intentionally pick the other choice. The work in either case gets done and I get the software to do what I want, but you get a ton of benefit of actually seeing your decisions in action and being able to compare the two working approaches, rather than just discussing and thinking about them.

5
  • Interesting, thanks. I think the builder pattern is what I'm after. I'm particularly interested in the case where Foo contains a large amount of data. Say a Foo has a private data member of type Data* which, points to a really big object. So I'll make FooBuilder, also with a private Data*. When I call FooBuilder::build(), it will make a new Foo object and swap its Data* with the Data* it contains (where are Data* are really smart pointers...). So I need a friend relationship, right, unless I want to expose the implementation detail that Foo has a Data* in a constructor.
    – jme
    Commented Jan 21, 2014 at 15:07
  • @jme: ... or as I suggested, if Data is a separate class, FooBuilder could just pass it into Foo's constructor. Then you could do the whole thing without having to make them friends, right?
    – DXM
    Commented Jan 21, 2014 at 16:04
  • So I'd have three classes: Foo, FooData, and FooBuilder. Foo has a FooData* in its implementation. FooBuilder makes a FooData and passes a pointer to the constructor of Foo. My question: FooData's constructor needs to be public so that FooBuilder can initialize it. So what prevents the users of my library from initializing their own FooData object and constructing a possibly invalid Foo, besides me not telling them that FooData exists in the documentation?
    – jme
    Commented Jan 21, 2014 at 17:29
  • @jme: there's only so much protection and bullet proofing you can offer without making things more complicated then they need to be. In situations like this, I like to ask, "if I give the user a shotgun, what prevents him from blowing off his own foot." :) If someone really wants to go crazy and initialize their own FooData, maybe they know what they are doing and they need "an alternate builder" for their specific situation or maybe they are going to blow off a finger. Either way, you gave them and documented a simple, safe way to use your library. Everything else is up to them.
    – DXM
    Commented Jan 21, 2014 at 17:50
  • Fair enough! When my research slows down and I have more time, I am going to read some of the references on design patterns you mentioned in your answer. I'm always wondering if the code I've written is the "correct" way to go about something, perhaps at the detriment of productivity.
    – jme
    Commented Jan 21, 2014 at 20:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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