Re: [Vala] Final / Sealed classes in Vala



Hi Jan,

Jan Hudec wrote:
Just like my earlier example with inheriting string, similar reasons apply to
inheriting concrete collection classes. For example a collection of options
would make perfect sense to be a collection plus special constructor or
initialization method to load it from a file and maybe a method to save it
back. Or a collection of headers would make perfect sense to be a collection
that does checks keys are valid identifiers on insertion.

IMHO, for both you examples, composition would be better design choice. For the all those reasons: encapsulation, reuse and maintainability.

Both these can *almost* be done by delegation, except
 - it's a lot more typing, so users will hate you for that, and

Wrong. With the support for events in gee you could do (exact API not yet defined so please consider this as it is, some pseudo code :p) :

Example 1: Options

public class Options {
        private string _filename;
        private bool _automatically_save = false;
        private Set<string> _options;
        private bool _changed = false;

        public Set<string> options { get { return _options; }

        public Options (string filename, bool automatically_save) {
                _options = new HashSet<string> ();
                _filename = filename;
                _automatically_save = automatically_save;
                try_load ();

                _options.change.connect (() => {
                        _changed = true;
                });
        }

        ~Options () {
                if (_automatically_save) {
                        commit_changes ();
                }
        }

        // Rest of code would be the same as with inheritance

        public void commit_changes () {
                if (_changed) {
                        // ...
                        _changed = false;
                }
        }

        // ...
}

Example 2: Headers

public class Includes {
        private List<string> _headers;

        public List<string> headers { get { return _headers; }

        public Includes () {
                _headers = new ArrayList<string> ();
                _headers.insertion.connect ((index, header) => {
                        check_valid_header (index, header);
                });
        }

        // ...
}

 - if some other interface expects the base class, you have to give it the
   delegee, but you loose any additional checking.

I guess you obviously understand that by the above design this statement is simply wrong, as we don't use delegation.

You can argue, that the second case should not happen, because any API should
take the interface, not the class, but all this is about user's who don't
know better, because those who do won't complain when they break things with
incorrect overriding.

'sealed' also for that purpose as noted Micheal. It has documentation and educative roles. 'sealed' on a class also means: "Hey you wanted to subclass me ? Guess what ? There is another possible design. So please stand back and think more about your needs and how you will achieve them!"

So you can persuade me to agree it makes sense to declare, and ensure, that
particular virtual methods must not be overriden (carefuly -- my second
example involves overriding add!), but I don't buy the argument that the
class as a whole may not be inherited.

I think you mismatch things here. I'm a fervent supporters of design patterns and the first thing I've read about it is that most of the time composition is to be preferred to inheritance. The latter only brings too deep class hierarchies, unmaintainable code and, what is the most important defect for me, confusion in the head of your users (why the hell are there so much collection-unrelated methods on that collection ? what does check_header has to do in a collection class ? ...)

There are Abstract* abstract classes available if one wants to start
a new collection implementation.

I don't want to. I want to add some auxiliary functionality or additional
semantic checks to the otherwise completely standard collection
implementation.

Then compose, by using the observer, wrapper or bridge design patterns in an appropriate manner depending on your use case.

In anyway, you don't have to interfere with the design and implementation choices made by the developers of the libraries you use. ('sealed' as a mean to avoid encapsulation failures)

Also it is worth to note that enabling sub-classing on concrete
implementations (like it is now) allows a possible failure because
of the badly-design GObject construction. Thanks to Jakob Kroon to
have pointed that to me.

The GObject system is somewhat strange and it is a little tricky to get the
usage right, but it is possible to implement the classes so they are
always constructed correctly.

Unfortunately this clashes with correct generics initialization. Please test that by yourself.

And by the way, it has nothing to do with inheritance -- there are many other
cases where creating objects via GLib.Object.new is useful.

For sure, reflective instantiation can be very useful, but not for containers as the choice of the container is made by its immediate user which chooses the appropriate collection type for the things he has to store in it.

Note, that vala makes implementing classes correct w.r.t. GLib.Object.new
trickier because it sets construct properties and calls base constructor in
the construction method body instead of using dedicated syntax like C++ and
C# do.

The fact is that we highly rely on creation method chain-up which can not be realized through GObject-style constructor chain up (as there is no parameter passing between them).

That's cool that Jürg announced we will have support for 'sealed' as we clearly need it.

Best regards, Didier.





[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]