7
\$\begingroup\$

I'm looking for a better way to create objects based off of a value of a string.

Consider the following:

Input file:

//each Vehicle type has a list of different options
//whose length varies, I provided a simple case
van*2006*dodge*grand caravan*green~
car*2010*toyota*corolla*red~
truck*2005*toyota*tundra*black~

Classes:

abstract class Vehicle: IVehicle{
    public int Year;
    public string Make;
    public string Model;

    ...
}

class Van : Vehicle
{
    ...
}

class Car : Vehicle
{
    ...
}

class Truck : Vehicle
{
    ...
}

Program:

static void Main(string[] args)
{
    char delimiter = '*';
    string fileToRead = "inventory.csv";
    string currentLine = string.Empty;
    List<IVehicle> inventory = new List<IVehicle>();

    //Open file
    using (StreamReader reader = new StreamReader(fileToRead))
    {
        //while you read each line
        while ((currentLine = reader.ReadLine()) != null)
        {
            IVehicle temp;

            //Tokenize the line 
            string[] parts = currentLine.Split(delimiter);

            switch(parts[0])
            {
                case "van":
                    temp = new Van();
                    ...
                break;
                case "car"
                    temp = new Car();
                    ...
                break;
                case "truck"
                    temp = new Truck();
                    ...
                break;
            }

            inventory.add(temp);
        }
    }

    ...
}

My problem with the code is the switch statement. It feels messy and I'm looking for a better way.

\$\endgroup\$
  • \$\begingroup\$ What's a Vechicle? \$\endgroup\$ – ChaosPandion Aug 17 '11 at 16:09
  • \$\begingroup\$ have you considered Enum.Parse? \$\endgroup\$ – Rakka Rage Apr 21 '14 at 3:07
11
\$\begingroup\$

In .Net laguages you can easily instatiate any object given only the full name of the class (=namespace+class name).

Here is a runnable example:

using System;
using System.Reflection;

namespace Test
{

    class MsgPrinter
    {
        public MsgPrinter()
        {
            System.Console.WriteLine("Hello!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            MsgPrinter printer = assembly.CreateInstance("Test.MsgPrinter") as MsgPrinter;
            Console.Read();
        }
    }
}

So in your case it would break down to something like (of course, the names in your parameter file would need to exactly match the class names in your application):

...
string[] parts = currentLine.Split(delimiter);
Vehicle vehicle = Assembly.GetExecutingAssembly().CreateInstance(namespace_name+"."+parts[0]) as Vehicle;
...
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ (First-time visitor to this site) I used this as an opportunity to learn about a feature - reflection - that I'd had absolutely no experience with. I don't know what the protocol here is, so rather than add my code to your answer I put it on steven.vorefamily.net/using-reflection-to-read-a-text-file - if I should add it to @ultimA's answer instead, or if this is otherwise inappropriate, please let me know. \$\endgroup\$ – StevenV Aug 19 '11 at 12:52
6
\$\begingroup\$

Here are a few tips:

  1. I would use regular expression (and specifically regular expression groups) to parse strings.
  2. Instead of a huge switch/case statement it is better to use a small IDictionary which contains all possible mappings from car type to constructor:

        IDictionary<string, Func<IVehicle>> carBuilder = new Dictionary<string, Func<IVehicle>>();
        carBuilder.Add("van", () => { return new Van(); });
        carBuilder.Add("car", () => { return new Car(); });
        carBuilder.Add("truck", () => { return new Truck(); });
    

And you can always get an instance of a Vehicle type using:

IVehicle vehicle = carBuilder["van"]();

It is always better to clearly separate concerns:

  • Introduce a DTO class called VehicleInfo which will contain all columns of a row in your csv file.

  • Introduce a class for parsing csv file named VehicleInfoProvider which has a method:

    public IList GetVehicleInfos(string filePath)

It will parse the file using the regular expression and fill all fields of a VehicleInfo class

  • Another class that can build specific vehicles based on a VehicleInfo. Lets name it VehicleBuilder. It has a method:

    public IVehicle CreateVehicle(VehicleInfo info)

This class would contain the dictionary and will be able to create all possible types of vehicle.

  • Using a foreach statement you can parse file rows and create all instances:

        IList<VehicleInfo> vehicleInfos = vehicleProvider.GetVehicleInfos(path);
    
        foreach (var vehicleInfo in vehicleInfos)
        {
            IVehicle vehicle = vehicleBuilder.CreateVehicle(vehicleInfo.VehicleType);
            inventory.Add(vehicle);
        }
    
| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

You could use the abstract factory pattern and pass the vehicle type to the create method of the factory. That would definitely be cleaner and allow for easier exstensibility when someone wants to create a new type of vehicle (crossover, suv, etc).

An example can be found here.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Scott explicitly asked to avoid the switch statement, but the factory pattern still requires it. \$\endgroup\$ – ultimA Aug 17 '11 at 22:01
  • 2
    \$\begingroup\$ @ultimA: Who says that the factory pattern implementation can't accept the raw string as input and use reflection, as you do in your answer? AFAIK the factory pattern does not require a switch statement. \$\endgroup\$ – rjnilsson Aug 18 '11 at 7:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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