Abstract class in Swift?

Few reasons against offloading, for the sake of completeness:

  1. If abstract part and concrete part need to call each other, this requires optionality in one of the parts - otherwise you cannot pass self to another part.

  2. Also, one of the reference should be made weak/unowned.

  3. There is an extra allocation, which can matter for performance-critical code.

I’m not saying any of those are decisive reasons against composition, just some factors that may affect decision making.

1 Like

OK. I've come up with an example of how to implement an "abstract" class, or at least a class with abstract methods that don't require a stub with fatalError in it.

This example also implements a template method, which is a typical usage of an abstract class.

protocol ExecutableCommand
{
  // required method
  func doExecute() -> Bool
  
  // optional method
  var doExecuteSuccess: (() -> ())? { get }
  
  // optional method
  var doExecuteFailure: (() -> ())? { get }
}

extension ExecutableCommand
{
  var doExecuteSuccess: (() -> ())?
  {
    return nil
  }
  
  var doExecuteFailure: (() -> ())?
  {
    return nil
  }
  
  // template method
  func execute()
  {
    if doExecute()
    {
      doExecuteSuccess?()
    }
    else
    {
      doExecuteFailure?()
    }
  }
}

In order to avoid having to code fatalError into each "abstract" method, I use a var that holds an optional closure.

These closures can then be overridden in an implementing type, that doesn't have to be a class :

struct TestCommand : ExecutableCommand
{
  func doExecute() -> Bool
  {
    return Bool.random()
  }
  
  var doExecuteSuccess: (() -> ())? =
  {
    print("Success")
  }

  var doExecuteFailure: (() -> ())? =
  {
    print("Failure")
  }
}

The use of optional vars for abstract methods then becomes very similar to how C++ declares an abstract class :

class ExecutableCommand
{
  virtual bool doExecute() = 0
  
  virtual void doExecuteFailure() = 0
  
  virtual void doExecuteSuccess() = 0
}

With the option of having default empty implementations like this :

void ExecutableCommand::doExecuteFailure() { }

void ExecutableCommand::doExecuteSuccess() { }

What is more, because the Swift "abstract" class is a protocol, it cannot be instantiated.

The only thing missing is the ability to have "shared" storage.

The problem with using closures like this is that you lose parameter labels at the call site.

Indeed. I suppose you could always declare a typealias to the closure type with parameter names, then document it. Not perfect but somebody somewhere got rid of parameter labels in closures already :unamused:

I've added this documentation to the var declaration :

  /// Closure to execute on success
  ///
  /// First parameter is a String for the name
  var doExecuteSuccess: ((_ name: String) -> ())? { get }

This gets picked up at the call site if you Alt-click

Capture d’écran 2020-01-06 à 13.33.22

Correct me if I'm wrong, but isn't the point of abstract methods that they aren't optional and the instantiable class must have an implementation?

3 Likes

Normally yes but Swift allows us to have optional methods by using vars to closures, so I thought I would take advantage. Note the doExecute()method is required.

If we remove the optionality (which is offtopic, because it has nothing to do with abstract classes) then we can refactor your code into something much nicer, without losing parameter labels

protocol ExecutableCommand
{
    // required method
    func doExecute() -> Bool
    // also required
    func doExecuteSuccess()
    // also required, but we have the option of having default empty implementations, just like your C++ example
    func doExecuteFailure()
}

extension ExecutableCommand
{
    // template method
    func execute()
    {
        if doExecute()
        {
            doExecuteSuccess()
        }
        else
        {
            doExecuteFailure()
        }
    }

    // default implementation
    func doExecuteFailure() { }
}


struct TestCommand : ExecutableCommand
{
    func doExecute() -> Bool
    {
        return Bool.random()
    }

    func doExecuteSuccess()
    {
        print("Success")
    }

    func doExecuteFailure()
    {
        print("Failure")
    }
}

