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\$
2
  • \$\begingroup\$ What's a Vechicle? \$\endgroup\$ Commented Aug 17, 2011 at 16:09
  • \$\begingroup\$ have you considered Enum.Parse? \$\endgroup\$
    – Rakka Rage
    Commented Apr 21, 2014 at 3:07

3 Answers 3

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;
...
\$\endgroup\$
1
  • 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
    Commented Aug 19, 2011 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);
        }
    
\$\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.

\$\endgroup\$
2
  • \$\begingroup\$ Scott explicitly asked to avoid the switch statement, but the factory pattern still requires it. \$\endgroup\$
    – ultimA
    Commented Aug 17, 2011 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
    Commented Aug 18, 2011 at 7:09

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.