1
\$\begingroup\$

Historically in my classes I've simply had a number of methods which would return DataSets based on data returned from a stored procedure. Recently however I've moved to giving my classes properties and returning data via lists as shown in the class below.

public class Contribution
{
    public int MemberNumber { get; }
    public DateTime DateDue { get; }
    public DateTime DateReceived { get; }
    public string TransactionType { get; }
    public string ContributionType { get; }
    public decimal ContributionAmount { get; }
    public decimal ContributionExpenses { get; }
    public decimal SocialTax { get; }
    public decimal Insurance { get; }
    public decimal AdminFee { get; }
    public decimal BrokerFee { get; }
    public decimal Investable { get; }

    public Contribution(int memberNumber, DateTime dateDue, DateTime dateReceived, string transactionType, string contributionType, decimal contributionAmount, decimal contributionExpenses, decimal socialTax, decimal insurance, decimal adminFee, decimal brokerFee, decimal investable)
    {
        MemberNumber = memberNumber;
        DateDue = dateDue;
        DateReceived = dateReceived;
        TransactionType = transactionType;
        ContributionType = contributionType;
        ContributionAmount = contributionAmount;
        ContributionExpenses = contributionExpenses;
        SocialTax = socialTax;
        Insurance = insurance;
        AdminFee = adminFee;
        BrokerFee = brokerFee;
        Investable = investable;
    }

    public static List<Contribution> GetContributions(Member member, DateTime startDate, DateTime endDate)
    {
        var contributions = new List<Contribution>();

        string[] adminCodes = {"AADMIN", "EADMIN", "RADMIN"};
        string[] brokerCodes = {"ABROK", "EBROK", "RBROK"};

        var insuranceCodes = Classes.Member.Insurance.GetCodes(member);

        var products = Product.GetProducts(member);

        using (var connection = new SqlConnection(Common.GetConnectionString()))
        {
            var cmd = new SqlCommand("GetMemberContributions", connection) {CommandType = CommandType.StoredProcedure};
            cmd.Parameters.Add("@MemberNumber", SqlDbType.Int).Value = member.MemberNumber;
            cmd.Parameters.Add("@StartDate", SqlDbType.DateTime).Value = startDate;
            cmd.Parameters.Add("@EndDate", SqlDbType.DateTime).Value = endDate;

            connection.Open();

            var ds = new DataSet();
            var da = new SqlDataAdapter(cmd);
            da.Fill(ds);

            if (ds.Tables[0].Rows.Count == 0)
                return contributions;

            foreach (DataRow row in ds.Tables[0].Rows)
            {
                DateTime.TryParse(row["Due"].ToString().Trim(), out DateTime dateDue);
                DateTime.TryParse(row["Received"].ToString().Trim(), out DateTime dateReceived);
                decimal.TryParse(row["Amount"].ToString().Trim(), out decimal amount);
                decimal.TryParse(row["Expenses"].ToString().Trim(), out decimal expenses);

                var transactionType = row["TransactionType"].ToString().Trim();
                var contributionType = row["ContributionType"].ToString().Trim();

                decimal socialTax = 0;
                decimal insurance = 0;
                decimal adminFee = 0;
                decimal brokerFee = 0;
                decimal investable = 0;

                if (transactionType == "TAX")
                    socialTax = amount;

                if (transactionType == "FEE")
                {
                    if (adminCodes.Contains(contributionType))
                        adminFee = amount;

                    if (brokerCodes.Contains(contributionType))
                        brokerFee = amount;
                }

                if (insuranceCodes.Find(i => i.Code == contributionType) != null)
                    insurance = amount;

                var x = contributionType.Substring(1, contributionType.Length - 1); //remove the leading A, E, R to match contribution type to product code

                if (products.Find(i => i.Code == x) != null)
                    investable = amount;

                var contribution = new Contribution(
                    Convert.ToInt32(row["Member"]),
                    dateDue,
                    dateReceived,
                    transactionType,
                    contributionType,
                    amount,
                    expenses,
                    socialTax,
                    insurance,
                    adminFee,
                    brokerFee,
                    investable
                );

                contributions.Add(contribution);
            }
        }

        return contributions;
    }
}

An answer given to the previous version of my question advised that the GetContributions method should not live within the above class.

However my original question was how best to include another method that returned a total of contributions per type. I currently have this second method in the below class but I don't believe this is the best approach as I would end up with a new class for each method that returned a different data structure.

public class ContributionTotal
{
    public string ContributionType { get; }
    public string ContributionDescription { get; }
    public decimal Total { get; }
    public DateTime LastContributionDate { get; }
    public decimal LastContributionAmount { get; }

    public ContributionTotal(string contributionType, string contributionDescription, decimal total, DateTime lastContributionDate, decimal lastContributionAmount)
    {
        ContributionType = contributionType;
        ContributionDescription = contributionDescription;
        Total = total;
        LastContributionDate = lastContributionDate;
        LastContributionAmount = lastContributionAmount;
    }

