patterncsharpModerate
Finding the lowest and highest values in an array... with "Poor Man's DI"
Viewed 0 times
thevaluesarraylowestwithfindingpoorhighestandman
Problem
I am practicing my object oriented knowledge, so I created a very simple program that determines which is the lowest and highest number inside array.
This is my main method:
The
...and its implementations:
The
```
class Math
{
private int[] arr;
private readonly IMathPerformer imathPerformer;
public Math(IMathPerformer imathPerformer, int[]arr)
{
this.imathPerformer = imathPerformer;
This is my main method:
class Program
{
static void Main()
{
Console.Write("How many numbers would you like to enter?: ");
int num = int.Parse(Console.ReadLine());
int[] arr = new int[num];
for (int x = 0; x < num; x++ )
{
Console.Write("Enter number {0}: ", (x+1));
arr[x] = int.Parse(Console.ReadLine());
}
Console.WriteLine();
Console.Write("Lowest (1) \nHighest (2) \nEnter choice: ");
int choice = int.Parse(Console.ReadLine());
Math math;
if (choice == 1){
math = new Math(new HighestNumber(), arr);
Console.WriteLine("Answer: " + math.returnAnswer());
}
else if (choice == 2){
math = new Math(new LowestNumber(), arr);
Console.WriteLine("Answer: " + math.returnAnswer());
}
Console.ReadKey();
}
}The
IMathPerformer interface:interface IMathPerformer
{
int getAnswer(int[]arr);
}...and its implementations:
class HighestNumber : IMathPerformer
{
public int getAnswer(int[]arr)
{
int highNum = 0;
for(int x = 0; x arr[x+1])
{
highNum = (x + 1);
}
}
return arr[highNum];
}
}
class LowestNumber : IMathPerformer
{
public int getAnswer(int[]arr)
{
int lowNum = 0;
for (int x = 0; x < arr.Length - 1; x++)
{
if (arr[lowNum] < arr[x + 1])
{
lowNum = (x + 1);
}
}
return arr[lowNum];
}The
Math class:```
class Math
{
private int[] arr;
private readonly IMathPerformer imathPerformer;
public Math(IMathPerformer imathPerformer, int[]arr)
{
this.imathPerformer = imathPerformer;
Solution
First of all, kudos for exercising OOP and DI! Coding against interfaces is going to make your life easier in so many wonderful ways!
I agree, no problem is too simple to solve with SOLID principles, especially for learning purposes. Shifting from procedural to object-oriented code takes practice.
The first key to proper OOP is abstraction.
These are the abstractions I see in your code:
That's unfortunately all. Unfortunate, because I see these requirements in your code:
And from these requirements, I see these responsibilities:
If a class should be responsible for one thing, then there should be at least 5-6 types in this program.
Back to your code: the
Abstraction levels are crucial. The entry point of your program (the
We create an
This method has a very special role: it's the only place in the application we're allowed to
Just by looking at this code, you can get a grasp of how the entire application is structured, because every type has its dependencies injected through the constructor.
You know that the
You know that the user input will be validated somehow.
Because that type takes a
We know exactly which commands will be available for the
The method then injects the collector and the selector dependencies into a new
The
You started implementing DI, but stopped somewhere along the way. Half-implemented DI unfortunately doesn't get half the benefits of DI: the only testable code is the code that you've extracted into an interface... which makes the
I agree, no problem is too simple to solve with SOLID principles, especially for learning purposes. Shifting from procedural to object-oriented code takes practice.
The first key to proper OOP is abstraction.
These are the abstractions I see in your code:
IMathPerformer, an interface that gets some answer given anint[]. Implementations are responsible for computing theMinorMaxvalue, or any other operation that returns anintgiven anint[].
That's unfortunately all. Unfortunate, because I see these requirements in your code:
- Collecting the data (the
int[]to work with)
- Determining what to do with the data
- Outputting the result
And from these requirements, I see these responsibilities:
- Interacting with the console
- Validating user input
- Collecting the data
- Knowing what to do with the data
- Creating the menu with each available command
- Performing the appropriate command
- Outputting the result
- Coordinating the sequence of operations
If a class should be responsible for one thing, then there should be at least 5-6 types in this program.
Back to your code: the
Math class doesn't seem to really "fit in" anywhere in the above list - or rather, it's redundant: the command to execute is already known when the class gets created, and it acts as a middle man that doesn't really do anything that the IMathPerformer can't do just as well without it; it's forcing dependency injection, but there's nothing that justifies it because the abstraction level in the calling code is too low.Abstraction levels are crucial. The entry point of your program (the
Main method) should have a very high abstraction level, and is usually pretty short. For example:public static void Main(string[] args)
{
var app = CreateApp(args);
app.Run();
}We create an
app object and call its Run method. The CreateApp method might look like this:private static ArrayOpsApp CreateApp(string[] args)
{
var validator = new UserInputValidator();
var console = new ConsoleUserInterface(validator);
var argsParser = new CommandLineArgsParser();
var collector = new IntConsoleCollector(console, argsParser, args);
var commands = new[] {
new MinValueFinder(),
new MaxValueFinder(),
new AvgCalculator(),
new StdDevCalculator(),
//...
};
var commandSelector = new ConsoleCommandSelector(console, commands);
var app = new ArrayOpsApp(console, collector, commandSelector);
return app;
}This method has a very special role: it's the only place in the application we're allowed to
new up one of our concrete types - everywhere else, we are coding against interfaces. It's the application's composition root, where we compose the application's dependency graph.Just by looking at this code, you can get a grasp of how the entire application is structured, because every type has its dependencies injected through the constructor.
You know that the
IntConsoleCollector is the specific implementation for what the rest of the code sees as some abstract IDataCollector, or even a generic IDataCollector.You know that the user input will be validated somehow.
Because that type takes a
ConsoleUserInterface implementation for what might be its IUserInterface dependency, we know that the type needs an object that's responsible for dealing with UI concerns, and because it takes an IArgsParser, we can infer that we can specify the input with command-line arguments and that the collector might just spit them back and skip prompting the user for them. Perhaps it's also used for parsing the user's input if the args array is empty or doesn't contain anything useful - but that's an implementation detail.We know exactly which commands will be available for the
commandSelector, and again we know that the selector will work with the console, because we're giving it console for its IUserInterface dependency; we can infer that the selector will be iterating the available commands to create a menu interface using the console.The method then injects the collector and the selector dependencies into a new
ArrayOpsApp, which will obviously be responsible for consuming these dependencies and coordinating the sequence of operations.The
Main method in your code has both hands into that sequence of operations; it's responsible for collecting the data, dealing with the user's input, determining which concrete types to use, ... it does pretty much everything except actually computing the min/max values.You started implementing DI, but stopped somewhere along the way. Half-implemented DI unfortunately doesn't get half the benefits of DI: the only testable code is the code that you've extracted into an interface... which makes the
IMathPerformer interface pretty much useleCode Snippets
public static void Main(string[] args)
{
var app = CreateApp(args);
app.Run();
}private static ArrayOpsApp CreateApp(string[] args)
{
var validator = new UserInputValidator();
var console = new ConsoleUserInterface(validator);
var argsParser = new CommandLineArgsParser();
var collector = new IntConsoleCollector(console, argsParser, args);
var commands = new[] {
new MinValueFinder(),
new MaxValueFinder(),
new AvgCalculator(),
new StdDevCalculator(),
//...
};
var commandSelector = new ConsoleCommandSelector(console, commands);
var app = new ArrayOpsApp(console, collector, commandSelector);
return app;
}if (choice == 1){
math = new Math(new HighestNumber(), arr);
Console.WriteLine("Answer: " + math.returnAnswer());
}
else if (choice == 2){
math = new Math(new LowestNumber(), arr);
Console.WriteLine("Answer: " + math.returnAnswer());
}if (choice == 1){
math = new Math(new HighestNumber(), arr);
}
else if (choice == 2){
math = new Math(new LowestNumber(), arr);
}
Console.WriteLine("Answer: " + math.returnAnswer());public class ArrayOpsApp
{
private readonly IUserInterface _ui;
private readonly IDataCollector _collector;
private reaodnly ICommandSelector _selector;
public ArrayOpsApp(IUserInterface ui, IDataCollector collector, ICommandSelector selector)
{
_ui = ui;
_collector = collector;
_selector = selector;
}
public void Run()
{
var data = _collector.CollectData();
var command = _selector.SelectCommand();
var result = command.Execute(data);
_ui.OutputResult(result);
}
}Context
StackExchange Code Review Q#154317, answer score: 12
Revisions (0)
No revisions yet.