"Optionality" in protocols can be achieved by providing a default implementation in an extension. As long as the method is also declared in the protocol itself, the conforming type can declare its own specific implementation. But if the conforming type leaves it out, then the default provided in the extension is used instead.

protocol SomeProtocol {
    func requiredMethod()

    func optionalMethod()
}

extension SomeProtocol {
    func optionalMethod() {
        // "Default" implementation
    }
}

class Foo: SomeProtocol {
    // Required or this won't compile
    func requiredMethod() {}

    // Left out `optionalMethod()`, so the extension default will be used.
}

let f = Foo()
f.requiredMethod()  // Calls Foo's implementation
f.optionalMethod()  // Calls the extension implementation

While noting that of course, this is always subjective experience, and nobody knows all the use cases, I have personally come to avoid the template method pattern in languages that support higher order functions. Using higher order functions is IMHO simpler and more concise, and the template method pattern suffers from the critical flaw that you may think you're overriding a helper method in your concrete class but you actually misspelled the function name (e.g. doExceuteSuccess for your example) and the code doesn't do what it's supposed to. Another reason why I dislike the template method pattern is that it makes it much easier to create a huge mess so that you can't really understand what's happening any more (although that can admittedly be solved through discipline). We have a huge part of our code base based on the template method pattern and I've seen exactly these problems come up again and again, with more and more hooks added to the template method until it's harder and harder to understand what's happening exactly.

For your example, I'd rather go with something like:

struct ExecutableCommand {
    // this is only necessary if you want to disallow people bypassing the smart constructor below
    fileprivate init(run: @escaping () -> ()) { self.run = run }

    let run: () -> ()
}

func makeCommand(
    command: @escaping () -> Bool,
    onSuccess: @escaping () -> () = {},
    onFailure: @escaping () -> () = {}
) -> ExecutableCommand {
    ExecutableCommand { command() ? onSuccess() : onFailure() }
}

you can even be more concise with something like

typealias ExecutableCommand = () -> ()

if you want to avoid the wrapper struct (although the struct gives you the benefit that, since you made the initialiser fileprivate, you can control exactly what kinds of commands can be created).

I agree with @ExFalsoQuodlibet here. Don't get me wrong, GoF is a great book and valuable to read for the insight, but it is true that some of the problems it tries to solve have different solutions in other languages that support things such as higher order functions. Another thing I agree with is that people IMHO overuse protocols (or similar construct in other languages) when concrete types are simpler and more adequate (of course, there are still lots of valid use cases for protocols).

1 Like

Very interesting. As you can tell, I'm fairly much a founder member of the GoF patterns appreciation society, reading the book fairly soon after it was published :sunglasses:

So, here's my version of a type with a template method, but using closures passed to the initialiser :

class ExecutableCommand
{
  private let doExecuteClosure: () -> Bool
  
  private let doExecuteSuccessClosure: (() -> ())
  
  private let doExecuteFailureClosure: (() -> ())
  
  convenience init()
  {
    self.init(doExecuteClosure: { false }, doExecuteSuccessClosure: { }, doExecuteFailureClosure: { })
  }
  
  init(doExecuteClosure: @escaping () -> Bool,
       doExecuteSuccessClosure: @escaping () -> (),
       doExecuteFailureClosure: @escaping () -> ())
  {
    self.doExecuteClosure = doExecuteClosure
    
    self.doExecuteSuccessClosure = doExecuteSuccessClosure
    
    self.doExecuteFailureClosure = doExecuteFailureClosure
  }
  
  func execute()
  {
    if doExecuteClosure()
    {
      doExecuteSuccessClosure()
    }
    else
    {
      doExecuteFailureClosure()
    }
  }
}
class TestCommand : ExecutableCommand
{
  init()
  {
    super.init(doExecuteClosure: { Bool.random() },
               doExecuteSuccessClosure: { print("Success") },
               doExecuteFailureClosure: { print("Failure") })
  }
}

Yes, I'm using a class, not a struct, but I've got to keep some semblance of OO somewhere :kissing_heart:

