Libzypp/Design/Resolvable/BinaryCompatibility
From openSUSE
Binary compatibility
I personally don't mind it. Unfortunately others do. As we have to maintain it a long time, there will be changes in the future. No matter how much time we spend now to anticipate them, it will be something which requires changes to the interface. For Resolvables this is usually introducing new attributes.
A new attribute is either a new data item in Resovable, or a new virtual method in one of the interface classes above. Both breaks binary compatibility.
We can try to avoid this (although the future will invent other features we can't handle). Of course we'll have to pay for it.
- no new data member in the interface class
That's why class zypp::Resolvable basically looks like this:
class Resolvable
{
public:
// Vital and frequently used data are stored within and
// provided by Resolvable.
string name() const;
string edition() const;
...
private:
struct Impl;
base::ImplPtr<Impl> _pimpl;
};
All data members go into a private struct Impl. Access to data members can't be inlined any more, but has to has to be done in Resolvable.cc. That's the price, together with allocation and deallocation of Impl.
Changing struct Impl doesn't bother binary compatibility, because just a pointer to it is visible. Adding a new nonvirtual member to access the new data members is ok.
- no new virtual functions
That's somewhat expensive and inconvenient. Remember ResObject:
class ResObject : public Resolvable
{
public:
virtual string summary() const { return ""; }
virtual string description() const { return ""; }
...
};
We have to hide virtuality by putting it into some hidden class, similar to the approach of hiding the data of Resolvable. As the class hides the implementation interface I called it:
class ResObjectImplIf; // hide implementation interface
class ResObject : public Resolvable
{
public:
typedef ResObjectImplIf Impl;
public:
string summary() const;
string desciption() const;
private:
/** Access implementation */
virtual Impl & pimpl() = 0;
virtual const Impl & pimpl() const = 0;
}
// the same for package
class PackageImplIf; // hide implementation interface
class Package : public ResObject
{
public:
typedef PackageImplIf Impl;
...
private:
/** Access implementation */
virtual Impl & pimpl() = 0;
virtual const Impl & pimpl() const = 0;
}
Now we've built a facade. The former hierarchy for implementing a package was:
ResObject->Package->MyPackageImplementation
Now it's
ResObjectImplIf->PackageImplIf->MyPackageImplementation
The visible interface to access the object is still
ResObject->Package
What's left, is to connect facade and implementation. After creating a
new MyPackageImplementation;
We could construct the facade, and store the MyPackageImplementation* (which is a PackageImplIf* too) in class package, and pass it down to store it as well in ResObject (it's a ResObjectImplIf* as well). That way we have one pointer on each level, all pointing in fact to the same implementation object. Additionally all kind of sanity checks and maintenance work has to be realized on each level.
What checks? Which maintenance?
- E.g. not passing a NULL implementation, or taking care that there
is a default implementation available and used, if a NULL is passed. (incl. error reporting, exception handling)
- You may have noticed that the implementation hierarchy starts with
ResObjectImplIf. So where is the Resolvable? For several reasons it's in the interface, and has a private implementation class, which is not to be exposed or available for derivation.
Impact: We need a backlink from the implementation to the interface, to let the implementation access the objects resolvable data. A real Resolvable* (not ::Ptr) to avoid a reference cycle, otherwise the whole Object will never vanish.
Bonus: The backlink indicates whether the implementation part is already connected to an interface part. You can't accidentally connect the same implementation object to different Resolvables.
All this has to be realized on each level! Because each interface class could be the topmost one. Creating a ResObject, ResObject is responsible; creating a Package, Package has to check; and once we derive from Package The new classes will have to check.
That's a maintenance issue. If checks have to be changed for some reason, they have to be changed in all classes. We should not forget one, and we can't cover classes which are not part of zypp (even if we don't care about them now). If we introduce new classes, we must not forget to implement the checks there.
That's why I decided not to do it this way. And again, we have to pay for it ;-)
Instead of storing the pointer to implementation on each level, and having the checks on each level, each level has a pure virtual method pimpl() (private implementation), which provides access to the implementation via the topmost class in the current hierarchy.
It returns a Impl &, so the topmost class is in charge not to provide NULL implementations, and the interface classes can rely on this.
(still here, or time for a break ;-)
Obviously none of the current interface classes can be the 'topmost'. This would prevent us from extending the hierarch in the future. And Package is just a part of the complete Resolvable tree (Selection, Patches...).
As we need a class which fits on top, wherever the top is, it's template class. You won't find it, and if you do, you don't want to use it ;) (It's zypp::detail::_resobjectfactory_detail::ResImplConnect<class _Res>)
All it does is connecting an interface class derived from Resolvable to an implementation derived from ResObjectImplIf. This is the class which implements pimpl().
base::ReferenceCounted ^ Resolvable <---------+ ^ backlink ResObject +-----------ResObjectImplIf ^ ^ Package PackageImplIf ^ ^ ResImplConnect<Package> --------> MyPackageImplementation
As I said, de-virtualizing the interface is somewhat expensive. Expensive in terms of writing lines of code. Introduce a new Package attribute:
Package.h string newattr() const;
Package.cc string Package::newattr() const
{ return pimpl().newattr(); }
PackageImplIf.h virtual string newattr() const =0;
or
virtual string newattr() const
{ return ""; /*or whatever is appropriate*/ }
MyPackageImplementation.h Must implement (in case of =0;), or may overload to provide the real value.
Runtime penalty is not as much as one may assume. The ImplIf hierarchy is what we'd need anyway. The base::ReferenceCounted would be needed too.
Memory: The visible interface (Resolvable -> Package) contains no data, ResImplConnect<Package> just a smart pointer to MyPackageImplementation. Plus 2 entries in the vtable per lever for pimple(). But the vtabe is global, not per object. Thus a pointer per object.
Runtime: One function call because we can't inline, and one virtual call for pimpl(). Not that much either.
Last edit in Trac '11/24/05 18:32:39' by 'kkaempf'
Last edit in Trac '11/24/05 18:32:39' by 'kkaempf'
Last edit in Trac '11/24/05 18:32:39' by 'kkaempf'
Last edit in Trac '11/24/05 18:32:39' by 'kkaempf'

