I have a rather large project structured in this format:
class One : FirstThree {
fileprivate var integers: [Int] {
return [1, 2, 3, 101, 102]
}
override func allIntegers() -> [Int] {
return integers
}
func doStuffForOne() {
//does stuff unrelated to the other classes
}
}
class Two : FirstThree {
fileprivate var integers: [Int] {
return [1, 2, 3, 201]
}
override func allIntegers() -> [Int] {
return integers
}
func doStuffForTwo() {
//does stuff unrelated to the other classes
}
}
class Three : Numbers {
fileprivate var integers: [Int] {
return [301, 302, 303]
}
override func allIntegers() -> [Int] {
return integers
}
func doStuffForThree() {
//does stuff unrelated to the other classes
}
}
class FirstThree : Numbers {
fileprivate var integers: [Int] {
return [1, 2, 3]
}
override func allIntegers() -> [Int] {
return integers
}
func doStuffForFirstThree() {
//does stuff unrelated to the other classes
}
}
class Numbers {
func allIntegers() -> [Int] {
fatalError("subclass this")
}
func printMe() {
allIntegers().forEach({ print($0) })
}
}
Numbers
has many methods like printMe()
which I want any instance of all my subclasses to be able to call.
Numbers
also has an allIntegers()
function which I want any instance of these subclasses to be able to call. Which would probably be better as a variable right? But I can't override variables in a subclass. So instead I use the same private variable integers
in each subclass, which is read and returned by allIntegers()
.
Also notice an instance of Numbers
itself should never call allIntegers()
, it should only be called on a subclass.
Last, notice some of the subclasses contain the same objects 1, 2, 3
and then each has some custom integers. But not all the subclasses. If I later decide that all those subclasses need a 4
integer, I have to manually go through each class and punch in a 4
into the array, which is obviously error prone.
I've read up on protocol oriented programming and feel like a solution might lie in there, or I'd appreciate any other suggestions and creative approaches to architecting a better project.
Thanks!
EDIT
All subclasses are different because they also have their own functions to perform. I've updated the code to reflect this.
Imagine, a given class like One
is initialized many time throughout the code base, and is always initialized with the exact same integers
. Putting:
let one = One(integers: [1, 2, 3, 101, 102])
All throughout the code base would be error prone.
Hopefully this resolves some of the concerns of the contrived example I've put forward.
SOLUTION
Thank you everyone for your help. Here is the solution I came up with (please assume that all classes have their own unique methods).
class One : FirstThree {
override init() {
super.init()
self.integers = super.integers + [101, 102]
}
}
class Two : FirstThree {
override init() {
super.init()
self.integers = super.integers + [201]
}
}
class Three : Numbers {
var integers = [301, 302, 303]
}
class FirstThree : Numbers {
let integers = [1, 2, 3]
}
protocol Numbers {
var integers: [Int] { get }
func printMe()
}
extension Numbers {
func printMe() {
integers.forEach({ print($0) })
}
}
Define a protocol with your common operations, including the objects
accessor:
protocol Numbers {
/// My objects. By default, `Numbers.commonObjects`. Subclasses can override to include more objects.
var objects: [Int] { get }
func printMeThatConformersCanOverride()
}
Provide default implementations in an extension:
extension Numbers {
/// The default implementation of `objects`, which just returns `Numbers_defaultObjects`.
var objects: [Int] { return Numbers_defaultObjects }
/// Since this is declared in the protocol, conformers can override it.
func printMeThatConformersCanOverride() {
Swift.print("Numbers " + objects.map({ "\($0)" }).joined(separator: " "))
}
}
/// It would be nice to make this a member of `Numbers`, but Swift won't let us.
private let Numbers_defaultObjects = [1, 2, 3]
Because those definitions implement things declared in the protocol, conforming types can override them. You can also define things in an extension that conforming types cannot override:
extension Numbers {
/// Since this is not declared in the protocol, conformers cannot override it. If you have a value of type `Numbers` and you call this method on it, you get this version.
func printMeThatConformersCannotOverride() {
Swift.print("Numbers " + objects.map({ "\($0)" }).joined(separator: " "))
}
}
We can then implement a class that conforms to the protocol. We can use a let
to override objects
:
class One: Numbers {
/// You can use a `let` to override `objects`.
let objects: [Int] = Numbers_defaultObjects + [101, 102]
func doStuffForOne() {
Swift.print("I'm doing One-specific stuff with \(objects)")
}
func printMeThatConformersCanOverride() {
Swift.print("One wins! You don't care what type I am.")
}
func printMeThatConformersCannotOverride() {
Swift.print("One wins! You think I'm type One, not type Numbers.")
}
}
We can use a stored property to override objects
:
class Two: Numbers {
/// You can use a stored property to override `objects`.
var objects: [Int] = Numbers_defaultObjects + [201]
func doStuffForTwo() {
Swift.print("I'm doing Two-specific stuff with \(objects)")
}
}
We can use a computed property to override objects
:
class Three: Numbers {
/// You can use a computed property to override `objects`.
var objects: [Int] { return [301, 302, Int(arc4random())] }
func doStuffForThree() {
Swift.print("I'm doing Three-specific stuff with \(objects)")
}
}
We don't even have to use a class type. We can use a struct type instead:
struct Four: Numbers {
func doStuffForFour() {
Swift.print("I'm doing Four-specific stuff with \(objects)")
}
}
I said above that you can define things in an extension and if they weren't declared in the protocol, then conforming types can't override them. This can be a bit confusing in practice. What happens if you try, like One
does, to override a method that was defined in an extension but isn't part of the protocol?
let one = One()
one.printMeThatConformersCanOverride()
// output: One wins! You don't care what type I am.
(one as Numbers).printMeThatConformersCanOverride()
// output: One wins! You don't care what type I am.
one.printMeThatConformersCannotOverride()
// output: One wins! You think I'm type One, not type Numbers.
(one as Numbers).printMeThatConformersCannotOverride()
// output: Numbers 1 2 3 101 102
For methods declared in the protocol, you run the version belonging to the run-time type of the value. For methods not declared in the protocol, you run the version belonging to the compile-time type of the value.