I know I can pass an empty closure if I want to do nothing but there is still a part of me that would like to know if it is possible to use an optional @escaping closure as a method parameter - just in case I really can justify it :roll_eyes:

Just for completeness, here's a version with a struct instead of a class :

struct TestCommand
{
  private let executableCommand = ExecutableCommand(doExecuteClosure: { Bool.random() },
                                                    doExecuteSuccessClosure: { print("Success") },
                                                    doExecuteFailureClosure: { print("Failure") })
  
  func execute()
  {
    executableCommand.execute()
  }
}

While that design solves some problems well, relying on init to fulfill requirements isn't compatible with InterfaceBuilder-connected classes which would likely be my primary use case :disappointed:. Some Dependency Injection library would probably help, but it's still a workaround requiring more code and causing some issues that don't exist with a basic implementation of abstract classes.

1 Like

This is a practical example of what I was talking about when I wrote "creating instances of concrete generic types instead of defining class/protocol hierarchies". To me, Swift really encourages defining stuff in terms of instances of generic types instead of new types that inherit from something: see for example (as an "encouragement") how it's easy to refer to an instance, where it's expected, through a static property or function with the "dot" syntax.

Can you provide an example of a problem you have with IB classes and how an abstract class would help solving it?

Basic example for an MVVM flow. TBC, IMO it's not worth debating the validity of an abstract Type. It's a design that exists in enough languages that IMO Swift should support the general idea of the design. The question is how it should be done.

class ViewModelVC<ViewModel: Equatable>: UIViewController {

    var viewModel: ViewModel {
        didSet {
             guard viewModel != oldValue else { return }
             update(with: viewModel)
        }
    }

    abstract func update(with viewModel: ViewModel)
}

I mean... Swift already supports the general idea of the design.
What's the general idea of abstract classes? Let's ask https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html

An abstract class protocol is a class type that is declared abstract protocol —it may or may not include abstract methods protocol requirements. Abstract classes protocols cannot be instantiated, but they can be subclassed.

An abstract method protocol requirement is a method that is declared without an implementation (without braces, and optionally followed by a semicolon), like this:

abstract void moveTo(double deltaX, double deltaY);
func moveTo(deltaX: Double, deltaY: Double)

If a class protocol includes abstract methods protocol requirements, then the class itself must be declared abstract a protocol , as in:

public abstract class GraphicObject {
// declare fields
// declare nonabstract methods
abstract void draw();
}
protocol GraphicObject {
// declare properties
// declare methods with default implementations
func draw()
}

When an abstract class protocol is subclassed, the subclass usually provides implementations for all of the abstract methods protocol requirements in its parent class. However, if it does not, then the subclass must also be declared abstract a protocol.

:face_with_raised_eyebrow: That response is, at best, pedantic and at worst, trolling. I would DM a response but apparently you've disabled that. There are numerous examples in this thread of use cases where a protocol does not fulfill the same use cases without major concessions.

Replacing abstract class with protocol doesn't work because a protocol is not a concrete Type. For simple examples that may not matter, but since they have no storage you must implement all stored properties in each Types. And good luck convincing your users to try calling the protocol's implementation of a method.

4 Likes

One of the places where this falls down is that if you have a hierarchy where you have something that conforms to the protocol, and then is subclassed, there is no way to call the "super" implementation or the default implementation. Swift's protocols have no answers for this specific pattern, but I have definitely run into cases in the past where it would have been useful. The current state of protocols is probably halfway between an interface and an abstract class, but there are a few gaps that are common pain points.

6 Likes

This isn't actually what happens, except perhaps accidentally in the simplest cases.

The problem is that the protocol default method doesn't participate in inheritance. If a class overrides the method, then the type of the variable used to call the method (specifically, whether it's an existential of the protocol, or a class type) determines which method is actually called. If you have an "optional" method like this, and a subclass of the base class that redefines the method, and a sub-subclass that overrides the redefinition, then it becomes a nightmare to keep track of what's going to end up being called.

