Re: Visibility
A public method signals the design intention that it is visible to all - if this isn't the designed intention, change the method's visibility accordingly, e.g. protected
(but obviously any subclass has access), or if all classes which are allowed to use Invoke
are in the same assembly, then Invoke
can be declared protected internal abstract
.
Re: Sealed
As per Lasse's point, sealed override
methods will disrupt the polymorphic virtual / override
chain during inheritance, but still, it can not change the fact that the base method is public. However, applying sealed
to a class will prevent other classes from inheriting it all, thus restricting access to all protected methods.
Solution
I believe the underlying problem relates to over-using inheritance - seemingly you want to inherit functionality to obtain reuse, but at the same time need to restrict access at some point in the chain to an "untrustworthy" subclass. Other than the point about making methods internal
+ moving all "trustworthy" subclasses into the base class assembly, you will have little control when using a full chain of subclasses.
I believe that decoupling your hierarchy via interfaces, and applying the principle of composition over inheritance, will better achieve what you are after. In fact, the Decorator pattern looks to be an option here.
You can also set the 'trustworthiness' boundary by making the 'last trustworthy' subclass (ExecutableJobPlugin
) as sealed
*.
Example:
// Expose just what is visible to your final Subclass on the interface
public interface IExecutableJobPlugin
{
bool IsActive { get; set; }
void CheckParameter();
void Initialize(IDictionary parameters);
}
// Sealed will prevent other classes from inheriting this class.
public sealed class ExecutableJobPlugin : JobPlugin, IExecutableJobPlugin
{
// Default implementation. NB, not abstract
public void Initialize(IDictionary parameters) {}
// This isn't visible on the interface
protected override sealed void Invoke(IDictionary parameters)
{
//final realization of Invoke() method
}
}
public class ConcreteExecutablePlugin : IExecutableJobPlugin
{
// Compose a decoupled IExecutableJobPlugin instead of direct inheritance
private readonly IExecutableJobPlugin _wrappedJobPlugin;
public ConcreteExecutablePlugin(IExecutableJobPlugin wrapped)
{
_wrappedJobPlugin = wrapped;
}
// Invoke() isn't on the interface so cannot be accessed here
public void Initialize(IDictionary parameters)
{
// Call 'super' if needed.
_wrappedJobPlugin.Initialize(parameters);
//concrete plugin initialization code here ...
}
public bool IsActive
{
get { return _wrappedJobPlugin.IsActive; }
set { _wrappedJobPlugin.IsActive = value; }
}
public void CheckParameter()
{
_wrappedJobPlugin.CheckParameter();
}
}
Notes
- Because
ConcreteExecutablePlugin
is no longer a subclass of PluginBase
, if you change method PluginBase.Invoke
to protected
, that ConcreteExecutablePlugin
will have no access to it (aside from hacks like reflection).
- All 'reused' methods and properties needed from the composed (née base) class
ExecutableJobPlugin
need to be rewired in the ConcreteExecutablePlugin
. Although somewhat tedious, it does allow for additional interception, e.g. cross cutting concerns like logging.
- The
ExecutableJobPlugin
class may no longer be abstract, since an instance will be needed for the composition to work.
- Ideally, the
ExecutableJobPlugin
should be injected externally (as opposed to new
within)
- Decoupling via interfaces improves the testability of your class hierarchy
*
Sealing ExecutableJobPlugin
won't however prevent others from subclassing public superclasses like PluginBase
and JobPlugin
. To prevent this, you could keep all the base classes in the same assembly and mark these as internal
, or continue to apply the interface decoupling / Decorator pattern instead of inheritance across the entire chain.
The pattern could obviously be repeated for multiple levels of your class hierarchy, and the interface segregation principle should be applied to ensure that your interfaces remain lean and focused.