March 1, 2021
Hot Topics:

Programming for Fun and Profit - Using the Card.dll

  • By Paul Kimmel
  • Send Email »
  • More Articles »

Good Abstractions are an Abstruse Art

The caveat divid et impera—divide and conquer—is our guiding principle. A big part of what divide and conquer means relative to programming is that if we divide a problem into the correct abstractions then we can conquer the problem. In fact, taking this concept a bit further, if we divide a problem into good abstractions the problem is significantly easier to conquer.

Finding good abstractions is difficult to do however. Yet, by practicing this abstruse art in a well-understood problem domain—for example, the notion of playing cards—we can become more practiced at finding good abstractions early. Applying the notion of divide and conquer to our card drawing tools, we can quickly resolve on some reasonably good abstractions.

There are only four suits available to us. Certainly we can use integers to express card suits but an enumeration is more expressive. Another similar abstraction is an enumeration representing the possible face values of the cards Ace to King. Listing 4 demonstrates our enumerations.

Listing 4: Using enumerations makes the notion of suit and face-value constrained to a specific set of named values and more expressive to the human reader.

Imports System

Public Enum Face
End Enum

Public Enum Suit
End Enum

Now when we talk about the value of a card we can do so in the domain of the problem: cards have a suite and a face value.

Clearly in the domain of playing cards is the notion of a card. A single card class would be a good place to add a constructor, initializing the face value and suit, and a good place to add our paint methods. Listing 5 demonstrates the new Card class with the aforementioned features.

Listing 5: The Card class.

Imports System
Imports System.Drawing

Public Class Card
  Private FCardFace As Face
  Private FCardSuit As Suit

#Region "External methods and related fields"
  Private Shared initialized As Boolean = False
  Private Shared width As Integer = 0
  Private Shared height As Integer = 0

  Private Declare Function cdtInit Lib "cards.dll" ( _
    ByRef width As Integer, ByRef height As Integer) As Boolean
  Private Declare Function cdtDrawExt Lib "cards.dll" ( _
    ByVal hdc As IntPtr, ByVal x As Integer, ByVal y As Integer, _
    ByVal dx As Integer, ByVal dy As Integer, ByVal card As Integer, _
    ByVal suit As Integer, ByVal color As Long) As Boolean
  Private Declare Sub cdtTerm Lib "cards.dll" ()
#End Region

  Public Shared Sub Init()
    If (initialized) Then Return
    initialized = True
    cdtInit(width, height)
  End Sub

  Public Shared Sub Deinit()
    If (Not initialized) Then Return
    initialized = False
  End Sub

  Public Sub New(ByVal cardSuit As Suit, ByVal cardFace As Face)

    FCardSuit = cardSuit
    FCardFace = cardFace

  End Sub

  Public Property CardSuit() As Suit
    Return FCardSuit
  End Get
  Set(ByVal Value As Suit)
    FCardSuit = Value
  End Set
  End Property

  Public Sub PaintGraphicFace(ByVal g As Graphics, ByVal x As Integer, _
    ByVal y As Integer)

    Dim hdc As IntPtr = g.GetHdc()
      Dim Card As Integer = CType(Me.FCardFace, Integer) * 4 + FCardSuit
      cdtDrawExt(hdc, x, y, MyClass.width, MyClass.height, Card, 0, 0)
    End Try
  End Sub

  Public Sub PaintGraphicBack(ByVal g As Graphics, ByVal x As Integer, _
    ByVal y As Integer)

    Dim hdc As IntPtr = g.GetHdc()
      cdtDrawExt(hdc, x, y, MyClass.width, MyClass.height, 61, 0, 0)
    End Try

  End Sub

End Class

From the listing you can see that we made the API methods private. This eliminates consumers of card from calling them directly. The shared Init and Deinit method make a valiant effort to ensure cdtInit is called just once, but in this implementation the Deinit method will need to be called by the consumer. (We could implement IDisposable, have the constructor track how many cards were created, and the Dispose method decrement the counter, calling Deinit when the counter is 0.) Finally, we wrap the cards.dll API methods in wrapper methods to ensure that the device context resource is managed correctly every time. (Notice that we eliminated the width and height arguments (dx and dy respectively) of the two paint methods. The width and height are fixed by the cdtInit method, so we might as well use this information.) The result is that our form is radically simplified and we can reuse Card, Suit, and Face in any Windows solution we'd like. Here is the revised form code (see listing 6).

Listing 6: A Form using the new Card class.

Public Class Main
    Inherits System.Windows.Forms.Form

[ Windows Form Designer generated code ]

  Private Ace As Card = New Card(Suit.Spade, Face.Ten)

  Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
    Ace.PaintGraphicFace(e.Graphics, 10, 10, 75, 100)
  End Sub

  Private Sub Main_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load
  End Sub

  Private Sub Main_Closing(ByVal sender As Object, _
    ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
  End Sub
End Class

After creating the Card class all we need to do is create an instance of a card and paint it in the OnPaint event handler.

Thus far we have discovered the easy abstractions. Suit, Face, and Card are pretty easy to find. The hard part is finding as many of the abstractions as we can relative to our problem domain. For example, it is reasonable that we might want a collection of cards, referred to as a deck. However, a pinochle deck has no cards less than 9 but many other games need all of the cards. Furthermore some games like BlackJack might use multiple decks and cards like the Ace may have more than one value depending on context: Ace can be electively used to represent the value of 1 or 11.

Now we have moved into the realm of moderate complexity. What if we want one set of classes to represent all games? What about rules? How do we codify rules to permit changing the rules' object depending on the game selected? What if we want to support Internet play, console play, or multi-player games? Our challenges become significantly greater.

The objective is to figure out what your real goals are and to code to support those goals. If it is possible to support known objectives and permit future growth—for instance, supporting multiple games and one player while leaving room for multi-player in the future—then you are likely to exceed your customer's expectations.

Page 2 of 3

This article was originally published on January 26, 2004

Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Thanks for your registration, follow us on our social networks to keep up-to-date