Property access for subclasses only


(James Dempsey) #1

I have a class in a framework that is expected to be subclassed in modules outside of the framework. I’d like to do the following:

1. I want some properties of that class to be accessible by subclasses outside of the framework.
2. I don’t want these properties of that class to be accessible by any piece of code outside of the framework because I don’t want dependencies on these properties to propagate beyond the subclass.

Am I correct in thinking that there is no way to achieve both #1 and #2 in Swift?

The only way I know to achieve #1 is to make the property public or open. And that makes the property public to everyone which makes #2 impossible.

It seems like the best I can do is put a big comment on each property that says something like:
// NOTE: FOR SUBCLASSERS ONLY! DO NOT USE EXCEPT IN A SUBCLASS!

But that does not seem like a very Swift-y way of doing things.

It’s quite possible I’m misunderstanding something - is there a way to do what I am trying to do, or a better way to achieve this?

Thanks,

James

···

—————————
James Dempsey
dempsey@mac.com


(Brent Royal-Gordon) #2

I have a class in a framework that is expected to be subclassed in modules outside of the framework. I’d like to do the following:

1. I want some properties of that class to be accessible by subclasses outside of the framework.
2. I don’t want these properties of that class to be accessible by any piece of code outside of the framework because I don’t want dependencies on these properties to propagate beyond the subclass.

Am I correct in thinking that there is no way to achieve both #1 and #2 in Swift?

The only way I know to achieve #1 is to make the property public or open. And that makes the property public to everyone which makes #2 impossible.

No, there is no way to do this.

This reflects a judgement on the part of Swift's designers that `protected` access is not very useful, because you can always get around it by creating an extension on the type and exposing the API under a different name. At the same time, it prevents developers from structuring their subclass code freely; code which would be better moved to free functions or helper types instead has to remain in the subclass solely to access protected symbols.

This judgement is not uncontroversial, and gets rehashed on swift-evolution at least once a year.

It seems like the best I can do is put a big comment on each property that says something like:
// NOTE: FOR SUBCLASSERS ONLY! DO NOT USE EXCEPT IN A SUBCLASS!

You can do a little bit better by putting the methods in an ancillary "sharps drawer" type:

  open class MyClass {
    open func publicThing() { … }
    
    public struct SubclassOnly {
      fileprivate var instance: MyClass
      
      public func protectedThing() { … }
    }
    
    var subclassOnly: SubclassOnly { get { SubclassOnly(instance: self) } }
  }

This is still sort of the "honor system", but it at least makes it impossible to accidentally access something you shouldn't.

If you want the protected methods to only be used from inside certain override points, you can actually enforce that by creating a type which can only be constructed internally, requiring that type be passed to the methods you want to protect, and only passing it to your override points. For example:

  open class MyClass {
    public struct ProtectionToken {
      fileprivate init() {}
    }
    
    // Instead of overriding this...
    public func publicThing() {
      overridablePublicThing(protected: ProtectionToken())
    }
    
    // Subclasses override this.
    open func overridablePublicThing(protected: ProtectionToken) { … }
    
    // You can only call this if you have a protection token, and you can
    // only get a protection token if the class gives it to you.
    public func protectedThing(protected: ProtectionToken) { … }
  }

This is a heavyweight solution, but if you really care strongly about who's allowed to call your methods, it'll do the trick. And it allows the kind of restructuring that `protected` forbids, since once you have a protection token, you can pass it to another method or type to delegate your access. You can also have several different kinds of protection tokens and do various other clever things.

···

On Mar 17, 2017, at 11:54 AM, James Dempsey via swift-users <swift-users@swift.org> wrote:

--
Brent Royal-Gordon
Architechies


(James Dempsey) #3

I have a class in a framework that is expected to be subclassed in modules outside of the framework. I’d like to do the following:

1. I want some properties of that class to be accessible by subclasses outside of the framework.
2. I don’t want these properties of that class to be accessible by any piece of code outside of the framework because I don’t want dependencies on these properties to propagate beyond the subclass.

Am I correct in thinking that there is no way to achieve both #1 and #2 in Swift?

The only way I know to achieve #1 is to make the property public or open. And that makes the property public to everyone which makes #2 impossible.

No, there is no way to do this.

This reflects a judgement on the part of Swift's designers that `protected` access is not very useful, because you can always get around it by creating an extension on the type and exposing the API under a different name. At the same time, it prevents developers from structuring their subclass code freely; code which would be better moved to free functions or helper types instead has to remain in the subclass solely to access protected symbols.

