Golf Tracker - Refactor for Tees Vertical
Now comes the moment of truth - having to realize this vision of refactoring into a real application. This image will remind you what I need to do to modify this application.I need to add the Tee table to the database and then build all the necessary components to go with it, including the business objects, the dataobjects, data mappers, repositories, services, all the way up to the MVC application and controllers and views.
As I mentioned in the last episode, I've decided to build an API that will handle all the Round calculating functions, and this API will be called from the service layer. This way the controllers are still only calling the services, and never calling the repositories directly. It's actually ok to call repositories from the controllers but I like to have the extra bit of abstraction with services or WCF in case further calculations need to be done, etc.
One of the first things I need to do is add the Tee table to the database.
This will store all the information for the various tees for golf courses. You can see it has a Foreign Key to the Course table with the CourseID. Now that we have this table I can create the business object.
01.
using
System;
02.
using
System.ComponentModel;
03.
using
System.ComponentModel.DataAnnotations;
04.
05.
namespace
GolfTracker.BusinessObjects
06.
{
07.
public
class
Tee
08.
{
09.
[ScaffoldColumn(
false
)]
10.
public
Guid ID {
get
;
set
; }
11.
12.
[Required(ErrorMessage =
"Tee Name is required."
)]
13.
[DisplayName(
"Tee Name"
)]
14.
public
string
TeeName {
get
;
set
; }
15.
16.
public
Guid CourseID {
get
;
set
; }
17.
18.
[Required(ErrorMessage =
"Course Rating is required."
)]
19.
[DisplayName(
"Course Rating"
)]
20.
public
decimal
CourseRating {
get
;
set
; }
21.
22.
[Required(ErrorMessage =
"Slope is required."
)]
23.
public
int
Slope {
get
;
set
; }
24.
25.
[Required(ErrorMessage =
"Yardage is required."
)]
26.
public
int
Yardage {
get
;
set
; }
27.
28.
[Required(ErrorMessage =
"Par is required."
)]
29.
public
int
Par {
get
;
set
; }
30.
31.
[Required(ErrorMessage =
"Tee Gender is required."
)]
32.
[DisplayName(
"Tee Gender"
)]
33.
public
string
TeeGender {
get
;
set
; }
34.
35.
public
string
rowversion {
get
;
set
; }
36.
}
37.
}
In the DataObjects I create an ITeeRepository interface that will contain a single member.
01.
using
System;
02.
using
System.Collections.Generic;
03.
04.
namespace
GolfTracker.DataObjects.Interfaces
05.
{
06.
public
interface
ITeeRepository : ICRUDRepository<GolfTracker.BusinessObjects.Tee>
07.
{
08.
List<GolfTracker.BusinessObjects.Tee> GetTeesForCourse(Guid id);
09.
}
10.
}
This will return a list of tees for a selected golf course.
The IRoundRepository interface will also have a single member that just gets a single round for a player.
01.
using
System;
02.
using
System.Collections.Generic;
03.
04.
namespace
GolfTracker.DataObjects.Interfaces
05.
{
06.
public
interface
IRoundRepository : ICRUDRepository<GolfTracker.BusinessObjects.Round>
07.
{
08.
List<GolfTracker.BusinessObjects.Round> GetByPlayer(Guid id);
09.
}
10.
}
This will return all the rounds for the selected player. This will be displayed on the details page for the player, showing the rounds played and all the statistics.
RoundManagerAPI
The RoundManageAPI is simply a separate project that contains a class that will handle all the calculations that have to do with posting a round or displaying rounds for a player. Since there are lots of calculations that are necessary for these actions, having them in one place seems the right thing to do.Here's the entire code for the RoundManager class.
001.
using
System;
002.
using
System.Collections.Generic;
003.
using
System.Linq;
004.
using
GolfTracker.BusinessObjects;
005.
using
GolfTracker.DTO;
006.
using
GolfTracker.DataObjects.Interfaces;
007.
008.
namespace
GolfTracker.Api
009.
{
010.
public
class
RoundManager : IRoundManager
011.
{
012.
private
IRoundRepository _roundRepository;
013.
private
ITeeRepository _teeRepository;
014.
private
IPlayerRepository _playerRepository;
015.
private
ICourseRepository _courseRepository;
016.
017.
public
RoundManager(IRoundRepository roundRepository, ITeeRepository teeRepository,
018.
IPlayerRepository playerRepository, ICourseRepository courseRepository)
019.
{
020.
this
._roundRepository = roundRepository;
021.
this
._teeRepository = teeRepository;
022.
this
._playerRepository = playerRepository;
023.
this
._courseRepository = courseRepository;
024.
}
025.
026.
#region IRoundManager Members
027.
028.
public
void
SaveRound(GolfTracker.BusinessObjects.Round round)
029.
{
030.
GolfTracker.BusinessObjects.Tee tee = _teeRepository.GetById(round.TeeID);
031.
032.
round.Differential = CalculateDifferential(round.Score, tee.CourseRating, tee.Slope);
033.
round.CourseRatingAndSlope = tee.CourseRating.ToString() +
"/"
+ tee.Slope.ToString();
034.
035.
_roundRepository.Insert(round);
036.
}
037.
038.
public
GolfTracker.DTO.RoundDto GetHistory(
string
indexNumber)
039.
{
040.
Player player = _playerRepository.GetByIndexNumber(indexNumber);
041.
042.
RoundDto dto =
new
RoundDto();
043.
dto.PlayerName = player.FirstName +
" "
+ player.LastName;
044.
dto.IndexNumber = player.IndexNumber;
045.
dto.ClubName = player.ClubName;
046.
dto.HistoryItems = GetPlayerHistory(player.ID);
047.
dto.HandicapIndex = CalculateHandicapIndex(dto.HistoryItems);
048.
049.
return
dto;
050.
}
051.
052.
public
SuccessDto GetSuccess(Guid roundId)
053.
{
054.
Round round = _roundRepository.GetById(roundId);
055.
Player player = _playerRepository.GetById(round.PlayerID);
056.
Tee tee = _teeRepository.GetById(round.TeeID);
057.
Course course = _courseRepository.GetById(tee.CourseID);
058.
059.
SuccessDto dto =
new
SuccessDto();
060.
dto.RoundID = roundId;
061.
dto.PlayerID = player.ID;
062.
dto.PlayerName = player.FirstName +
" "
+ player.LastName;
063.
dto.IndexNumber = player.IndexNumber;
064.
dto.CourseName = course.CourseName;
065.
dto.CourseRating = tee.CourseRating.ToString();
066.
dto.Slope = tee.Slope.ToString();
067.
dto.City = course.City;
068.
dto.State = course.State;
069.
dto.Score = round.Score.ToString();
070.
dto.DatePlayed = round.DatePlayed.ToShortDateString();
071.
dto.Differential = round.Differential.ToString();
072.
073.
return
dto;
074.
}
075.
076.
#endregion
077.
078.
#region Private Methods
079.
080.
/// <summary>
081.
/// Calculate the HandicapIndex for the given differentials.
082.
/// </summary>
083.
/// <param name="items"></param>
084.
/// <returns></returns>
085.
private
string
CalculateHandicapIndex(List<HistoryItemDto> items)
086.
{
087.
string
result =
string
.Empty;
088.
089.
if
(items.Count == 0)
090.
{
091.
return
result;
092.
}
093.
094.
// Get the lowest differentials of the list
095.
var newItems = items.OrderBy(d => Convert.ToDecimal(d.Differential)).AsEnumerable();
096.
newItems = newItems.Take(TakeValue(newItems.Count()));
097.
098.
// Get the average of the taken items
099.
var avg = newItems.Average(h => Convert.ToDecimal(h.Differential));
100.
101.
// Multiply by .96
102.
var plusBonus = avg * 0.96m;
103.
104.
// Trim after tenths (do not round)
105.
string
str = plusBonus.ToString();
106.
int
dot = str.IndexOf(
"."
);
107.
if
(dot == -1)
108.
throw
new
ArgumentException(
"Dot not found."
);
109.
110.
result = str.Substring(0, dot + 2);
111.
112.
return
result;
113.
}
114.
115.
116.
/// <summary>
117.
/// Get the player history.
118.
/// </summary>
119.
/// <param name="playerId"></param>
120.
/// <returns></returns>
121.
private
List<HistoryItemDto> GetPlayerHistory(Guid playerId)
122.
{
123.
List<HistoryItemDto> historyList =
new
List<HistoryItemDto>();
124.
HistoryItemDto dto =
null
;
125.
126.
var rounds = _roundRepository.GetByPlayer(playerId)
127.
.OrderByDescending(r => r.DatePlayed).Take(20);
128.
129.
foreach
(var item
in
rounds)
130.
{
131.
dto =
new
HistoryItemDto();
132.
dto.DatePlayed = item.DatePlayed.ToShortDateString();
133.
dto.Score = item.Score.ToString();
134.
dto.CourseRatingAndSlope = item.CourseRatingAndSlope;
135.
dto.Differential = item.Differential.ToString();
136.
historyList.Add(dto);
137.
}
138.
139.
return
historyList;
140.
}
141.
142.
/// <summary>
143.
/// Calculate the differential for inserting with new scores.
144.
/// </summary>
145.
/// <param name="score"></param>
146.
/// <param name="courseRating"></param>
147.
/// <param name="slope"></param>
148.
/// <returns></returns>
149.
private
decimal
CalculateDifferential(
int
score,
decimal
courseRating,
int
slope)
150.
{
151.
decimal
result = 0.0m;
152.
153.
if
(score <= 0)
154.
throw
new
ArgumentNullException(
"Score"
);
155.
156.
if
(courseRating <= 0.0m)
157.
throw
new
ArgumentNullException(
"Course Rating"
);
158.
159.
if
(slope <= 0)
160.
throw
new
ArgumentNullException(
"Slope"
);
161.
162.
result = ((score - courseRating) * 113) / slope;
163.
164.
return
Math.Round(result, 1);
165.
}
166.
167.
/// <summary>
168.
/// Used in CalculateHandicapIndex to determine
169.
/// how many records to take based on the overall
170.
/// record count.
171.
/// </summary>
172.
/// <param name="count"></param>
173.
/// <returns></returns>
174.
private
int
TakeValue(
int
count)
175.
{
176.
int
result = 0;
177.
178.
switch
(count)
179.
{
180.
case
5:
181.
result = 1;
182.
break
;
183.
case
6:
184.
result = 1;
185.
break
;
186.
case
7:
187.
result = 2;
188.
break
;
189.
case
8:
190.
result = 2;
191.
break
;
192.
case
9:
193.
result = 3;
194.
break
;
195.
case
10:
196.
result = 3;
197.
break
;
198.
case
11:
199.
result = 4;
200.
break
;
201.
case
12:
202.
result = 4;
203.
break
;
204.
case
13:
205.
result = 5;
206.
break
;
207.
case
14:
208.
result = 5;
209.
break
;
210.
case
15:
211.
result = 6;
212.
break
;
213.
case
16:
214.
result = 6;
215.
break
;
216.
case
17:
217.
result = 7;
218.
break
;
219.
case
18:
220.
result = 8;
221.
break
;
222.
case
19:
223.
result = 9;
224.
break
;
225.
case
20:
226.
result = 10;
227.
break
;
228.
default
:
229.
result = 1;
230.
break
;
231.
}
232.
233.
return
result;
234.
}
235.
#endregion
236.
237.
}
238.
}
One significant thing to notice here is that I've decided to have the types that will be returned to the views will be straight DTOs (data transfer objects). So this means that I am flattening all values that populate the DTO properties and converting them all to strings. There is no use for anything other than string values here.
By doing this also lets the view simply display the data instead of possibly having to convert values.
So to produce this view...
... the round manager would populate and return a RoundDto object.
01.
using
System.Collections.Generic;
02.
03.
namespace
GolfTracker.DTO
04.
{
05.
public
class
RoundDto
06.
{
07.
public
string
PlayerName {
get
;
set
; }
08.
public
string
IndexNumber {
get
;
set
; }
09.
public
string
HandicapIndex {
get
;
set
; }
10.
public
string
ClubName {
get
;
set
; }
11.
public
List<HistoryItemDto> HistoryItems {
get
;
set
; }
12.
}
13.
}
The list of HistoryItems is the information for all the rounds of golf for the selected player.
01.
namespace
GolfTracker.DTO
02.
{
03.
public
class
HistoryItemDto
04.
{
05.
public
string
DatePlayed {
get
;
set
; }
06.
public
string
Score {
get
;
set
; }
07.
public
string
CourseRatingAndSlope {
get
;
set
; }
08.
public
string
Differential {
get
;
set
; }
09.
}
10.
}
Managing Data
In order to manage the data, including the golf courses and tees and adding and managing new players, I built an MVC Area called Dashboard. But in order to make this work as it would in a real world application I included the ASP.NET Membership system into the application. This way only logged on Administrators will be able to add players or golf course information.Players can easily post rounds of golf without logging in, since that isn't sensitive information.
The easy way to get to the Dashboard to create a golf course is to go to the home page of the application and click on the "Create Courses" link in Step 1.
This will take you to the log on page.
I've included the username and password above the fields so there's no need to have to remember them. Once you log in you'll be able to manage any particular section.
From here you can create or edit a course, or manage the tees for a course.
To edit a Tee click the "Edit" link and you'll see this form.
Conclusion
There are lots more happening under the covers and to describe it all in the article would involve a lot of reading. So download the source code from the Golf Tracker Kit and examine the code at your leisure.I'll wrap up in the next episode and go through in a little more detail what I've done to complete this application.
Stay tuned.