This is arguably a big bug in Swift as it exists today, but it's unlikely it's ever going to be fixed in isolation, because it's a deep pool of hurt. (For example, the syntax for specifying protocol conformances/default implementations probably needs to be be revamped first.)

2 Likes

There are several huge problems with this approach:

  1. There's no way for ExecutableCommand to provide storage and/or properties. If I know that every command will need some specific property values (for example, maybe they all need an array to hold argument values), then every command must re-declare that. This is excessively verbose and is solved by subclassing (the superclass provides defaults).

  2. The other huge problem is that this makes composition very problematic. I can't (for example) create a "TestAndRunCommand" without duplicating all their functionality into that conformer. This is because there's no way to customize the behavior of the execute() method, since it's provided as a non-overridable implementation in a protocol extension. In the example as shown it would probably work, but does not work in a general case.

  3. The problem with making the execute() method part of the protocol requirement but have a default implementation is that I have no way to supplement its logic. I must accept the version as-is in the extension, or I must entirely replace it. There's no way to call "super", because there is no "super".

All of these add up towards the idea that "we need abstract classes".

6 Likes

My experience with MVVM is based on a completely different approach, so I'm not familiar with the problems related to this design, but there's a few strange things here that I don't understand.

What's the "abstraction" here? If I understand this correctly, a client of ViewModelVC will be able to get the ViewModel (which doesn't make sense), and will be able to directly call update(with:) which will break an invariant, because the viewModel property will not be updated: to solve this, we'd need protected scope. But still, I don't see a clear abstraction here, because "abstraction" means to remove details and only expose what matters for a particular functionality, and here what matters is that the type must have a ViewModel: Equatable associated type and an update(with:) method, something that can be modeled with a protocol:

protocol ViewModelUpdatable {
  associatedtype ViewModel: Equatable
  func update(with: ViewModel)
}

This of course would be needed only if it's possible and useful to define algorithms based on the existence of a ViewModelUpdatable. Then, in each conforming class I'd define a private var viewModel: ViewModel for storage. To restore the guard logic some boilerplate would be needed in the update(with:) implementation. Alternatively, to encapsulate the "update if needed, otherwise return" logic, I'd wrap the view model into a type like this:

final class Updating<A: Equatable> {
  private(set) var wrapped: A

  init(_ initial: A) {
    wrapped = initial
  }

  /// Returns `true` if actually updated.
  func updateIfNeeded(with new: A) -> Bool {
    guard new != wrapped else { return  false }
    wrapped = new
    return true
  }
}

Finally, a conforming class that composes the abstraction with the updating behavior would be something like the following (assuming ViewModel == Int):

final class SomeVC: ViewModelUpdatable {
  private let viewModel = Updating<Int>(0)

  func update(with new: Int) {
    guard viewModel.updateIfNeeded(with: new) else { return }

    /// the rest, using `viewModel.wrapped`
  }
}

You can still declare properties in protocols, and use those: a conforming type would simply need to declare that property, which is frankly not that verbose, or (in case of a conforming class) subclass from a base class created for the sole purpose of holding that storage.

Not sure I'm understanding this: if you have a TestCommand and a RunCommand, you can create a TestAndRunCommand that's constructed from the other two, and calls their methods in its implementations.

I'm not adding anything to this, having already expressed my opinion on super in a previous post.

Here's a small clarification, though. The reason why the mantra "prefer composition over inheritance" exists is because the very concept of implementation inheritance is antithetical to composition: implementation inheritance is not composition. "Composition" means "creating different things that work by themselves, but can be combined into a single thing that does both", which is not what super gives you. In my opinion, if "composition" is what we're shooting for here, patterns based on abstract class are not the way.

3 Likes

I don't mean this to come across as snarky, but the reason the mantra starts with prefer is because composition isn't always the best way to express something. Regardless of whether it could be done.

1 Like