4
\$\begingroup\$

Ensuring that some logic is always being run before the user's overriding methods.

I'm writing a library and I have some abstract classes that can be binary serialized (and users can subclass them and further implement their serialization logic) e.g.

abstract class Base
{
    protected int id;
    public virtual void Serialize(BinaryWriter writer)
    {
        writer.Write(id);
    }
}



class SomeonesClass : Base
{
    public override void Serialize(BinaryWriter writer)
    {
        base.Serialize(writer);
        // ...
    }
}

Now, because writing the id is so important for all Base objects, I want to ensure (force) that writer.Write(id) gets called even in the subclasses written by users. So I don't even want to give them the option of calling base.Serialize(writer).

One way that I've done it was to split it into two methods:

abstract class Base
{
    protected int id;
    public void Serialize(BinaryWriter writer)
    {
        writer.Write(id);
        InternalSerialize(writer);
    }
    protected virtual void InternalSerialize(BinaryWriter writer) { }
}



class SomeonesClass : Base
{
    protected override void InternalSerialize(BinaryWriter writer)
    {

    }
}

The user could still avoid it by overriding Serialize using the new keyword etc, but at that point I'm fine to accept it.

Is this the best approach to achieve my intended design? The design being: send a very strong message to the users of the library that the id must be the first thing to be written.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ By the way, if you are interested in what a modern serialization library looks like, I can only recommend studying the Serde crate for (and written in) the Rust programming language. It has some pretty interesting features: 1) If you want to make your data type serializable, you only need to implement some simple interfaces, and your type will work with any serialization format without you having to know anything about the serialization formats. 2) If you want to implement a serialization format for Serde, you only need to implement some … \$\endgroup\$ Commented Jan 7, 2023 at 1:14
  • 1
    \$\begingroup\$ … simple interfaces, and your serialization format will work with any serializable type without you having to know anything about the types. 3) There is no runtime reflection, figuring out how to serialize your custom type and how to generate the custom serialization format, all happens at compile time with no runtime overhead. 4) For simple composite types, e.g. structs, Serde can automatically generate the implementation of the serialization and deserialization interfaces for you, at compile time. 5) Of course, both the serialization for the data type and the serialization format are … \$\endgroup\$ Commented Jan 7, 2023 at 1:17
  • 1
    \$\begingroup\$ … implemented as a separate, third-party module, you never have to add or modify something in Serde itself. If you have 2 hours time, I can only recommend the video linked on the Serde homepage. \$\endgroup\$ Commented Jan 7, 2023 at 1:19
  • \$\begingroup\$ @JörgWMittag thank you! Serde looks surprisingly robust and refreshing. Will look into it! \$\endgroup\$
    – Saturn
    Commented Jan 7, 2023 at 5:24

1 Answer 1

6
\$\begingroup\$

Basically you have implemented the template method design pattern.

template method uml

In your case your Serialize method acts as the template method. It contains certain commands (before and/or after) the customisation points (usually referred as steps).

So, the answer for your question is yes, you have implemented it in a correct way.


Just a tiny note: It might make sense to declare your InternalSerialize as abstract rather than virtual. Also you could work on its naming to better express your intent.

\$\endgroup\$

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.