    public static List<ContributionTotal> GetContributionTotals(Member member)
    {
        var contributionTotals = new List<ContributionTotal>();

        using (var connection = new SqlConnection(Common.GetConnectionString()))
        {
            var cmd = new SqlCommand("GetMemberTotalContributions", connection) {CommandType = CommandType.StoredProcedure};
            cmd.Parameters.AddWithValue("@MemberNumber", member.MemberNumber);
            cmd.Parameters.AddWithValue("@SchemeCode", member.SchemeCode);

            connection.Open();

            var ds = new DataSet();
            var da = new SqlDataAdapter(cmd);
            da.Fill(ds);

            if (ds.Tables[0].Rows.Count == 0)
                return contributionTotals;

            //calculate latest contribution date for each contribution type
            var avcLastContributionDate = DateTime.MinValue;
            var employeeLastContributionDate = DateTime.MinValue;
            var employerLastContributionDate = DateTime.MinValue;

            foreach (DataRow row in ds.Tables[0].Rows)
            {
                var contributionType = row["pmc_cont"].ToString().Substring(0, 1); //Should return either A, E or R for AVC, Employee or Employer

                if (!DateTime.TryParse(row["LastDate"].ToString().Trim(), out DateTime lastDate)) continue;

                if (lastDate > avcLastContributionDate && contributionType == "A")
                    avcLastContributionDate = lastDate;

                if (lastDate > employeeLastContributionDate && contributionType == "E")
                    employeeLastContributionDate = lastDate;

                if (lastDate > employerLastContributionDate && contributionType == "R")
                    employerLastContributionDate = lastDate;
            }

            decimal avcLastContributionAmount = 0;
            decimal avcTotal = 0;
            decimal employeeLastContributionAmount = 0;
            decimal employeeTotal = 0;
            decimal employerLastContributionAmount = 0;
            decimal employerTotal = 0;

            foreach (DataRow row in ds.Tables[0].Rows)
            {
                var contributionType = row["ContributionType"].ToString().Substring(0, 1); //Should return either A, E or R for AVC, Employee or Employer

                if (!DateTime.TryParse(row["LastDate"].ToString().Trim(), out DateTime lastDate)) continue;
                if (!decimal.TryParse(row["LastAmount"].ToString().Trim(), out decimal lastAmount)) continue;
                if (!decimal.TryParse(row["Total"].ToString().Trim(), out decimal total)) continue;

                if (avcLastContributionDate == lastDate && contributionType == "A")
                {
                    avcLastContributionAmount += lastAmount;
                    avcTotal += total;
                }

                if (employeeLastContributionDate == lastDate && contributionType == "E")
                {
                    employeeLastContributionAmount += lastAmount;
                    employeeTotal += total;
                }

                if (employerLastContributionDate == lastDate && contributionType == "R")
                {
                    employerLastContributionAmount += lastAmount;
                    employerTotal += total;
                }
            }

            if(avcTotal > 0)
                contributionTotals.Add(new ContributionTotal("A", "Employee Voluntary", avcTotal, avcLastContributionDate, avcLastContributionAmount));

            if (employeeTotal > 0)
                contributionTotals.Add(new ContributionTotal("E", "Employee Regular", employeeTotal, employeeLastContributionDate, employeeLastContributionAmount));

            if (employerTotal > 0)
                contributionTotals.Add(new ContributionTotal("R", "Employer Regular", employerTotal, employerLastContributionDate, employerLastContributionAmount));

            return contributionTotals;
        }
    }
}

The methods mentioned above are called from the code behind of my ASP.NET web application as shown below

    private void GetContributionHistory()
    {
        var member = (Member)Session["Member"];

        var contributions = Contribution.GetContributions(member, ContributionHistoryStartDate, ContributionHistoryEndDate);                      

        rptContributionHistory.DataSource = contributions;
        rptContributionHistory.DataBind();
    }

    private void GetTotalContributions()
    {
        var member = (Member)Session["Member"];

        var totalContributions = ContributionTotal.GetContributionTotals(member);

        rptContributionTotals.DataSource = totalContributions;
        rptContributionTotals.DataBind();

        var total = totalContributions.Sum(totalContribution => totalContribution.Total);

        ltTotalContributions.Text = Common.FormatCurrency(total);
        ltContributionsSinceJoining.Text = Common.FormatCurrency(total);

    }
\$\endgroup\$
5
  • \$\begingroup\$ Why is it static? Why is it not just using the AccountNumber. Stub code is usually not accepted here. \$\endgroup\$
    – paparazzo
    Commented Aug 2, 2017 at 15:15
  • \$\begingroup\$ @Paparazzi - It has to be static in order to be called from the code behind page of my web application. Can you clarify what you mean when you ask why is it not just using the AccountNumber? Excuse my ignorance... \$\endgroup\$ Commented Aug 2, 2017 at 15:35
  • \$\begingroup\$ @Paparazzi, I think that's a variation of static factory method (anti-)pattern: stackoverflow.com/a/929273/1386995 . A class that implements it normally does not have a public constructor and does not return collections though. \$\endgroup\$
    – Nikita B
    Commented Aug 2, 2017 at 15:46
  • \$\begingroup\$ Last try. Why are you passing accountNumber to GetTransactions? \$\endgroup\$
    – paparazzo
    Commented Aug 2, 2017 at 15:47
  • \$\begingroup\$ It would be passed to the database via a stored procedure to return the data which would be used to populate my list of transactions. \$\endgroup\$ Commented Aug 2, 2017 at 15:57

1 Answer 1

4
\$\begingroup\$

Well, in my opinion transaction should definitely NOT be responsible for getting a list of itself... Not only it makes little sense from domain perspective but it also violates SRP.

Another major concern is that this design clearly encourages having classes with lots of hidden static dependencies. This problem alone is good enough reason to avoid it.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the feedback. So would the list function move into its own class such as TransactionList? \$\endgroup\$ Commented Aug 2, 2017 at 15:24
  • \$\begingroup\$ @BugLover it would move to service/repository that is responsible for managing transactions. See: msdn.microsoft.com/en-us/library/ff649690.aspx \$\endgroup\$
    – Nikita B
    Commented Aug 2, 2017 at 15:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.