Alternatives to an array of T swift

Viewed 714

In my Quiz app I initialize quizzes, and the providing class does not know the format of the questions before being provided them (although they are constrained by a QuestionProtocol):

public protocol QuestionProtocol {
    init?(fields: [String] )
    var description: String {get}
    var question: String {get}
    var solution: String {get}
    var explainAnswer: String {get}
    var answered: Int {get set}
    var qa: String {get}
    var qb: String {get}
    var qc: String {get}
    var qd: String {get}
}

And I can initialize the quizzes and return them easily enough through a method with the signature

public func initializeQuizzes<T: QuestionProtocol>(with type: T.Type, withCompletionHandler completion: ((Result<[Quiz<T>], Error>) -> Void)?) 

However to provide these quizzes is expensive (an API call or a SQL retrieval) so I want to store these quizzes and retrieve them separately from a suitable function with signature

public func getNextQFromSet<T: QuestionProtocol>(with type: T.Type) -> (question: T, answers: [String])?

The problem I have is storing these questions which are of type T.

They are linked to a Quiz object:

public class Quiz<T> {
    private let questions : [T]
    private let name : String

    init(name: String, questions: [T]) {
        self.name = name
        self.questions = questions
    }

    public func getQuestions() -> [T] {
        return questions
    }

    func getName() -> String {
        return name
    }
}

So I'm able to store them as quizzes that conform to the QuestionProtocol

private var quizzes = [Quiz<QuestionProtocol>]()

But then I lose the extra information I want to store in the question.

I can store Any, but I believe that is bad practice

private var anyquizzes = [Quiz<Any>]()

Ideally I would like to store T i.e.

Quiz<T>

but that seems to be impossible in Swift.

Because these classes are in a pod they have no way of knowing about the internal workings of a Question, and are provided these at runtime hence the use of generics and the difficulties in storing these questions.

I can't think of a way to improve the design of the App (more specifically the Pod) - I want to initialize the quizzes once and once only and then run functions like getNextQFromSet() to retrieve a relevant question - which obviously depends on me knowing the type of the question (which I do not know before runtime).

For clarity here is a link to the Pod: https://github.com/stevencurtis/QuizManager

How can I store an array containing these questions without knowing the type?

4 Answers

How can I store an array containing these questions without knowing the type?

To my knowledge you can't. As rraphael pointed out in his comment generics aren't resolved at runtime. Furthermore Arrays in swift are designed to hold a single type:

Specifically, you use the Array type to hold elements of a single type, the array’s Element type.

So whatever you do you'll have either an array of Any or maybe QuestionProtocol but nothing more dynamic than that : the type will be resolved at compilation time

You may be able to redesign your QuestionProtocol to suit your needs but without any information on the different types of question it's a bit difficult to help you more since it is an architecture matter.

To be short, I think it makes sense to remove QuestionProtocol and replace it with plain data structure struct Question.

Before I explain my point of view, I want to note that even though I looked at the pod, I still do not know all the requirements, so I might be wrong.

Let's try to have a look at the problem from design perspective instead of programming language perspective.

What is the reason of having QuestionProtocol? Could it be replaced with, let's say, object instead? Why do those properties should be polymorphic? Of course implementation details should be hidden, but hiding data is not about protocols or additional function layers, is about abstractions.

Let's convert QuestionProtocol to Question object for now and think about an abstraction. If there is a real abstraction, then there should an object that hides the data (details) and expose functions that manipulate that data. But there is no functions in Question object and it means that there is no real abstraction behind.

Finally, It means that Question entity most likely is a plain data structure with public properties and could be defined as struct Question.

Having this Question struct now, you can define quizzes as Quiz<Question> and use it to save and retrieve the data.

In addition, I think it worth to point out two things which could simplify and potentially improve design and implementation:

  1. Why does SQLiteManager knows something about concrete question (depends on QuestionProtocol)? I think it makes sense to introduce some generic DBObject or at least plain dictionary [String: Any] which SQLiteManager would know how process and then insert. Then Repository could transform Question data structure into DBObject on some level of composition and pass it to SQLiteManager.

  2. When using generics, in most cases there is no need to define additional type: T.Type parameter. Once generic is defined you can use it as [T], T.init, etc. If you still need a metatype (T.Type) you can get by T.self.

Hope this helps!

Update:

There is great example of Quiz app created with TDD and modular design: Quiz App. There is also a video series explaining design and creation process step by step.

You can use enum with associated values for describing types. For example:

struct QuestionTypeA { }
struct QuestionTypeB { }
struct QuestionTypeC { }

enum Question {
    case typeA(question: QuestionTypeA)
    case typeB(question: QuestionTypeB)
    case typeC(question: QuestionTypeC)
}

And then:

public class Quiz {
    private let questions : Question
    private let name : String
    ...

And store an array of Quiz without generic

private var anyquizzes = [Quiz]()

You wouldn't be able to store a Quiz<T> and a Quiz<U> in the same array using either of those types. They're just not the same type.

If you have an Array<QuizProtocol>, you can just match against your known types in a switch-case statement:

var quizzes: [QuizProtocol] = ...

for quiz in quizzes {
    switch quiz {
       case let someQuiz as SomeQuiz:
           ...
       case let someOtherQuiz as SomeOtherQuiz:
           ...
       default: 
           ... // couldn't cast to any known type; do some fallback logic
       ....
    }
}

where SomeQuiz and SomeOtherQuiz conform to QuizProtocol (though strictly speaking, you could match against any type).

Related