No snarkiness detected, I definitely agree with this.
Edit: Did a quick update to show that if the type of the variable reference is of the superclass, that the subclass's optional override is still invoked.
Hmm. I do agree that these method resolution rules have sharp edges that I've cut myself on as well! Though in this case, the subclass overriding relationship seems to work as expected and isn't thrown off by using a protocol to enforce the shape of the super and subtypes. At least as far as I'm understanding your concern.
Here's what I'm referring to:
protocol SomeProtocol {
func requiredMethod()
func optionalMethod()
}
extension SomeProtocol {
func optionalMethod() {
print("protocol extension \(#function)")
}
}
class Foo: SomeProtocol {
func requiredMethod() {
print("Foo \(#function)")
}
func optionalMethod() {
print("Foo \(#function)")
}
}
class Bar: Foo {
override func requiredMethod() {
print("Bar \(#function)")
}
override func optionalMethod() {
print("Bar \(#function)")
}
}
let f = Foo()
f.requiredMethod() // prints "Foo requiredMethod()"
f.optionalMethod() // prints "Foo optionalMethod()"
let b = Bar()
b.requiredMethod() // prints "Bar requiredMethod()"
b.optionalMethod() // prints "Bar optionalMethod()"
let otherF: Foo = b
otherF.requiredMethod() // print "Bar requiredMethod()"
otherF.optionalMethod() // print "Bar optionalMethod()"
It's probably more of a hybrid with dependency injection + one-way data-flow. I mainly use that kind of pattern form child-VCs to confine the interaction between parent-child to updating a single property. It's not the MS flavor of MVVM, but uses some similar ideas. Not sure what else to call it . Anyway that's a digression from the topic
I do generally agree that composition is preferable over class-hierarchy. I've switched to Value semantics where possible and favor protocols over class-based inheritance. Regardless of my preferences, my day-to-day work is in UIKit+InterfaceBuilder as is the case with many Swift users and likely will be for (at least) the next few years until SwiftUI matures enough to be a full replacement.
It's not that I don't want to use protocols/composition, it's that in many cases, **it doesn't work *. We're already limited when interacting with protocols and Obj-C, and they're almost useless when dealing with User-Interaction through UIKit (and I'm guessing AppKit too).
Reasons why protocols aren't good enough for my usage
- You can only have
@objc
functions in Classes and concrete Extensions. That means any@IBAction
,@IBOutlet
@IBDesignable
, orSelector
will have to be implemented by each adhering class. - Protocols can't hook into the life cycle, so you have to rely on implementers to manually call any setup during
viewDidLoad
/awakeFromNib
and any cleanup afterviewDidDisappear
. That's a bad idea for your own code, much less anything others may use. - You can't use
didSet
orwillSet
in an extension. These are very useful for simplifying data flow/management in MVC.
I'm all for pointing out the flaws in OOP, and can talk about the failures of MVC until the cows come home. The reality is it's the most popular use case for Swift. the core team has deferred, not rejected, the idea with ~all of the features potentially replacing it still not in the language >3 years later. It's an abstraction that exists in multiple popular languages with many people finding it useful. The pragmatic thing would be to add it to language
I understand, and probably the reason why I don't feel such need is that I decided a long time ago, in my iOS work, to deal with UIKit and IB as little as possible, put those classes at the extreme boundary of my architecture, and apply the humble object pattern to the extreme (that is, removing any logic and leaving only the "glue code").
Also, while it could be pragmatic for some users, it could actually be impractical from the point of view of the compiler, but I don't really know the internals. Still, I feel that Swift covers these use cases in a decent enough way with the aforementioned fake abstract methods that use fatalError()
, so a "solution", while suboptimal, already exists, while other parts of the language have typed holes that should be filled as soon as possible, so I'd still direct the effort there.
Iām loving the extremes people are going to avoid adding, what would basically be, an abstract
keyword to the language..
I'm glad you mentioned that because I started feeling that swift lacks abstract classes exactly when I started moving away from IB.
I think this happened because it's way easier to create reusable code when it doesn't depend on IB because relying more on view code makes we write more code and we end up with repetitive patterns and boilerplate code that we wish we could avoid. Also IB sometimes is dangerous exactly because its runtime depencies like IBOutlets can trigger crashes only at runtime like when they're disconnected from IB.
I don't know if you are following this thread since the beginning but I've already posted an example, which is a very simple but yet common scenario for the vast majority of swift developers. E.g. you have a list to display, then you write a ViewController, add a tableView as subview, VC also has an array of elements it will display, for simplicity the VC is also the data source of its tableView and uses the array to calculate the required dataSource internals. I surely can employ my example today to avoid boilerplate code, but I have to put a lot of fatalError stubs to act as fake abstract methods, which compiler cannot ensure are implemented at compile time and, despite being away from view code, we come back to the same problems that we faced with IB, which is lack of compile time safety.
I don't think this is a fair representation of the current discussion.
vs (roughly)
abstract class ExecutableCommand
{
abstract var doExecute: Bool
abstract func doExecuteSuccess()
abstract func doExecuteFailure()
func execute() {
if doExecute {
doExecuteSuccess()
} else {
doExecuteFailure()
}
}
class TestCommand: ExecutableCommand {
override var doExecute: Bool { Bool.random() }
override func doExecuteSuccess() {
print("Success")
}
override func doExecuteFailure() {
print("Failure")
}
}
I seriously think the former is an extreme workaround that is nowhere near as readable.
I don't know. This kind of logic is how you end up with something like C++, just adding everything under the sun. Big tent languages have their place, I guess, but it's also useful if a language is opinionated and avoids bloat. There are plenty of languages that don't have "an abstract type" and they seem to be doing fine too.
I'm personally biased against inheritance (and Java-like OOP) most of the time, but I could understand if Swift chose to support that programming style more extensively (especially since it might be more useful for people writing UIs, which I don't). That said, I think it needs to be a conscious choice to encourage a particular programming style, instead of just adding features for completeness's sake.
I totally agree. The lack of OO concepts like abstract classes and protected visibility con force you to do some horrendous hacks.
And, I must say, despite the protestations of those who feel everything can be constructed with just protocols and structs. A bit like when I learnt, many years ago, that "every" possible program could be described using JSP (Jackson Structured Programming)
One of Swift's goals is to improve compile-time safety. For example, Optional. Having most of the "standard" OOP features of classes, while not having abstract classes, leads to implementations that sacrifice compile-time safety for runtime enforcement. I think that if Swift is going to offer a feature, it should be as complete as possible. Lack of abstract classes leaves a large hole amongst users of inheritance.
I'm personally biased against inheritance (and Java-like OOP) most of the time, but I could understand if Swift chose to support that programming style more extensively (especially since it might be more useful for people writing UIs, which I don't). That said, I think it needs to be a conscious choice to encourage a particular programming style, instead of just adding features for completeness's sake.
I don't know the numbers exactly but I'm quite sure the biggest use case for Swift is writing iOS apps. it needs to support OOP because UIKit and Foundation are heavily based on that. So your sentence of "if Swift chose to support that programming style more extensively" seems odd to me because that is exactly the style it should be supporting.
Yes things like SwiftUI are the future but people are going to be using UIKit and Foundation for years ahead (while SwiftUI matures to the point it is stable and usable, and while apps have to support older iOS versions).
I can understand not implementing multiple inheritance and such that we end up with C++. But this is just a modifier that is a compliment to an existing modifier (final), and is similar to optionals (vs Objective-C nils) in that it brings runtime behaviour to compile time safety.
I agree. But I still think your characterization of the discussion at hand was unfair.
I apologise if it came across as rude. (I probably shouldn't have put the words "I'm loving"). But I still do think that the example was extreme and the conversation was around that example.
My interpretation was that this was just joking - and imho discussion is often way to eager:
Abstract classes are not something every language needs, but on the other hand, "prefer composition over inheritance" is little more than a preference (and no natural law).
In many cases, the power of certain practices (like FP) comes not with what you can do with it, but rather with what you can't do, so adding new capabilities can really cause a mess.
I don't think that's the case for abstract classes, though: This would not be a fundamental change, just a new feature.
For me, the first question to answer is wether this feature is worth the price of increased complexity (it's not much, but still, it's something to learn). This is highly subjective, and depending on which benefit of abstract classes are considered important, there might or might not be better alternatives. However, I'm convinced that it's a bad idea trying to use protocols as a full replacement.
To my mind, one of the biggest problems with Xcode as an IDE, is that it promotes, as do C#, Delphi and a few other IDEs, the concept of "let's design the UI" before you've even had a chance to work out the data model.
Most of the problems that people encounter with Xcode, Objective-C and Swift come from the fact that they are presented with a very convenient single code module that represents a Controller class. It's meant to be Model View Controller, not Massive View Controller.
So people start to throw code into overridden methods from the delegates, some never ever considering to create a separate code unit (or more) that models the data; preferring instead to just add a var with an array or an object to the controller.
Why am I such a fan of good OO design? Because, in the 25+ years that I have been using and teaching it, it leads to less bugs and more readable code if you do need to bug fix.
The fundamentals of OO are still valid today (apart from multiple inheritance, of course). Abstract classes and protected scope are of vital importance in data encapsulation, only revealing what the user of a type needs to see, thus ensuring less mistakes are made by calling a method on the wrong level of a hierarchy.
Certainly composition has its place but inheritance is equally important in good design. The difference is that some developers never get to the stage of knowing when to use which.
Example :
// inheritance
class Person
{
var name: String
var age: Int
}
class Employee : Person
{
}
class Manager : Employee
{
}
This seems all well and good but, what if the Employee becomes a Manager but then gets moved to another role?
Whereas :
// composition
class Person
{
var name: String
var age: Int
}
enum Role
{
case apprentice
case cleaner
case manager
}
class Employee : Person
{
var role: Role
}
⦠allows the Employee to change roles without changing its type.
We could also argue that a Person may not ever become an Employee, therefore, should we move the Role var to the Person class?
Then we need to consider the fact that most people fulfil more than one role during their lifetime, sometimes at the same time. Should we define an array var called Roles?
It all boils down to how well you analyse the data model before rushing in to code what happens when a button is tapped/clicked.
Maybe we've come to believe that iOS apps are small and don't really need a complex data model - it's just not worth the time and effort when we can just "shove" some code in the View Controller.
Until⦠the model needs changing or adding to to accommodate a new concept. Then it is a mad scramble to find all the interconnections that have been hard-coded and that now need re-wiring to a changed UI. Oh, and by next Friday please, because we've already done the press release!
Why use well designed OO? Let me give you the example of a fairly massive Delphi business management project that had "evolved" over the years. I was brought in to rationalise, what can only be described as "object-based", but not properly OO code. Whilst unravelling the spaghetti code, we found that Sales Tax was being calculated in around seven different places, in seven different ways - each one added by a developer who couldn't be bothered to use the existing one because it wasn't local enough to the code they was writing.
The Sales Tax code had been "composed" into several places, rather than being part of an inherited structure, leading to the differences.
Having to implement a Protocol in several places can do exactly the same thing :
⦠one of the vars in the protocol is a calculated property, or is it? in one implementation the var has willSet and didSet observers, but not in others⦠and so on.
It's these kind of situations that scream out for abstract classes. I rest my case (for the moment at least)
I don't think that's a valid example. The proper thing to do is extract the calculating code into its own object, when can than be instantiated anywhere sales tax needs to be calculated. The extracted entity could be a simple function, or a full-blown class that does DB lookups to get rate information.
Unless you consider the possibility of an abstract Tax Calculation class that is inherited by Sales Tax Calculation, Income Tax Calculation, etc. Certain parts of any calculation are identical and should never vary from one subtype to another; whereas the parts of the calculation can vary, depending on what is being calculated, how many steps are involved in the calculation, etc.
Of course, but you made it sound like callers of the tax-calculation code was implementing it ad-hoc, and not the tax-calculation code itself doing so. Without more details, it's still hard to say that inheritance is objectively better than composition. Math tends to compose rather well.
I tried to read and understand the entire thread before posting, and the answer @DevAndArtist gave you seemed satisfying to me (save for storage in protocol).
Anyway, to confront your specific scenario, I would do something radically different. Here's some code.
/// Let's get this out of the way: I'd define `Section` as a generic `struct`, no point in using a protocol here.
struct Section<Item> {
var items: [Item]
}
/// First, I'd define a table view data source as a separate class: I'd spend hours discussing why doing this is a good idea, but let's just say that it's good to create small components focused on doing one thing.
final class TableViewDataSource<Item>: NSObject, UITableViewDataSource {
let sections: [Section<Item>]
/// The important trick is to explicitly pass this function (which is the only real "abstract" part) to the data source, so it can use it regardless of the instance that'll implement it.
let getCell: (UITableView, Item, IndexPath) -> UITableViewCell
init(tableView: UITableView, sections: [Section<Item>], getCell: @escaping (UITableView, Item, IndexPath) -> UITableViewCell) {
self.sections = sections
self.getCell = getCell
super.init()
tableView.dataSource = self
}
func numberOfSections(in tableView: UITableView) -> Int {
sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
sections[section].items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
getCell(tableView, sections[indexPath.section].items[indexPath.row], indexPath)
}
}
/// Here's the only real abstraction: this method is the main differentiator and the only thing we care about, the rest can be abstracted away.
protocol TableViewCellFactory {
associatedtype Item
func tableView(_ tableView: UITableView, cellForItem item: Item, at indexPath: IndexPath) -> UITableViewCell
}
/// This is just a utility protocol, related to [this issue](https://forums.swift.org/t/superclass-constraint-is-recursive/19910) ... not really a big deal, and makes for a cleaner desing.
protocol TableViewOwner: class {
associatedtype Item
var tableView: UITableView! { get }
var sections: [Section<Item>]! { get }
var dataSource: TableViewDataSource<Item>! { get set }
}
/// This method composes the features of a table view controller (basically, its properties) with the method privided by a `TableViewCellFactory`.
extension TableViewOwner where Self: TableViewCellFactory {
func setupTableViewDataSource() {
dataSource = .init(
tableView: tableView,
sections: sections,
getCell: { [weak self] tableView, item, indexPath in
self?.tableView(tableView, cellForItem: item, at: indexPath) ?? UITableViewCell()
}
)
}
}
/// This adds a little clarity.
typealias TableViewController = TableViewOwner & TableViewCellFactory
/// Finally, here's a possible concrete table view controller.
class ConcreteTableViewController<Item>: UIViewController, TableViewController {
/// These properties are protocol requiements: statically checked.
var tableView: UITableView!
var sections: [Section<Item>]!
var dataSource: TableViewDataSource<Item>!
/// This method is a protocol requirement: statically checked, and the only real differentiator.
func tableView(_ tableView: UITableView, cellForItem item: Item, at indexPath: IndexPath) -> UITableViewCell {
fatalError("must write an implementation here")
}
/// This is the only bit that's not generalizable right now: it's a small thing to remember, not a big deal.
override func viewDidLoad() {
super.viewDidLoad()
/// Only callable from a `TableViewController`.
setupTableViewDataSource()
}
}
That's a massive understatement of the discussion we're having here. Abstract classes and protected visibility were missing from Objective-C too, so different patterns were employed there: same for Swift, it's a different language, and has its set of strengths that can be leveraged
From what you write one could infer that abstract classes and protected scope are the only way to deal with the problems you're enumerating (like Massive View Controller). I disagree.
For example, my experience is completely different, I realized that OO code is worse than the one produced by many other approaches when it comes to maintainability. I only have about 10 years of experience, though, but that's not relevant: what kind of experience or how many years one has are not useful (nor valid) arguments when it comes to discussing the features of a programming language. The experience can (and should) certainly inform one's opinions, but experience by itself doesn't validate anything.
I completely disagree with this statement, and in modeling "data" I 100% use value types, that is, enums and structs: enums, for example, are the correct type to represent alternative states, and exhaustive switching over an enum is enforced by the compiler, so its usage presents many advantages. I'd say that enums are one of the best, cleanest features of Swift, way ahead of some class-based approaches found in some other languages.
This is purely subjective, and and not valid argument in favor of inheritance.
I 100% agree that a deep analysis is 100% necessary when modeling data. I's also say that things change, and one should always solve the problem at hand, not some imaginary future problem.
Here's what I do when a business domain entity changes (for example, a single role to a list of roles, like you said): I change the code. What usually happens is that the project doesn't compile anymore: that's perfect, the compiler is informing me pretty clearly about the repercussions of that change, and thankfully I'm using a statically typed language, and a strict typing policy; also, after restoring compilation, maybe some tests don't pass: great, thankfully I wrote them.
Also, protocols are perfect to abstract some concretely modeled data, because they allow for "retroactive" modeling: whatever the type I used to model something, Swift allows me to add to it a protocol conformance, including structs and enums.
That's just an example of bad code/architecture, it's not strictly related to the concept of "composition" in itself.
That's just related to the fact that protocols can (and, frequently, should) have semantics attached, and not just bags of syntax: whatever the pattern, without clear semantics chaos arrives fast.
Using a mathematical approach, the way you would compose this is by creating a type that holds the identical parts, and several types that hold the custom parts, in a way that can be combined: functions are usually perfect for this. Then, you would inform the client (and who's reading the code) by defining nominal types that produce specific compositions. I probably did things like these many times, and no abstract classes were needed. Actually, I'd use this as a prime example of "prefer composition over inheritance".
All that being said, I want to clarify this: I don't oppose additional features for the language, like abstract classes and protected scope, even if I probably wouldn't take advantage of them, and if they're not problematic to add to the compiler, I say let's have them. I'm only worried about the added complexity for the compiler, a possible distortion to the language model itself, and the general fact that I'd direct the effort to something I'd consider more important.