File pointers are not the only way to access files on POSIX systems (which, obviously Linux is).
As a matter of fact, the FILE
struct exists to make it easier to work with files in a cross-platform manner (fopen()
will work on both Linux, Mac OS X, any BSD and Windows) and to provide some facilities like transparent buffering (buffered I/O is way faster).
This is all usually good, unless it's counter-productive (you need the file to be synced for common access from many processes) or dangerous (buffered output might not be flushed when necessary and it might leave the file in a bad state if interrupted improperly). Now, I'm not an expert and these issues might be nonexistent, but there certainly are reasons to use the lower-level API provided by POSIX: the file descriptor mechanism, open()
, read()
, write()
, close()
and mmap()
.
These functions are the building blocks of the FILE*
abstraction, under Linux, at least (you can test that by running a sample file I/O program with strace
) and you can use them if you want direct access to a file.
This is probably the solution to the problem the interviewer was expecting. The reason he might want you to know this is if you were interviewed for a job in embedded systems; knowledge of the underlying system is essential in such a position.
I once met a Linux programming guru, who said to me that he thinks a person may claim he knows C when he can implement the whole standard library by himself on at least one platform without any significant difficulties. In certain domains, low-level knowledge is a must.
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.
Best Answer
When seeing a class with a constructor signature like
I think it is pretty obvious that this constructor will read the given file (and so need some time), and it should not be to hard to understand that the caller has to care for possible exceptions from this (because file IO can fail). So despite what other answers are saying, this design is ok.
Don't get me wrong, in the future you might get requirements where this design might not be sufficient any more, but as long as this simple interface and behaviour is all your program needs, I would stick to the KISS and YAGNI principles and avoid overcomplicating things by providing a separate initialization method "just in case".