1

A typical introductory example to OOP, classes, and constructors is object Car, with properties such as float fuel, bool is_engine_running, etc etc, and a class and constructor definition might be as follows (I'm using a Java-esque syntax here, but it works in any OOP language):

class Car {
    float fuel_remaining;
    bool is_engine_running;
    // ... other fields

    Car(float fuel, bool is_engine_running, ...) {
        this.fuel_remaining = fuel;
        this.is_engine_running = is_engine_running;
        // Other assignments
    }
}

Most examples of constructors I've seen (admittedly not many, as I haven't dug through the sources for large Java/C++ projects yet) tend to do simple this.x = x assignments. I would like to implement a class representing UUIDs (I know there already exists various built-in/third-party libraries dealing with this; this is a custom namespace-based UUID for a client with specific requirements) with the UUID itself internally represented as a 128-bit (or 32-byte) array. I am considering C++'s std::bitset<128> for this, or any equivalent in other languages.

When I initialise the UUID object, I would think that valid constructors would accept, amongst others, the current time, the variables (typically strings) from the namespace, some hardware address (also possibly a string), or even a UUID string itself; for instance, "9fbaea6e-a929-4833-a802-9d64ac432126". Otherwise, I would have to directly provide a bit/byte array for the constructor to do something like this.uuid_bits = uuid_bits, which I feel is rather pointless, because it removes any abstraction whatsoever and leaks the internal bit-array representation to the caller.

Therefore, my question is this: The above UUID class will require some string-parsing, bit-shifting, etc to properly initialise any object while still abstracting away the internal representation. Is it hence an anti-pattern to do more work than merely trivial assignment in the constructor? If not, how might I separate out this 'extra work'? Would I perhaps write and use private static functions/methods to further process the arguments to the constructor, and return the appropriate internal representation, so that the constructor itself only does an assignment?

0

2 Answers 2

1

It very much depends on your class, what it does, how you expect to use it, and what kind of performance optimization you're looking for.

If you're looking for the constructor to trigger some external work, e.g. a cleanup of the file server, that is not appropriate. But I suspect you're thinking more along the lines of calculations of values that are relevant to the currently constructed class' lifetime.

The first approach is to calculate your values once, in the beginning. This means that you don't have to repeat the same calculation whenever the data is requested.

For the examples, I'm going to use taking the first letter of a string as "the calculation". It's of course a trivial amount of effort, but it's merely intended as a simple example of a value calculation of any level of complexity.

public class Person
{
    public string Name { get; private set; }

    public char FirstLetterOfName { get; private set; }

    public Person(string name)
    {
        this.Name = name;
        this.FirstLetterOfName = name[0];
    }
}

This is straightforward, and leads to increased performance on repeated usage, at the cost of initial performance when initializing the object.

The second approach is to defer the execution of the value until it is asked. This means you don't waste time during initialization, especially if you never end up fetching this specific value. But if you fetch the specific value multiple times, you'll lose performance by redoing the same calculation.

public class Person
{
    public string Name { get; private set; }

    public char FirstLetterOfName
    { 
        get { return this.Name[0]; }
    }

    public Person(string name)
    {
        this.Name = name;
    }
}

The third approach tries to have it both ways: deferred execution, and caching the result so the execution is not repeated.

public class Person
{
    public string Name { get; private set; }

    private char? _firstLetterOfName;
    public char FirstLetterOfName
    {
        get
        {
            if(_firstLetterOfName == null)
                _firstLetterOfName = this.Name[0];

            return _firstLetterOfName;
        }
    }

    public Person(string name)
    {
        this.Name = name;
    }
}

This approach maximizes performance both during initialization and during repeated usage, but it comes at the cost of a more complex implementation, which affects readability and maintainability, in a "straw that broke the camel's back" sense.


So, depending on which approach you take, you can enhance:

  1. Performance during initialization
  2. Performance during repeated usage
  3. Code readability and maintainability

You can't have it all, but you can have two out of three. Take your pick.

  • 1 + 2 => third approach
  • 1 + 3 => second approach
  • 2 + 3 => first approach
2
  • " I would like to implement a class representing UUIDs"
    – Ewan
    May 28, 2021 at 10:36
  • 1
    @Ewan "For the examples, I'm going to use taking the first letter of a string as "the calculation". It's of course a trivial amount of effort, but it's merely intended as a simple example of a value calculation of any level of complexity."
    – Flater
    May 28, 2021 at 10:40
-1

Problems with logic in constructors:

  1. It's considered bad practice for constructors to throw exceptions
  2. Constructors are not inherited, so you cant mock or dependency inject them
  3. Calling code often doesn't control when they are called eg deserialisation or binding
  4. They are not optional, so lists of objects or copies etc are problematic

However, in your case you have a reasonably good argument for a constructor. The logic will be a simple function of the input parameters, unlikely to throw exceptions, unlikely to require inheritance or injection etc.

Although, if we check what microsoft does, they go with the static method approach

Guid.NewGuid()

This suffers from lots of the same issues, but again there is a clear spec for Guids that's unlikely to change in a breaking way and there is precedence for such things in Math.Whatever() functions.

Probably the best way to go is to have a builder object

var b = new GuidV3Builder();
var guid = b.Generate(params);

Now you can separate the various versions, have a testable interface, obey SOLID and also have a clear way to implement things like sequential GUIDS

29
  • 2
    The arguments listed are a bit, well, arguable. (1) There is a subset of developers who favor never allowing an invalid state to exist, e.g. if 1234 is not a zip code, then new ZipCode("1234") shouldn't pretend like it is. (2) Constructors are uniquely scoped to only their class, they have no shared nature. Inheritance makes no sense here, nor does mocking. Using a real class requires a real constructor, as per the purpose of a class' constructor. (3) Calling code doesn't know the logic behind properties either but that's not an argument against having some logic in a property.
    – Flater
    May 28, 2021 at 10:16
  • 1. they are wrong 2. exactly, this is a problem if you want to test something that depends on that logic or inject it. 3. it is
    – Ewan
    May 28, 2021 at 10:32
  • 1
    (1) Try justifying your position instead of just claiming anyone who doesn't agree with you is wrong. (2) If you ever think to yourself "I need this constructor to be mocked while using the actual class itself", then you've got a really skewed idea on what the purpose of a constructor is. And as far as injection goes, hence the constructor parameters? (3) Same as (1). Note that I said some logic. For example, a simple null check, or a simple cleaning algorithm (for e.g. profanity, whitespace tidying, ...). I didn't say "any logic, no matter the size or complexity".
    – Flater
    May 28, 2021 at 10:38
  • 1. zipcode validation changes. you cant read any of the old zipcodes from your database because the constructor fails. 2. the point is you want to be able to swap out the logic, tightly coupling to the class prevents this. 3. you admit its a problem with big logic, but its not a matter of length of code is it, you could have a single problematic line. Hence any logic is "problematic" ie a potential problem.
    – Ewan
    May 28, 2021 at 10:44
  • (1) It makes no sense to want to parse invalid values when already applying the hardline stance that you refuse to acknowledge their existence. You're pointing out how a half-applied rule is inconsistent, which is tautological (2) Tight coupling is irrelevant for a constructor, by definition of what a constructor is. The point of a constructor is to have rigorously fix the required initialization logic. If any subset of that logic is swappable or injectable, that's what you have constructor parameters for. (3) You seem to have glossed over me mentioning "size or complexity".
    – Flater
    May 28, 2021 at 11:09

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