2

Assume we want to model a table where players can sit down to get together to play a game (card games, dice games, ...). Assume a few properties associated with each seat

class Seat
{
    public int PlayerId { get; set; }
    public double Money { get; set; }
    // ...
}

Say the Table is simply modelled as

class Table
{
    public Seat[] Seats = new Seat[6];
}

And I want to represent empty seats. Is it a better practise to have Seats[i] = null or adding a Flag / Enumeration / ... that indicates the Seat is empty and all properties are invalid? Like if (Seats[i].SeatState != Seatstate.Empty) { ... }

Christophe
  • 74,672
  • 10
  • 115
  • 187
Benj
  • 169
  • 5
  • What does the seat represent? Does it represent a seat (i.e. a chair), or a seated player (i.e. the person on the chair)? Based on the content of your `Seat` class, I'm inclined to read this as a seated player rather than a seat by itself. Naming matters, and it helps make things clearer as to what your expectations of the code's behavior should be. – Flater Nov 08 '19 at 09:28
  • 1
    @Flater I think it's `Seat` like a spot, so it's likely a `Chair` (or position) around a `Table`. Not a `Player` state. – Laiv Nov 08 '19 at 09:35
  • @Laiv: If that were the case, it seems counterintuitive for this seat class to have a money property attached to it. Hence my question to ask for elaboration. – Flater Nov 08 '19 at 09:37
  • In one or another design, `Money` seems out of the place. – Laiv Nov 08 '19 at 09:39
  • @Flater It is indeed a spot at a table from which a player can stand-up and sit-down. I added `Money` directly to the seat to not further complicate the example. I could indeed instead use a Player object – Benj Nov 08 '19 at 09:42
  • @Benj: Brevity does not always make things easier. The lack of separation between a seat and a player can lead to different conclusions being made on how to handle the overly terse structure. – Flater Nov 08 '19 at 09:47
  • 3
    Are you familiar with the Special Case Pattern? (aka Null Object Pattern)? – Laiv Nov 08 '19 at 11:50
  • @Laiv That sounds interesting. Will have a look at it. – Benj Nov 08 '19 at 13:00
  • Null object pattern is a powerful one for this sort of application. It is particularly powerful if you also go with an immutable object architecture. That is, a change to the world is not modeled by *mutating a variable* but by *creating a new immutable world that has the change*. That sounds expensive, but if you are clever you can re-use most of the original state; after all, it is immutable! – Eric Lippert Nov 22 '19 at 19:39

1 Answers1

3

A Seat should know whether or not it is occupied. Simply because someone joins or leaves the game doesn't mean you need a new Seat(). The player has just vacated that spot at the table.

That being said, the PlayerId and Money properties are currently value types, meaning they will always have a value. In this case it appears they do not always have a value, so the first change should be to make those properties nullable (the second change I would make is to use decimal as the data type for the Money property).

Currently everything in both Table and Seat is public. In the very least you want to enforce some encapsulation, and make the setters for your properties private. My first sentence has some bold face words and phrases, which you can use to guide your design of the Seat class:

  • is occupied
  • joins or leaves the game
  • vacated

These give you some of the basic operations that can be performed on a Seat. The Seat class should have a property that returns whether or not this particular seat is currently occupied. When both PlayerId and Money are null, then seat.IsOccupied should return false.

This leads us to discover our first class invariant: when PlayerId is not null, then Money should not be null as well. The setters for both properties need to be private. Something needs to ensure when a Seat becomes occupied that both player and money have values. We need a public method on the Seat class called Occupy that takes both values: public void Occupy(int playerId, decimal money). Here we can use the Type System in C# to ensure both values are not null by virtue of the fact they are both value types.

Now when you create a new Seat() the player Id and money properties are both null and seat.IsOccupied returns false. After calling seat.Occupy(45, 1200.00m) both properties are set, and then seat.IsOccupied returns true.

Finally, when someone leaves the game, they vacate the seat. The seat.Vacate() method should set both PlayerId and Money to null, which will cause seat.IsOccupied to return false.

var seat = new Seat(); // Create vacant seat

seat.Occupy(45, 50.00m);

// Play game

seat.Vacate(); // Spouse calls. Kids are going crazy. Time to go home.

But we still have a problem. Calling seat.Vacate() makes the player (and their money) disappear. While this works out great for players that owe the House money (or maybe it doesn't...) players that still have money do not want to blink out of existence simply because they left the game. You need a Player class to encapsulate the player Id and money properties. So a Seat really only needs a Player in order to determine if it IsOccupied:

var player = new Player(45, 50.00m);
var blackJackTable = new Table();
var texasHoldemTable = new Table();

blackJackTable.Seats[0].Occupy(player);
Console.Writeline(blackJackTable.Seats[0].IsOccupied); // Prints "True"
blackJackTable.Seats[0].Vacate();

texasHoldemTable.Seats[2].Occupy(player);
texasHoldemTable.Seats[2].Vacate(); // Spouse calls. Kids are going crazy. Time to go home.

Now a Player can occupy and vacate a seat, and then transfer their winnings to another game.

But we still have a problem. You need to know which seat is available when joining a table. The Table class needs some encapsulation as well. The Table class should expose a public method allowing you to join the table. In fact, public Seat Join(Player newPlayer) would be a great method to add to the Table class. That encapsulates logic for joining the table. It returns a Seat which at a later time the player can choose to Vacate(). By exposing a public boolean property on Table called IsFull that returns true if all Seats are occupied, code outside the Table class does not need access to the Seats array. The Seats array should be completely hidden. Make Seats a private, read-only property. This is called information hiding, and is another fundamental concept of object oriented programming.

Greg Burghardt
  • 34,276
  • 8
  • 63
  • 114