Flaw: Constructor does Real Work

前端 未结 6 1600
伪装坚强ぢ
伪装坚强ぢ 2020-12-17 04:15

I have a class which represents a set of numbers. The constructor takes three arguments: startValue, endValue and stepSize. The class

相关标签:
6条回答
  • 2020-12-17 04:36

    Concepts that urge you to keep your constructors light weight:

    • Inversion of control (Dependency Injection)
    • Single responsibility principle (as applied to the constructor rather than a class)
    • Lazy initialization
    • Testing
    • K.I.S.S.
    • D.R.Y.

    Links to arguments of why:

    • How much work should be done in a constructor?
    • What (not) to do in a constructor
    • Should a C++ constructor do real work?
    • http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

    If you check the arguments in the constructor that validation code can't be shared if those arguments come in from any other source (setter, constructor, parameter object)

    If you fill values into the collection or generate the information strings in the constructor that code can't be shared with other constructors you may need to add later.

    In addition to not being able to be shared there is also being delayed until really needed (lazy init). There is also overriding thru inheritance that offers more options with many methods that just do one thing rather then one do everything constructor.

    Your constructor only needs to put your class into a usable state. It does NOT have to be fully initialized. But it is perfectly free to use other methods to do the real work. That just doesn't take advantage of the "lazy init" idea. Sometimes you need it, sometimes you don't.

    Just keep in mind anything that the constructor does or calls is being shoved down the users / testers throat.

    EDIT:

    You still haven't accepted an answer and I've had some sleep so I'll take a stab at a design. A good design is flexible so I'm going to assume it's OK that I'm not sure what the information strings are, or whether our object is required to represent a set of numbers by being a collection (and so provides iterators, size(), add(), remove(), etc) or is merely backed by a collection and provides some narrow specialized access to those numbers (such as being immutable).

    This little guy is the Parameter Object pattern

    /** Throws exception if sign of endValue - startValue != stepSize */
    ListDefinition(T startValue, T endValue, T stepSize);
    

    T can be int or long or short or char. Have fun but be consistent.

    /** An interface, independent from any one collection implementation */
    ListFactory(ListDefinition ld){
        /** Make as many as you like */
        List<T> build();
    }
    

    If we don't need to narrow access to the collection, we're done. If we do, wrap it in a facade before exposing it.

    /** Provides read access only.  Immutable if List l kept private. */
    ImmutableFacade(List l);
    

    Oh wait, requirements change, forgot about 'information strings'. :)

    /** Build list of info strings */
    InformationStrings(String infoFilePath) {
         List<String> read();
    }
    

    Have no idea if this is what you had in mind but if you want the power to count line numbers by twos you now have it. :)

    /** Assuming information strings have a 1 to 1 relationship with our numbers */
    MapFactory(List l, List infoStrings){
        /** Make as many as you like */
        Map<T, String> build();
    }
    

    So, yes I'd use the builder pattern to wire all that together. Or you could try to use one object to do all that. Up to you. But I think you'll find few of these constructors doing much of anything.

    EDIT2
    I know this answer's already been accepted but I've realized there's room for improvement and I can't resist. The ListDefinition above works by exposing it's contents with getters, ick. There is a "Tell, don't ask" design principle that is being violated here for no good reason.

    ListDefinition(T startValue, T endValue, T stepSize) {
        List<T> buildList(List<T> l);
    }
    

    This let's us build any kind of list implementation and have it initialized according to the definition. Now we don't need ListFactory. buildList is something I call a shunt. It returns the same reference it accepted after having done something with it. It simply allows you to skip giving the new ArrayList a name. Making a list now looks like this:

    ListDefinition<int> ld = new ListDefinition<int>(3, 1, -1);
    List<int> l = new ImmutableFacade<int>(  ld.buildList( new ArrayList<int>() )  );
    

    Which works fine. Bit hard to read. So why not add a static factory method:

    List<int> l = ImmutableRangeOfNumbers.over(3, 1, -1);
    

    This doesn't accept dependency injections but it's built on classes that do. It's effectively a dependency injection container. This makes it a nice shorthand for popular combinations and configurations of the underlying classes. You don't have to make one for every combination. The point of doing this with many classes is now you can put together whatever combination you need.

    Well, that's my 2 cents. I'm gonna find something else to obsess on. Feedback welcome.

    0 讨论(0)
  • 2020-12-17 04:40

    If you want to use Microsoft's .NET Framework was an example here, it is perfectly valid both semantically and in terms of common practice, for a constructor to do some real work.

    An example of where Microsoft does this is in their implementation of System.IO.FileStream. This class performs string processing on path names, opens new file handles, opens threads, binds all sorts of things, and invokes many system functions. The constructor is actually, in effect, about 1,200 lines of code.

    I believe your example, where you are creating a list, is absolutely fine and valid. I would just make sure that you fail as often as possible. Say if you the minimum size higher than the maximum size, you could get stuck in an infinite loop with a poorly written loop condition, thus exhausting all available memory.

    The takeaway is "it depends" and you should use your best judgement. If all you wanted was a second opinion, then I say you're fine.

    0 讨论(0)
  • 2020-12-17 04:43

    The constructor is a special member function, in a way that it constructor, but after all - it is a member function. As such, it is allowed to do things.

    Consider for example c++ std::fstream. It opens a file in the constructor. Can throw an exception, but doesn't have to.

    As long as you can test the class, it is all good.

    0 讨论(0)
  • 2020-12-17 04:50

    It's not a good practice to do "real work" in the constructor: you can initialize class members, but you shouldn't call other methods or do more "heavy lifting" in the constructor.

    If you need to do some initialization which requires a big amount of code running, a good practice will be to do it in an init() method which will be called after the object was constructed.

    The reasoning for not doing heavy lifting inside the constructor is: in case something bad happens, and fails silently, you'll end up having a messed up object and it'll be a nightmare to debug and realize where the issues are coming from.

    In the case you describe above I would only do the assignments in the constructor and then, in two separate methods, I would implement the validations and generate the string-information.

    Implementing it this way also conforms with SRP: "Single Responsibility Principle" which suggests that any method/function should do one thing, and one thing only.

    0 讨论(0)
  • 2020-12-17 04:51

    As far as cohesion is concerned, there's no "real work", only work that's in line (or not) with the class/method's responsibility.

    A constructor's responsibility is to create an instance of a class. And a valid instance for that matter. I'm a big fan of keeping the validation part as intrinsic as possible, so that you can see the invariants every time you look at the class. In other words, that the class "contains its own definition".

    However, there are cases when an object is a complex assemblage of multiple other objects, with conditional logic, non-trivial validation or other creation sub-tasks involved. This is when I'd delegate the object creation to another class (Factory or Builder pattern) and restrain the accessibility scope of the constructor, but I think twice before doing it.

    In your case, I see no conditionals (except argument checking), no composition or inspection of complex objects. The work done by your constructor is cohesive with the class because it essentially only populates its internals. While you may (and should) of course extract atomic, well identified construction steps into private methods inside the same class, I don't see the need for a separate builder class.

    0 讨论(0)
  • 2020-12-17 04:56

    It's true, a constructur should do minimum of work oriented to a single aim - successful creaation of the valid object. Whatever it takes is ok. But not more.

    In your example, creating this collection in the constructor is perfectly valid, as object of your class represent a set of numbers (your words). If an object is set of numbers, you should clearly create it in the constructor! On the contrary - the constructur does not perform what it is made for - a fresh, valid object construction.

    These info strings call my attention. What is their purpose? What exactly do you do? This sounds like something periferic, something that can be left for later and exposed through a method, like

    String getInfo()
    

    or similar.

    0 讨论(0)
提交回复
热议问题