Thank you for your reply Brent, I appreciate it as well as your suggested workarounds. My reply below is not meant to be argumentative with you - no need to reply. I’m just responding with my current thoughts:

So the state of the world is pretty much the same as the initial rationale (except for the bit about conflating protection levels with inheritance concepts, that ship seems to have sailed with the open keyword).

Note that I really only care about properties, not methods.

It seems very strange that there is no way for a subclass in a different module to access its superclass’s internal state without completely breaking encapsulation by making the state completely public and available to all code in all importing modules.

With regards to structuring subclass code freely, allowing property access to any code in the subclasses’ file (similar to fileprivate allowing file-wide access) would allow for free functions and helper types. That seems like a reasonable approach to overcome this objection.

The other premise is that a subclasser might go out of their way to circumvent encapsulation, so why bother putting any protection in place at all? (The ‘some people will commit crime anyway, so why make it illegal?’ premise)

One reason is that by forcing all exposed properties to be public to everyone, the only way to determine the intended usage is to look at comments or documentation to realize that you should not be accessing that property from outside a subclass. If you are working in Xcode, it will happily code-complete things that you are not intended to have access to. Since there is no way to express that intent in the language, incorrect usage is effectively encouraged by the tools.

It also makes this sort of misuse much more difficult to catch in code review. It is relatively easy to spot code that is going through some gyrations to re-expose things which should not be exposed. It is much more difficult to realize that a particular property access should not be allowed.

Basically, the limitation prevents expressing the intent in the language, and so the compiler and other tools can’t help enforce the intent or even make the intent known.

Possibly ‘protected’ is the wrong way to describe these. Maybe something like ‘classpubllc’ would better express the intent, but also the reality that absolute protection can't be guaranteed against wily API re-exposers.

As you say, it is not an uncontroversial judgment.

Thanks again for the suggested workarounds - I have some comments about them below.

James

This judgement is not uncontroversial, and gets rehashed on swift-evolution at least once a year.

It seems like the best I can do is put a big comment on each property that says something like:
// NOTE: FOR SUBCLASSERS ONLY! DO NOT USE EXCEPT IN A SUBCLASS!

You can do a little bit better by putting the methods in an ancillary "sharps drawer" type:

  open class MyClass {
    open func publicThing() { … }
    
    public struct SubclassOnly {
      fileprivate var instance: MyClass
      
      public func protectedThing() { … }
    }
    
    var subclassOnly: SubclassOnly { get { SubclassOnly(instance: self) } }
  }

This is still sort of the "honor system", but it at least makes it impossible to accidentally access something you shouldn’t.

Yes, I had considered something like this, but adding the additional code complexity of the of level indirection to "kind-of” workaround a problem in the language has made me too sad to implement it yet. (I might still be at ‘bargaining’ but will probably get to sad ‘acceptance’ soon.)

If you want the protected methods to only be used from inside certain override points, you can actually enforce that by creating a type which can only be constructed internally, requiring that type be passed to the methods you want to protect, and only passing it to your override points. For example:

  open class MyClass {
    public struct ProtectionToken {
      fileprivate init() {}
    }
    
    // Instead of overriding this...
    public func publicThing() {
      overridablePublicThing(protected: ProtectionToken())
    }
    
    // Subclasses override this.
    open func overridablePublicThing(protected: ProtectionToken) { … }
    
    // You can only call this if you have a protection token, and you can
    // only get a protection token if the class gives it to you.
    public func protectedThing(protected: ProtectionToken) { … }
  }

This is a heavyweight solution, but if you really care strongly about who's allowed to call your methods, it'll do the trick. And it allows the kind of restructuring that `protected` forbids, since once you have a protection token, you can pass it to another method or type to delegate your access. You can also have several different kinds of protection tokens and do various other clever things.

This is a clever mechanism. Unfortunately, this mechanism won’t work directly with properties, and properties are my main interest. I’d have to expose every property as a method or two to accept the protection token. In my particular case at the moment, the protection added probably doesn’t outweigh the additional code complexity it would add in the code I’m working on.

···

On Mar 17, 2017, at 2:19 PM, Brent Royal-Gordon <brent@architechies.com> wrote:

On Mar 17, 2017, at 11:54 AM, James Dempsey via swift-users <swift-users@swift.org> wrote:


James Dempsey
dempsey@mac.com