Programming for Fun and Profit - Using the Card.dll, Page 2
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
Ace
Two
Three
Four
Five
Six
Seven
Eight
Nine
Ten
Jack
Queen
King
End Enum
Public Enum Suit
Diamond
Club
Heart
Spade
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
cdtTerm()
End Sub
Public Sub New(ByVal cardSuit As Suit, ByVal cardFace As Face)
Init()
FCardSuit = cardSuit
FCardFace = cardFace
End Sub
Public Property CardSuit() As Suit
Get
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()
Try
Dim Card As Integer = CType(Me.FCardFace, Integer) * 4 + FCardSuit
cdtDrawExt(hdc, x, y, MyClass.width, MyClass.height, Card, 0, 0)
Finally
g.ReleaseHdc(hdc)
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()
Try
cdtDrawExt(hdc, x, y, MyClass.width, MyClass.height, 61, 0, 0)
Finally
g.ReleaseHdc(hdc)
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
Card.Init()
End Sub
Private Sub Main_Closing(ByVal sender As Object, _
ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
Card.Deinit()
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.
