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.