The next stage in the Application pattern is to design the domain level classes. This stage is perhaps somewhat different from other visual programming systems which encourage you to build an application starting with the user interface. Although it is possible to build an application this way in Dolphin, we would recommend against it since it can obfuscate the design process. It is much cleaner to design and build the core of the system, the domain model, first and then to add the user interface classes subsequently. Indeed, this is just as easy as the standard "visual method" because the Smalltalk development environment provides tools to directly test the domain classes as you go; you do not need to build a user interface to do this.
We're going to add 3 domain level classes for our application.
PersonalMoney. This class will maintain and owner's name and a list of accounts.
PersonalAccount. This class will maintain account details and a list of transactions on that account held in date order. It will also provide a facility to calculate the current balance of the account.
PersonalAccountTransaction. Instances of this class represent transactions on PersonalAccounts. Each one holds details such as the amount, the description, and the date of the transaction.
For each of the classes below follow the New Class pattern.
The PersonalMoney class can be subclassed from Model. This will usually be the norm for domain model classes that are liable to trigger update events when aspects of them change. You might choose a different superclass if you need to inherit specific behaviour from another class or if your model does not trigger any update events (in this latter case a more suitable superclass might be Object). Define the PersonalMoney class as follows:
Model subclass: #PersonalMoney instanceVariableNames: 'owner accounts ' classVariableNames: '' poolDictionaries: ''
The owner instance variable will hold a string containing the user's name, and the account variable will hold a collection of account objects.
The next responsibility is to add any specialized instance creation methods that the class may require. We have two instance variables here that require initialization and the normal method to ensure that this occurs is to add an initialize instance method to the class and arrange that this is called from whenever an instance is created. In fact, the Model superclass contains a new method that sends #initialize to all newly created instances, therefore we only need to add the initialize functionality since it will be called by default.
initialize "Private - Initialize the receiver"
accounts := ListModel with: OrderedCollection new.
We initialize our accounts collection with a ListModel rather than just an OrderedCollection because we'll want to receive change notifications as the list is updated. If this was not the case then a plain OrderedCollection would do just fine.
The next step is to add Accessor Methods to allow other objects access to the contents of the instance variables.
owner "Answer the owner of the receiver"
^owner.
owner: aString "Set the owner of the receiver to aString"
owner := aString. accounts "Answer the accounts collection" ^accounts
There is no need to add an accessor method to set the accounts collection since this is only ever necessary during the initialization of an instance. Consequently this can be considered a private operation which does not warrant such a method.
Tip: as you add each method to the system using the Class Hierarchy Browser it's a good idea to categorize them as you go. An easy way to do this is to use the Method/Suggest Category command. Choose this with a method selected in the browser and you will be presented with a list of potential categories for the method. If none of these is suitable you can add a new category using the New button.
The next duty, according to the New Class pattern, is to define the classs behaviour. A useful method to add to all new classes is #printOn: to print the contents of an object in a form suitable for a developer to read. This will aid later testing and debugging.
printOn: aStream "Append, to aStream, a String whose characters are a description of the receiver as a developer would want to see it." self basicPrintOn: aStream. aStream nextPut: $(. self owner printOn: aStream. aStream nextPut: $).
We are also going to need to add new PersonalAccounts to the application and also to allow them to be removed.
addAccount: aPersonalAccount "Add aPersonalAccount to the collection of accounts owned by the receiver Answers aPersonalAccount" ^self accounts add: aPersonalAccount
removeAccount: aPersonalAccount "Remove aPersonalAccount from the collection of accounts owned by the receiver. Answers aPersonalAccount" ^self accounts remove: aPersonalAccount
Notice the use of descriptive parameter names for these methods which aids readability.
Let's now create a class to represent each account.
Model subclass: #PersonalAccount instanceVariableNames: 'name accountNumber initialBalance transactions currentBalance' classVariableNames: '' poolDictionaries: ''
Each account instance holds some details about the account, such as the name, the account number and the initial balance, together with a list of all the transactions on the account. These variables obviously require initialization to place an instance into a known state.
initialize "Private - Initialize the receiver" name := 'New account'. initialBalance := currentBalance := 0.0. transactions := ListModel with: (SortedCollection sortBlock: [:x :y | x date <= y date])
Once again our list is to be a ListModel, but this time it wraps a SortedCollection capable of sorting the account objects that it holds based on their dates.
Now add the following accessor methods.
accountNumber "Answer the account number of the receiver" ^accountNumber.
accountNumber: aString "Set the account number of the receiver to aString"
accountNumber := aString.
currentBalance "Answer the calculated current balance of the receiver." ^currentBalance
currentBalance: aNumber "Set the current balance of the receiver to aNumber." currentBalance := aNumber. self trigger: #currentBalanceChanged.
initialBalance "Answer the account initial balance of the receiver" ^initialBalance.
initialBalance: aNumber "Set the account initial balance of the receiver to aNumber" initialBalance := aNumber. self calculateCurrentBalance
name "Answer the account name of the receiver" ^name.
name: aString "Set the account name of the receiver to aString" name := aString.
transactions "Answer the transactions collection" ^transactions.
Again there is no need to provide a method to set the transactions ListModel.
The following methods add behaviour to the class.
printOn: aStream "Append, to aStream, a String whose characters are a description of the receiver as a developer would want to see it." self basicPrintOn: aStream. aStream nextPut: $(. self name displayOn: aStream. aStream nextPut: $,. self accountNumber displayOn: aStream. aStream nextPut: $,. self currentBalance displayOn: aStream. aStream nextPut: $).
displayOn: aStream "Append, to aStream, a String whose characters are a description of the receiver as a user would want to see it." self name displayOn: aStream. aStream nextPut: $-. self accountNumber displayOn: aStream.
addTransaction: aPersonalAccountTransaction "Add aPersonalAccountTransaction to the collection of transactions owned by the receiver. Answers aPersonalAccountTransaction" self transactions add: aPersonalAccountTransaction. self calculateCurrentBalance. ^aPersonalAccountTransaction.
removeTransaction: aPersonalAccountTransaction "Remove aPersonalAccountTransaction from the collection of transactions owned by the receiver. Answers aPersonalAccountTransaction"
self transactions remove: aPersonalAccountTransaction. self calculateCurrentBalance. ^aPersonalAccountTransaction.
calculateCurrentBalance "Calculate and set the current balance of the receiver. Add the total of all the actual amounts to the initial balance" | balance | balance := self transactions inject: self initialBalance into: [:total :each | total + each actualAmount ]. self currentBalance: balance.
One more class to go and then we can do some experimentation. Now will define the class that represents a transaction on a PersonalAccount.
Model subclass: #PersonalAccountTransaction instanceVariableNames: 'date description amount isDebit ' classVariableNames: '' poolDictionaries: ''
Each transaction holds a descriptive String and a numeric amount, together with the date of the transfer. The isDebit flag is a boolean to indicate whether the transaction is a debit or a credit operation on the account.
initialize "Private - Initialize the receiver"
date := Date today. description := String new. amount := 0.0. isDebit := true.
The initialization and accessor methods are again straightforward.
amount "Answer the amount of the receiver" ^amount.
amount: aNumber "Set the amount of the receiver to aNumber" amount := aNumber.
date "Answer the date of the receiver" ^date.
date: aDate "Set the date of the receiver to aDate" date := aDate.
description "Answer the String description of the receiver" ^description.
description: aString "Set the description of the receiver to aString" description := aString.
isDebit "Answer true if the receiver is a Debit transaction" ^isDebit.
isDebit: aBoolean "Set the receiver to a Debit transaction according to aBoolean. If aBoolean is true then the receiver is treated as a credit" isDebit := aBoolean.
It will be useful to provide a proper method that answers the effective amount for the transaction taking in into consideration the isDebit flag.
actualAmount "Answer the actual amount of the receiver taking into consideration whether it is a debit or credit transaction" ^self isDebit ifTrue: [ self amount negated ] ifFalse: [ self amount ].
Again we provide a printOn: method, but this time we also include a displayOn: method to determine how a transaction will appear when presented to the user as text. By default it is this method that is used to display an object within a list or text view on the screen.
printOn: aStream "Append, to aStream, a String whose characters are a description of the receiver as a developer would want to see it." self basicPrintOn: aStream. aStream nextPut: $(. self displayOn: aStream. aStream nextPut: $).
displayOn: aStream "Append, to aStream, a String whose characters are a description of the receiver as a user would want to see it." self date displayOn: aStream. aStream nextPutAll: ': '. aStream nextPutAll: self description. aStream nextPutAll: (self isDebit ifTrue: [' - debit '] ifFalse: [ ' - credit ']). self amount displayOn: aStream.
Click here to move on to the next section or here to go back to the previous section.