Protocol-oriented architecture

I'm trying to learn protocols and protocol extensions and decided to make an app where user can track points in a match. For example in tennis and badminton. Tennis match contains sets, games and points and badminton match contains games and points. All matches contains participants(players).


Point stores a single point in a match.

struct Point {
    let value: Int

    init(_ value: Int) {
        self.value = value
    }
}


Match contains participants for example Players.

protocol Participant {
    var id: String { get }
    var name: String { get }
}
struct Player: Participant {
    let id: String
    let name: String
}


The match controller (or view model) only needs to know about the match items and it should be able add add points to a certain player.

let player1 = Player(id: "0", name: "John")
let player2 = Player(id: "1", name: "Jane")

var match = TennisMatch(participants: [player1, player2)
match.addChild() // Adds set in a match
let currentPointCollection = match.currentPointCollection() // Gets current point collection (set 1, game 1)
currentPointCollection.addPoint(Point(1), toParticipant: player1)


I started by creating two protocols: PointCollection and ItemCollection. PointCollection is a collection for points e.g. a game contains points. ItemCollection is a collection for match's child items e.g. match contains games or sets and set contains games etc.

protocol PointCollection {
    mutating func addPoint(point: Point, toParticipant participant: Participant)
}

protocol ItemCollection {
    typealias ItemType

    var count: Int { get }
    mutating func addChild()
    subscript(i: Int) -> ItemType { get }
    func currentPointCollection() -> PointCollection
}


Match is now basically an ItemCollection.

protocol Match: ItemCollection {}


This is where I'm struggling. ItemCollection can have both ItemCollections and PointCollections as a child because a match can contain both. For example if I define a tennis match. This fails in “Protocol 'ItemCollection' can only be used as a generic constraint because it has Self or associated type requirements”

struct TennisMatch {
    private var sets: [ItemCollection] = []
}
extension TennisMatch: ItemCollection {
    typealias ItemType = ItemCollection

    var count: Int { return sets.count }

    mutating func addChild() {
        sets.append(MatchGame())
    }

    subscript(i: Int) -> ItemCollection {
        return sets[i]
    }
}

struct MatchGame {
    private var participantPoints: [String: [Point]] = [:]
}
extension MatchGame: PointCollection {
    mutating func addPoint(point: Point, toParticipant participant: Participant) {
        var points = participantPoints[participant.id] ?? []
        points.append(point)
        participantPoints[participant.id] = points
    }
}



I know what the error means, but I'm not sure how to fix it. If you have a better idea how to define this kind of architecture feel free to post.

You will have to define another protocol that is common to both ItemCollection and PointCollection that does not have a Self or associated type constraint and then declare the sets array as having elements of this common type.

I'm not sure if I understood correctly. So both ItemCollection and PointCollection should inherit from another protocol that does not have a Self or associated type constraints? So does the same apply to the Match protocol if I want to use it from match controller? It can't have Self or associated type constraints?


var match: Match = TennisMatch(participants: [player1, player2) //Error now because Match inherits from ItemCollection

Well, to be honest I'm a bit confused. So, I apologize. I've been reviewing your code example, and I think I am missing something. You say, "ItemCollection can have both ItemCollections and PointCollections as a child because a match can contain both." By "child" do you mean an item in the array sets? If so, does PointCollection inherit from ItemCollection because I don't see that in the code? The only addChild() method I see implenented is one that adds a PointCollection but sets doesn't accept PointCollection in the code as it is written.

I know this might be confusing. That's why I posted this in this forum because I've been trying different solutions and all seems to fail at some point. Maybe I should have only asked how to implement this kind of feature instead of providing some sample code. 😀


Maybe I should only have one protocol (without Self or associated type constraints) which contains methods for adding points and childs (By child I mean sets in match, games in sets etc.). But then for example a game in set would have addChild method but it souldn't do anything and set would have addPoint method but it shouldn't do anything (or maybe it could forward the point to it's child).

Also, part of the problem for me is I'm not familiar enough with tennis. However, I offer some general comments. I would avoid avoid adding extra methods to a protocol to accomodate features specific to a particular implementation. It wouldn't be a blanket statement but maybe more of a warning that the design may be wrong if you find you need to do that. You shouldn't force types to implement protocols that don't make sense for them. The protocol should represent the common subset of functionality of an implementing type for a partiicular use case.


To help clarify the problem for me, what is the reason for mixing sets and games in the same array? That is what I though the original post was asking, but now I'm not so sure. Now it sounds more like you have an array of sets within a match and an array of games in a set. If that is the case, then is the question how to make TennisMatch and TennisSet share a common protocol (e.g. TennisChallenge) that contains items (sets in TennisMatch and games in TennisSet). And is it true that TennsiSet and TennisGame also share a common (e.g. TennisItem) protocol?

Thanks for helping me. Sets and games are not mixed in the same array. A match contains sets, a set contains games and a game contains points in Tennis. Like I said earlier, I shouldn't have posted the code snippet because it just confuses more.


So a match controller (view controller or view model etc.) should only know about a Match which define methods for adding points to a match and Match also has children which can be sets or games (and set has games as children). I was thinking if match, sets and games all shared the same protocol, I could easily forward a point to a correct child when a point is added to the match. For example in tennis, a single point should be added into a game in set. Because I want to support different types of matches from the same user interface, single match's structure can differ.

Sorry, my previous answer had errors in it, so I removed it. I think the following code may be more on the lines of what you were asking. Unfortunately, comments don't seem to make it through in this forum editor.


struct Player : Hashable, CustomStringConvertible {
  var hashValue : Int { return id }
  var description : String { return name }
  let id : Int
  let name : String
}
func == (player1 : Player, player2 : Player) -> Bool {
  return player1.id == player2.id
}
protocol PointHandler : CustomStringConvertible {
  mutating func addPointForPlayer(player : Player)
  var score : [Player:Int] { get }
}
extension PointHandler {
  var description : String {
  return self.score.description
  }
}
protocol TennisEvent : PointHandler {
  typealias TennisItem : PointHandler
  var items : [TennisItem] { get set }
}
extension TennisEvent {
  /
  var score : [Player:Int] {
  var score : [Player:Int] = [:]
  for item in self.items {
  var mostPoints = 0
  var bestPlayer : Player? = nil
  for (player,points) in item.score {
  if points > mostPoints {
  mostPoints = points
  bestPlayer = player
  }
  }
  if let player = bestPlayer {
  var currentScore = score[player] ?? 0
  score[player] = ++currentScore
  }
  }
  return score
  }
  / default implementation to add a point to the latest item */
  mutating func addPointForPlayer(player : Player) {
  var latestItem = self.items.removeLast()
  latestItem.addPointForPlayer(player)
  self.items.append(latestItem)
  }
}
struct Game : PointHandler {
  /
  var points : [Player:Int] = [:]
  var score : [Player:Int] { return points }
  /
  mutating func addPointForPlayer(player: Player) {
  var points = getPoints(player)
  self.points[player] = ++points
  }
  /
  func getPoints(player: Player) -> Int {
  return points[player] ?? 0
  }
}
struct TennisSet : TennisEvent {
  var items : [Game]
  init() {
  self.items = [Game()]
  }
}
struct Match : TennisEvent {
  var items : [TennisSet]
  init() {
  self.items = [TennisSet()]
  }
}
let player1 = Player(id: 1, name: "Defending Champ")
let player2 = Player(id: 2, name: "Someone Else")
var match = Match()
match.addPointForPlayer(player1)
match.addPointForPlayer(player1)
match.addPointForPlayer(player2)
print( match )

Thank you so much for taking the time to help me. I also thought about conforming Player to Hashable but I had problems with it because I was using Participant protocol (which Player implements) type for dictionary key. What if I wanted to define Match as a protocol? I want to do this because the controller which handles the match shouldn't know what kind of match is played (tennis, etc.). I can't use any protocol which has Self or associated type requirements like TennisEvent has.

I see. You are looking for a design that allows for multiple types of matches. That would take some serious design work beyond what I can provide here. The only guidance I have is that you can mix protocols with and without Self constraints as you need.

That's fine. This already helps me a lot.

Protocol-oriented architecture
 
 
Q