House plan design (Head First C#)
$begingroup$
I've been working through the book Head First C# and this is an exercise from chapter 7, which is about interfaces and abstract classes.
Note that the class design was mandated by the authors, I'm open to suggestions to improve.
I'd like a review on any and all aspects. My code ended up quite a bit different than the book's solution, because it's a few years old and I think "holds back" on newer C#/.NET features and syntax.
In a shared code base, I would include that XML documentation, but since this is just local code for learning, I didn't. Any and all feedback appreciated.
The authors' published implementation is here on GitHub. The idea is to implement a floor plan that can be explored, with room connections and doors to the outside. It's done through a WinForms application.
Here is a picture of the plan from the book. Sorry, couldn't find a screenshot.
The app looks like this:
Location.cs
using System;
namespace House
{
abstract class Location
{
public Location(string name) => Name = name;
public string Name { get; private set; }
public Location Exits;
public virtual string Description
{
get
{
string description = $"You're standing in the {Name}. You see exits to the following places: rn";
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
return description;
}
}
}
}
Room.cs
using System;
namespace House
{
class Room : Location
{
public Room(string name, string decoration)
: base(name)
{
Decoration = decoration;
}
private string Decoration { get; set; }
public override string Description => $"You see {Decoration}. {base.Description} ";
}
}
Outside.cs
using System;
namespace House
{
class Outside : Location
{
public Outside(string name, bool hot)
: base(name)
{
Hot = hot;
}
private bool Hot { get; }
override public string Description => Hot
? "It's very hot here. " + base.Description
: base.Description;
}
}
IHasInteriorDoor.cs
using System;
namespace House
{
interface IHasExteriorDoor
{
string DoorDescription { get; }
Location DoorLocation { get; }
}
}
OutsideWithDoor.cs
using System;
namespace House
{
class OutsideWithDoor : Outside, IHasExteriorDoor
{
public OutsideWithDoor(string name, bool hot, string doorDescription)
: base(name, hot)
{
DoorDescription = doorDescription;
}
public string DoorDescription { get; private set; }
public Location DoorLocation { get; set; }
public override string Description => $"{base.Description}rn You see {DoorDescription} to go inside.";
}
}
RoomWithDoor.cs
using System;
namespace House
{
class RoomWithDoor : Room, IHasExteriorDoor
{
public RoomWithDoor(string name, string decoration, string doorDescription)
: base(name, decoration)
{
DoorDescription = doorDescription;
}
public string DoorDescription { get; private set; }
public Location DoorLocation { get; set; }
}
}
And here is the WinForms to make it work. Leaving out IDE generated code.
ExploreTheHouseForm.cs
using System;
using System.Windows.Forms;
namespace House
{
public partial class ExploreTheHouseForm : Form
{
Location currentLocation;
RoomWithDoor livingRoom;
RoomWithDoor kitchen;
Room diningRoom;
OutsideWithDoor frontYard;
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects();
MoveToLocation(livingRoom);
}
private void CreateObjects()
{
// Configure the locations
livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
diningRoom = new Room("dining room", "a crystal chandelier");
frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new Location { diningRoom };
kitchen.Exits = new Location { diningRoom };
diningRoom.Exits = new Location { livingRoom, kitchen };
frontYard.Exits = new Location { backYard, garden };
backYard.Exits = new Location { frontYard, garden };
garden.Exits = new Location { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
}
private void MoveToLocation(Location location)
{
currentLocation = location;
ExitsComboBox.Items.Clear();
foreach (Location exit in location.Exits)
{
ExitsComboBox.Items.Add(exit.Name);
}
ExitsComboBox.SelectedIndex = 0;
DescriptionTextBox.Text = currentLocation.Description;
ShowGoThroughExteriorDoorButton(currentLocation);
}
private void ShowGoThroughExteriorDoorButton(Location location)
{
if (location is IHasExteriorDoor)
{
GoThroughExteriorDoorButton.Visible = true;
return;
}
GoThroughExteriorDoorButton.Visible = false;
}
private void GoHereButton_Click(object sender, EventArgs e)
{
MoveToLocation(currentLocation.Exits[ExitsComboBox.SelectedIndex]);
}
private void GoThroughExteriorDoorButton_Click(object sender, EventArgs e)
{
IHasExteriorDoor locationWithExteriorDoor = currentLocation as IHasExteriorDoor;
MoveToLocation(locationWithExteriorDoor.DoorLocation);
}
}
}
c# winforms interface xaml
$endgroup$
add a comment |
$begingroup$
I've been working through the book Head First C# and this is an exercise from chapter 7, which is about interfaces and abstract classes.
Note that the class design was mandated by the authors, I'm open to suggestions to improve.
I'd like a review on any and all aspects. My code ended up quite a bit different than the book's solution, because it's a few years old and I think "holds back" on newer C#/.NET features and syntax.
In a shared code base, I would include that XML documentation, but since this is just local code for learning, I didn't. Any and all feedback appreciated.
The authors' published implementation is here on GitHub. The idea is to implement a floor plan that can be explored, with room connections and doors to the outside. It's done through a WinForms application.
Here is a picture of the plan from the book. Sorry, couldn't find a screenshot.
The app looks like this:
Location.cs
using System;
namespace House
{
abstract class Location
{
public Location(string name) => Name = name;
public string Name { get; private set; }
public Location Exits;
public virtual string Description
{
get
{
string description = $"You're standing in the {Name}. You see exits to the following places: rn";
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
return description;
}
}
}
}
Room.cs
using System;
namespace House
{
class Room : Location
{
public Room(string name, string decoration)
: base(name)
{
Decoration = decoration;
}
private string Decoration { get; set; }
public override string Description => $"You see {Decoration}. {base.Description} ";
}
}
Outside.cs
using System;
namespace House
{
class Outside : Location
{
public Outside(string name, bool hot)
: base(name)
{
Hot = hot;
}
private bool Hot { get; }
override public string Description => Hot
? "It's very hot here. " + base.Description
: base.Description;
}
}
IHasInteriorDoor.cs
using System;
namespace House
{
interface IHasExteriorDoor
{
string DoorDescription { get; }
Location DoorLocation { get; }
}
}
OutsideWithDoor.cs
using System;
namespace House
{
class OutsideWithDoor : Outside, IHasExteriorDoor
{
public OutsideWithDoor(string name, bool hot, string doorDescription)
: base(name, hot)
{
DoorDescription = doorDescription;
}
public string DoorDescription { get; private set; }
public Location DoorLocation { get; set; }
public override string Description => $"{base.Description}rn You see {DoorDescription} to go inside.";
}
}
RoomWithDoor.cs
using System;
namespace House
{
class RoomWithDoor : Room, IHasExteriorDoor
{
public RoomWithDoor(string name, string decoration, string doorDescription)
: base(name, decoration)
{
DoorDescription = doorDescription;
}
public string DoorDescription { get; private set; }
public Location DoorLocation { get; set; }
}
}
And here is the WinForms to make it work. Leaving out IDE generated code.
ExploreTheHouseForm.cs
using System;
using System.Windows.Forms;
namespace House
{
public partial class ExploreTheHouseForm : Form
{
Location currentLocation;
RoomWithDoor livingRoom;
RoomWithDoor kitchen;
Room diningRoom;
OutsideWithDoor frontYard;
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects();
MoveToLocation(livingRoom);
}
private void CreateObjects()
{
// Configure the locations
livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
diningRoom = new Room("dining room", "a crystal chandelier");
frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new Location { diningRoom };
kitchen.Exits = new Location { diningRoom };
diningRoom.Exits = new Location { livingRoom, kitchen };
frontYard.Exits = new Location { backYard, garden };
backYard.Exits = new Location { frontYard, garden };
garden.Exits = new Location { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
}
private void MoveToLocation(Location location)
{
currentLocation = location;
ExitsComboBox.Items.Clear();
foreach (Location exit in location.Exits)
{
ExitsComboBox.Items.Add(exit.Name);
}
ExitsComboBox.SelectedIndex = 0;
DescriptionTextBox.Text = currentLocation.Description;
ShowGoThroughExteriorDoorButton(currentLocation);
}
private void ShowGoThroughExteriorDoorButton(Location location)
{
if (location is IHasExteriorDoor)
{
GoThroughExteriorDoorButton.Visible = true;
return;
}
GoThroughExteriorDoorButton.Visible = false;
}
private void GoHereButton_Click(object sender, EventArgs e)
{
MoveToLocation(currentLocation.Exits[ExitsComboBox.SelectedIndex]);
}
private void GoThroughExteriorDoorButton_Click(object sender, EventArgs e)
{
IHasExteriorDoor locationWithExteriorDoor = currentLocation as IHasExteriorDoor;
MoveToLocation(locationWithExteriorDoor.DoorLocation);
}
}
}
c# winforms interface xaml
$endgroup$
$begingroup$
The winforms screen gives the option to go through the exterior door, but whatever is behind the exterior door doesn't get listed in the available places? What happens if the kitchen had multiple exterior doors to multiple places? What if the entire house was just a kitchen?
$endgroup$
– Mast
Jan 17 at 15:15
4
$begingroup$
If you are interested in the general problem of how to write text-based adventures, I encourage you to look intoInform7
, an exceedingly clever language for writing such adventures. If you're interested in learning about virtual machines for implementing such adventures, I did a series on my blog a few years ago about the Z-machine.
$endgroup$
– Eric Lippert
Jan 17 at 22:21
3
$begingroup$
As a bonus, on a Z-machine code for this problem will occupy less than a kilobyte. You won't be able to compile the above code into a .NET assembly that takes up that much. (This is another way of saying "in Ye Olde Days we didn't have no interfaces and abstract classes". However, we were also much more likely to be eaten by a grue, so it all evens out in the end.)
$endgroup$
– Jeroen Mostert
Jan 18 at 13:36
add a comment |
$begingroup$
I've been working through the book Head First C# and this is an exercise from chapter 7, which is about interfaces and abstract classes.
Note that the class design was mandated by the authors, I'm open to suggestions to improve.
I'd like a review on any and all aspects. My code ended up quite a bit different than the book's solution, because it's a few years old and I think "holds back" on newer C#/.NET features and syntax.
In a shared code base, I would include that XML documentation, but since this is just local code for learning, I didn't. Any and all feedback appreciated.
The authors' published implementation is here on GitHub. The idea is to implement a floor plan that can be explored, with room connections and doors to the outside. It's done through a WinForms application.
Here is a picture of the plan from the book. Sorry, couldn't find a screenshot.
The app looks like this:
Location.cs
using System;
namespace House
{
abstract class Location
{
public Location(string name) => Name = name;
public string Name { get; private set; }
public Location Exits;
public virtual string Description
{
get
{
string description = $"You're standing in the {Name}. You see exits to the following places: rn";
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
return description;
}
}
}
}
Room.cs
using System;
namespace House
{
class Room : Location
{
public Room(string name, string decoration)
: base(name)
{
Decoration = decoration;
}
private string Decoration { get; set; }
public override string Description => $"You see {Decoration}. {base.Description} ";
}
}
Outside.cs
using System;
namespace House
{
class Outside : Location
{
public Outside(string name, bool hot)
: base(name)
{
Hot = hot;
}
private bool Hot { get; }
override public string Description => Hot
? "It's very hot here. " + base.Description
: base.Description;
}
}
IHasInteriorDoor.cs
using System;
namespace House
{
interface IHasExteriorDoor
{
string DoorDescription { get; }
Location DoorLocation { get; }
}
}
OutsideWithDoor.cs
using System;
namespace House
{
class OutsideWithDoor : Outside, IHasExteriorDoor
{
public OutsideWithDoor(string name, bool hot, string doorDescription)
: base(name, hot)
{
DoorDescription = doorDescription;
}
public string DoorDescription { get; private set; }
public Location DoorLocation { get; set; }
public override string Description => $"{base.Description}rn You see {DoorDescription} to go inside.";
}
}
RoomWithDoor.cs
using System;
namespace House
{
class RoomWithDoor : Room, IHasExteriorDoor
{
public RoomWithDoor(string name, string decoration, string doorDescription)
: base(name, decoration)
{
DoorDescription = doorDescription;
}
public string DoorDescription { get; private set; }
public Location DoorLocation { get; set; }
}
}
And here is the WinForms to make it work. Leaving out IDE generated code.
ExploreTheHouseForm.cs
using System;
using System.Windows.Forms;
namespace House
{
public partial class ExploreTheHouseForm : Form
{
Location currentLocation;
RoomWithDoor livingRoom;
RoomWithDoor kitchen;
Room diningRoom;
OutsideWithDoor frontYard;
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects();
MoveToLocation(livingRoom);
}
private void CreateObjects()
{
// Configure the locations
livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
diningRoom = new Room("dining room", "a crystal chandelier");
frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new Location { diningRoom };
kitchen.Exits = new Location { diningRoom };
diningRoom.Exits = new Location { livingRoom, kitchen };
frontYard.Exits = new Location { backYard, garden };
backYard.Exits = new Location { frontYard, garden };
garden.Exits = new Location { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
}
private void MoveToLocation(Location location)
{
currentLocation = location;
ExitsComboBox.Items.Clear();
foreach (Location exit in location.Exits)
{
ExitsComboBox.Items.Add(exit.Name);
}
ExitsComboBox.SelectedIndex = 0;
DescriptionTextBox.Text = currentLocation.Description;
ShowGoThroughExteriorDoorButton(currentLocation);
}
private void ShowGoThroughExteriorDoorButton(Location location)
{
if (location is IHasExteriorDoor)
{
GoThroughExteriorDoorButton.Visible = true;
return;
}
GoThroughExteriorDoorButton.Visible = false;
}
private void GoHereButton_Click(object sender, EventArgs e)
{
MoveToLocation(currentLocation.Exits[ExitsComboBox.SelectedIndex]);
}
private void GoThroughExteriorDoorButton_Click(object sender, EventArgs e)
{
IHasExteriorDoor locationWithExteriorDoor = currentLocation as IHasExteriorDoor;
MoveToLocation(locationWithExteriorDoor.DoorLocation);
}
}
}
c# winforms interface xaml
$endgroup$
I've been working through the book Head First C# and this is an exercise from chapter 7, which is about interfaces and abstract classes.
Note that the class design was mandated by the authors, I'm open to suggestions to improve.
I'd like a review on any and all aspects. My code ended up quite a bit different than the book's solution, because it's a few years old and I think "holds back" on newer C#/.NET features and syntax.
In a shared code base, I would include that XML documentation, but since this is just local code for learning, I didn't. Any and all feedback appreciated.
The authors' published implementation is here on GitHub. The idea is to implement a floor plan that can be explored, with room connections and doors to the outside. It's done through a WinForms application.
Here is a picture of the plan from the book. Sorry, couldn't find a screenshot.
The app looks like this:
Location.cs
using System;
namespace House
{
abstract class Location
{
public Location(string name) => Name = name;
public string Name { get; private set; }
public Location Exits;
public virtual string Description
{
get
{
string description = $"You're standing in the {Name}. You see exits to the following places: rn";
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
return description;
}
}
}
}
Room.cs
using System;
namespace House
{
class Room : Location
{
public Room(string name, string decoration)
: base(name)
{
Decoration = decoration;
}
private string Decoration { get; set; }
public override string Description => $"You see {Decoration}. {base.Description} ";
}
}
Outside.cs
using System;
namespace House
{
class Outside : Location
{
public Outside(string name, bool hot)
: base(name)
{
Hot = hot;
}
private bool Hot { get; }
override public string Description => Hot
? "It's very hot here. " + base.Description
: base.Description;
}
}
IHasInteriorDoor.cs
using System;
namespace House
{
interface IHasExteriorDoor
{
string DoorDescription { get; }
Location DoorLocation { get; }
}
}
OutsideWithDoor.cs
using System;
namespace House
{
class OutsideWithDoor : Outside, IHasExteriorDoor
{
public OutsideWithDoor(string name, bool hot, string doorDescription)
: base(name, hot)
{
DoorDescription = doorDescription;
}
public string DoorDescription { get; private set; }
public Location DoorLocation { get; set; }
public override string Description => $"{base.Description}rn You see {DoorDescription} to go inside.";
}
}
RoomWithDoor.cs
using System;
namespace House
{
class RoomWithDoor : Room, IHasExteriorDoor
{
public RoomWithDoor(string name, string decoration, string doorDescription)
: base(name, decoration)
{
DoorDescription = doorDescription;
}
public string DoorDescription { get; private set; }
public Location DoorLocation { get; set; }
}
}
And here is the WinForms to make it work. Leaving out IDE generated code.
ExploreTheHouseForm.cs
using System;
using System.Windows.Forms;
namespace House
{
public partial class ExploreTheHouseForm : Form
{
Location currentLocation;
RoomWithDoor livingRoom;
RoomWithDoor kitchen;
Room diningRoom;
OutsideWithDoor frontYard;
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects();
MoveToLocation(livingRoom);
}
private void CreateObjects()
{
// Configure the locations
livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
diningRoom = new Room("dining room", "a crystal chandelier");
frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new Location { diningRoom };
kitchen.Exits = new Location { diningRoom };
diningRoom.Exits = new Location { livingRoom, kitchen };
frontYard.Exits = new Location { backYard, garden };
backYard.Exits = new Location { frontYard, garden };
garden.Exits = new Location { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
}
private void MoveToLocation(Location location)
{
currentLocation = location;
ExitsComboBox.Items.Clear();
foreach (Location exit in location.Exits)
{
ExitsComboBox.Items.Add(exit.Name);
}
ExitsComboBox.SelectedIndex = 0;
DescriptionTextBox.Text = currentLocation.Description;
ShowGoThroughExteriorDoorButton(currentLocation);
}
private void ShowGoThroughExteriorDoorButton(Location location)
{
if (location is IHasExteriorDoor)
{
GoThroughExteriorDoorButton.Visible = true;
return;
}
GoThroughExteriorDoorButton.Visible = false;
}
private void GoHereButton_Click(object sender, EventArgs e)
{
MoveToLocation(currentLocation.Exits[ExitsComboBox.SelectedIndex]);
}
private void GoThroughExteriorDoorButton_Click(object sender, EventArgs e)
{
IHasExteriorDoor locationWithExteriorDoor = currentLocation as IHasExteriorDoor;
MoveToLocation(locationWithExteriorDoor.DoorLocation);
}
}
}
c# winforms interface xaml
c# winforms interface xaml
edited Jan 17 at 7:21
Phrancis
asked Jan 17 at 6:34
PhrancisPhrancis
14.8k648141
14.8k648141
$begingroup$
The winforms screen gives the option to go through the exterior door, but whatever is behind the exterior door doesn't get listed in the available places? What happens if the kitchen had multiple exterior doors to multiple places? What if the entire house was just a kitchen?
$endgroup$
– Mast
Jan 17 at 15:15
4
$begingroup$
If you are interested in the general problem of how to write text-based adventures, I encourage you to look intoInform7
, an exceedingly clever language for writing such adventures. If you're interested in learning about virtual machines for implementing such adventures, I did a series on my blog a few years ago about the Z-machine.
$endgroup$
– Eric Lippert
Jan 17 at 22:21
3
$begingroup$
As a bonus, on a Z-machine code for this problem will occupy less than a kilobyte. You won't be able to compile the above code into a .NET assembly that takes up that much. (This is another way of saying "in Ye Olde Days we didn't have no interfaces and abstract classes". However, we were also much more likely to be eaten by a grue, so it all evens out in the end.)
$endgroup$
– Jeroen Mostert
Jan 18 at 13:36
add a comment |
$begingroup$
The winforms screen gives the option to go through the exterior door, but whatever is behind the exterior door doesn't get listed in the available places? What happens if the kitchen had multiple exterior doors to multiple places? What if the entire house was just a kitchen?
$endgroup$
– Mast
Jan 17 at 15:15
4
$begingroup$
If you are interested in the general problem of how to write text-based adventures, I encourage you to look intoInform7
, an exceedingly clever language for writing such adventures. If you're interested in learning about virtual machines for implementing such adventures, I did a series on my blog a few years ago about the Z-machine.
$endgroup$
– Eric Lippert
Jan 17 at 22:21
3
$begingroup$
As a bonus, on a Z-machine code for this problem will occupy less than a kilobyte. You won't be able to compile the above code into a .NET assembly that takes up that much. (This is another way of saying "in Ye Olde Days we didn't have no interfaces and abstract classes". However, we were also much more likely to be eaten by a grue, so it all evens out in the end.)
$endgroup$
– Jeroen Mostert
Jan 18 at 13:36
$begingroup$
The winforms screen gives the option to go through the exterior door, but whatever is behind the exterior door doesn't get listed in the available places? What happens if the kitchen had multiple exterior doors to multiple places? What if the entire house was just a kitchen?
$endgroup$
– Mast
Jan 17 at 15:15
$begingroup$
The winforms screen gives the option to go through the exterior door, but whatever is behind the exterior door doesn't get listed in the available places? What happens if the kitchen had multiple exterior doors to multiple places? What if the entire house was just a kitchen?
$endgroup$
– Mast
Jan 17 at 15:15
4
4
$begingroup$
If you are interested in the general problem of how to write text-based adventures, I encourage you to look into
Inform7
, an exceedingly clever language for writing such adventures. If you're interested in learning about virtual machines for implementing such adventures, I did a series on my blog a few years ago about the Z-machine.$endgroup$
– Eric Lippert
Jan 17 at 22:21
$begingroup$
If you are interested in the general problem of how to write text-based adventures, I encourage you to look into
Inform7
, an exceedingly clever language for writing such adventures. If you're interested in learning about virtual machines for implementing such adventures, I did a series on my blog a few years ago about the Z-machine.$endgroup$
– Eric Lippert
Jan 17 at 22:21
3
3
$begingroup$
As a bonus, on a Z-machine code for this problem will occupy less than a kilobyte. You won't be able to compile the above code into a .NET assembly that takes up that much. (This is another way of saying "in Ye Olde Days we didn't have no interfaces and abstract classes". However, we were also much more likely to be eaten by a grue, so it all evens out in the end.)
$endgroup$
– Jeroen Mostert
Jan 18 at 13:36
$begingroup$
As a bonus, on a Z-machine code for this problem will occupy less than a kilobyte. You won't be able to compile the above code into a .NET assembly that takes up that much. (This is another way of saying "in Ye Olde Days we didn't have no interfaces and abstract classes". However, we were also much more likely to be eaten by a grue, so it all evens out in the end.)
$endgroup$
– Jeroen Mostert
Jan 18 at 13:36
add a comment |
5 Answers
5
active
oldest
votes
$begingroup$
RobH's review covers syntax and style well so I won't go into that. Instead I'd like to give my take on feedback given by Svek and BittermanAndy.
Separation Of Concerns
I think Svek's commentary about the CreateObjects
method is spot on, but I don't think it goes far enough. The need for such a method in the first place hints that the ExploreTheHouseForm
class is doing to much. With the current implementation, each of the rooms is a field on the form. This effectively makes the ExploreTheHouseForm
the house itself. As such it would be more aptly named ExplorableHouseForm
.
In general (and this becomes ever more important as you get into more complex projects) we want to separate the presentation of the data from the data itself.
The form is the UI it already has the duty to present the data to the user. It shouldn't also be the data. I would much rather the house be constructed elsewhere and passed to the form's constructor:
public ExploreTheHouseForm(Location initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
With this simple change, you can remove all the individual Location
fields from ExploreTheHouseForm
with the exception of currentLocation
. Also, if you desire, you can use the same form to explore any number of different houses without further modification.
Immutability
A fair amount of BittermanAndy's advice (as of the time of this writing, his post was updated at least once since I started) was to try to make your Location
class immutable. Since with the overall design as it is, locations need to reference each other, you run into a chicken & egg scenario preventing immutability where each Location
needs their neighbors to be created before them. I don't see a way around this, however if you have your locations implement an interface, and write your form to consume the interface rather than Location
you can get much of the same benefit of actual immutability.
public interface ILocation
{
public string Name { get; }
public IList<ILocation> Exits {get;}
public string Description { get;}
}
In ILocation
we only specify the get
portions of the properties. This means to consumers of ILocation
the properties are effectively read-only even if the implementing classes implement the set
. We also declare Exits
as a collection of ILocation
rather than Location
so that accessed members are also read-only to consumers.
You don't have to change much about Location
itself:
public abstract class Location: ILocation
{
...
//private field to back Exits property.
private IList<ILocation> _exits;
public IList<ILocation> Exits {
get
{
// AsReadOnly so that consumers are not allowed to modify contents.
// there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.
return _exits?.AsReadOnly();
}
set{ _exits = value;}
}
}
Updating ExploreTheHouseForm
is also straight forward, simply change the type of the field currentLocation
the Location
parameters of ExploreTheHouseForm
, MoveToLocation
, and ShowGoThroughExteriorDoorButton
to ILocation
:
...
private ILocation _currentLocation;
...
public ExploreTheHouseForm(ILocation initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
...
private void MoveToLocation(ILocation location)
...
private void ShowGoThroughExteriorDoorButton(ILocation location)
The overall impact of this is that the locations are mutable during construction (by some factory) but once construction is complete, all you work with the read-only ILocation
// GenerateHouse returns the entry point of the house.
public ILocation GenerateHouse()
{
// Configure the locations
var livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
var kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
var diningRoom = new Room("dining room", "a crystal chandelier");
var frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
var backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
var garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new List<ILocation>() { diningRoom };
kitchen.Exits = new List<ILocation>() { diningRoom };
diningRoom.Exits = new List<ILocation> { livingRoom, kitchen };
frontYard.Exits = new List<ILocation> { backYard, garden };
backYard.Exits = new List<ILocation> { frontYard, garden };
garden.Exits = new List<ILocation> { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
// return entry point.
return livingRoom;
}
Location Connectivity
I agree with the other reviews and comments that pulling the concept of the location connection into a separate class/set of classes would allow for a better design. Locations can have any number of exits, and it is a property of the exit, not the location if that exit is an open archway, a door, or just an abstract dividing line (outdoor location to outdoor location) Comintern does a good job of covering this in their review so I won't go into it further.
$endgroup$
1
$begingroup$
Excellent answer. Separating the creation of the house, and presenting the form only with the interface it needs for navigation, solves the creation / immutability problem very nicely. (My edits were fairly minor, by the way).
$endgroup$
– BittermanAndy
Jan 18 at 8:26
$begingroup$
"there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.". At the very least I don't see any reason to not simply change the return value toIReadOnlyList
which makes the intent much clearer. Admittedly the better fix for the consumer would be to use IImmutableList from the immutable collections package which really guarantees that the content of the list won't change.
$endgroup$
– Voo
Jan 19 at 13:25
add a comment |
$begingroup$
I have a general rule: if I see string concatenation with a loop I assume it's not the best way of doing it. Let's look at this:
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
I see this pattern a lot. What you need is Select
with string.Join
:
var exitList = string.Join(Environment.NewLine, Exits.Select(exit => $"— {exit.Name}"));
I've used Environment.NewLine
because I like using well-named constants. Random aside: r
is a Carriage Return and n
is a Line Feed. The terminology comes from physical printers. Another reason I prefer Environment.NewLine
is that it means you don't have to know and remember that.
Edit
A comment notes that Environment.NewLine
is different on Windows vs Linux/Unix. I think that's important to know and I really should have mentioned it the first time. As the comment exchange shows, it's really easy to get platforms and line-endings mixed up which I think illustrates the usefulness of the constant.
The Name
property of Location
is only set in the constructor. You can use a read-only auto property instead of a private set one:
public string Name { get; }
Outside.Description
could be using string interpolation like the other Description properties.
You're missing your access modifiers on your class declarations. MS guidelines state that they should always be specified.
I'm not aware of a convention for it but override public string
seems to be an odd order to me. I would expect the access modifier first: public override string
. The main thing is to keep that consistent though.
$endgroup$
2
$begingroup$
Environment.NewLine
is better for working with Windows or Linux because it contains a different value depending on the OS. For Windows,Environment.NewLine
is "rn" and on Linux, it is "r", but by usingEnvironment.NewLine
you do not need to worry about those details.
$endgroup$
– Rick Davin
Jan 17 at 15:26
1
$begingroup$
@RickDavin - yes, that's true. I think thatr
was old mac though, no? Unix/linux usen
AFAIK.
$endgroup$
– RobH
Jan 17 at 15:34
$begingroup$
My mistake. Yes Linux is "n". See stackoverflow.com/questions/1015766/…
$endgroup$
– Rick Davin
Jan 17 at 16:59
add a comment |
$begingroup$
You've already made a decent effort to make your types immutable by setting its properties in a constructor and providing get-only access, not set - like you have with Location.Name
for example. This is good practice whenever it is reasonable to do so (because, among other things, this means you can pass objects around without ever worrying that something else will change their state unexpectedly). However, I note that Location.Exits
is a public field, which means it could be replaced with another array while the program is running - but the house is supposed to be fixed in structure. It would be better as a get-only public property, passed as another parameter in the constructor.
More subtly, not only could someone do something like currentLocation.Exits = ...
, which the above advice would prevent; someone could also do currentLocation.Exits[0] = ...
, and again they are changing something that is supposed to be fixed. (When I say "someone", understand that that could mean "you, by mistake", especially in a larger more complex program). Since you mentioned you have been learning about interfaces, the public get accessor for Location.Exits
should be an IEnumerable<Location>
, which lets things enumerate through the exits array to see what they are, but not change them. (If you've not used generics yet, don't worry too much about this for now).
So it would end up like this:
abstract class Location
{
public Location(string name, Location exits)
{
Name = name;
_exits = exits;
}
public string Name { get; }
private Location _exits;
public IEnumerable<Location> Exits => _exits;
// ...
}
(Secretly, I'm not super-happy about passing in exits as an array, either. Again, someone could change that array after creating the Location
. I'd rather pass in exits as an IEnumerable<Location>
and copy them into a new array or other container type private to the Location
. But that raises some design and object ownership questions that aren't too relevant here so let's not worry about that).
Digging deeper on locations/exits - a couple of things here, which are probably too large a change to worry about for this exercise, but something to think about in future.
Firstly, OutsideWithDoor
inherits from Outside
and implements the IHasExteriorDoor
interface. This works for your purposes, but means the question of whether or not an exit from one location to the next has a door depends on the type of the locations, whereas logically speaking it's a property of the connection between them. (It's also limited to only one door per location, and a few other tricky bits - prefer to avoid unnecessary inheritance, and prefer composition over inheritance). So, I would suggest a LocationConnection
type, where Location
s are joined by LocationConnection
s rather than directly to other Location
s, and a LocationConnection
can either have a door or not (a boolean property).
Secondly, Location
exits are bi-directional, that is, if you can go from one location to another, you can always go back, too. That makes sense (if you go from the kitchen to the dining room, it'd be very odd to be unable to go back to the kitchen!) but depends on your initialization code always getting that right, which is a common source of bugs. (What if the building was a mansion, with a hundred rooms!?) This problem could go away if that LocationConnection
type were well-implemented, someone could travel along it in either direction and it only needs to be coded once. Something to bear in mind in future: whenever you have to remember to write BA if you write AB, someone's going to forget to do it. Make their (your) lives easier!
Introducing that new type may be a bigger change than is really justified for this code review, but it could solve a couple of potential problems.
A couple of very minor comments on ShowGoThroughExteriorDoorButton
. Firstly, the name of the method is OK, but it sounds like it's always going to show that button. I'd call it ShowOrHide...
, though that's just my personal preference. Also, the if-statement in that method is a bit inelegant. I'd write simply:
GoThroughExteriorDoorButton.Visible = (location is IHasExteriorDoor);
...which gets rid of those naked true
and false
values, and also gets rid of that ugly return
halfway through the method. In general, prefer methods to have a single point of exit, at the end, though this isn't always practical, especially when you start to use exceptions.
Always specify access modifiers, especially private
for all those fields on the ExploreTheHouseForm
. Also, a common convention for private fields is to prefix them with an underscore, e.g. private Location _currentLocation;
, though this is not universally followed - I like it because it helps make obvious what's a parameter or local variable, and what's a member variable.
$endgroup$
1
$begingroup$
The desire to have theLocation
immutable and take the exits in the constructor is great, but it seems incompatible with two-way travel. The kitchen has an exit to the dining room, and the dining room has an exit to to the kitchen. In order to follow your implementation advice, both would have to be created before the other. The proposedLocationConnection
class seems to have the same chicken and egg problem preventing immutability. It will have to reference the locations it connects, and they will have to reference it. How would you resolve this conflict?
$endgroup$
– Mr.Mindor
Jan 17 at 21:45
1
$begingroup$
Fair point. One possible solution would be to create the Locations without exit information, then create a list/LUT of LocationConnections. The Location then wouldn't itself hold references to its exits - every time it wanted to know, it would have to check the LocationConnection list. Is that better or worse than just allowing the Location class to be mutable? It becomes a judgement call at that point and depends on how the classes will be used. Mutable is great "whenever it is reasonable to do so"... it isn't always.
$endgroup$
– BittermanAndy
Jan 17 at 22:34
1
$begingroup$
Very interesting answer, thank you! I'll be taking this into consideration in the future!
$endgroup$
– Phrancis
Jan 18 at 2:36
$begingroup$
Comintern's (good) answer provides some examples of what the exit/connection types might look like.
$endgroup$
– BittermanAndy
Jan 18 at 8:21
1
$begingroup$
@MrMindor The usual way around this (apart from redesigning to avoid circular references) is to use a builder pattern during creation which is mutable and generate immutable classes from that one.
$endgroup$
– Voo
Jan 19 at 13:08
add a comment |
$begingroup$
The other reviews covered many of the main points I'd raise, but there are a handful of others.
Constructors in abstract classes should be protected
, not public
:
public Location(string name) => Name = name;
You can't create a new instance of the abstract class, so it is for all intents and purposes protected
anyway. Make the access modifier match the behavior.
I'm not sure that I like some of the naming. For example, CreateObjects()
gives only the slightest clue as to what it is doing. I'd probably go with something more along the lines of GenerateMap()
. A couple of the member names are also a little ambiguous as to how they function - for example, which room is IHasExteriorDoor.DoorLocation
relative to?
Speaking of doors (keeping in mind that I'm not sure how much was proscribed by the exercise), I also think I find it a bit more natural to have the exits be the primary motive object. It's more natural to me to think of "using a door" than it is to think of "leaving a room". To me, a room is a static thing - it has exits, but you don't really use a room by leaving it. I'd consider building your map from the standpoint of the connections between the locations instead of the locations themselves. Something more like this:
public interface ILocation
{
string Name { get; }
string Description { get; }
}
public interface IExit
{
// TODO: my naming sucks too.
ILocation LocationOne { get; }
ILocation LocationTwo { get; }
// Takes the room you're exiting as a parameter, returns where you end up.
ILocation Traverse(ILocation from);
}
public abstract class Location : ILocation
{
private readonly IReadOnlyList<IExit> _exits;
protected Location(string name, string description, IEnumerable<IExit> exits)
{
_exits = exits.ToList();
}
public IEnumerable<IExit> Exits => _exits;
// ...other properties...
}
That lets you concentrate on the spatial relationships between the locations from a more natural direction (IMO and no pun intended). You'll likely find this to be more easily extensible down the road when you need to, say close or lock a door:
public interface Door : IExit
{
bool IsOpen { get; }
bool IsLocked { get; }
}
$endgroup$
add a comment |
$begingroup$
In this code block here:
...
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects(); // <--- bleh
MoveToLocation(livingRoom);
}
This is call to the CreateObject()
method, is something I don't like to see in code (it could be a personal style issue) but if you are constructing an object, then all code related to the construction of an object should just stay in the constructor...
I would prefer that it ended up looking like
...
private readonly OutsideWithDoor _backYard; // now it can be readonly
private readonly Outside _garden;
public ExploreTheHouseForm()
{
InitializeComponent();
...
_backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
_garden = new Outside("garden", false);
MoveToLocation(livingRoom);
}
$endgroup$
1
$begingroup$
"all code related to the construction of an object should just stay in the constructor" I have the disagree with that. If it's a one-liner? Do it inline - no point creating helper methods. But for more complex initializations separating logically independent parts into their own methods keeps methods small, simple and self-documenting. Otherwise we end up with a giant constructor method that requires comments to separate the different parts. Now you certainly do want to keep things readonly, but the usual solution is to simply return the created object and assign it in the constructor.
$endgroup$
– Voo
Jan 19 at 13:20
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f211662%2fhouse-plan-design-head-first-c%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
5 Answers
5
active
oldest
votes
5 Answers
5
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
RobH's review covers syntax and style well so I won't go into that. Instead I'd like to give my take on feedback given by Svek and BittermanAndy.
Separation Of Concerns
I think Svek's commentary about the CreateObjects
method is spot on, but I don't think it goes far enough. The need for such a method in the first place hints that the ExploreTheHouseForm
class is doing to much. With the current implementation, each of the rooms is a field on the form. This effectively makes the ExploreTheHouseForm
the house itself. As such it would be more aptly named ExplorableHouseForm
.
In general (and this becomes ever more important as you get into more complex projects) we want to separate the presentation of the data from the data itself.
The form is the UI it already has the duty to present the data to the user. It shouldn't also be the data. I would much rather the house be constructed elsewhere and passed to the form's constructor:
public ExploreTheHouseForm(Location initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
With this simple change, you can remove all the individual Location
fields from ExploreTheHouseForm
with the exception of currentLocation
. Also, if you desire, you can use the same form to explore any number of different houses without further modification.
Immutability
A fair amount of BittermanAndy's advice (as of the time of this writing, his post was updated at least once since I started) was to try to make your Location
class immutable. Since with the overall design as it is, locations need to reference each other, you run into a chicken & egg scenario preventing immutability where each Location
needs their neighbors to be created before them. I don't see a way around this, however if you have your locations implement an interface, and write your form to consume the interface rather than Location
you can get much of the same benefit of actual immutability.
public interface ILocation
{
public string Name { get; }
public IList<ILocation> Exits {get;}
public string Description { get;}
}
In ILocation
we only specify the get
portions of the properties. This means to consumers of ILocation
the properties are effectively read-only even if the implementing classes implement the set
. We also declare Exits
as a collection of ILocation
rather than Location
so that accessed members are also read-only to consumers.
You don't have to change much about Location
itself:
public abstract class Location: ILocation
{
...
//private field to back Exits property.
private IList<ILocation> _exits;
public IList<ILocation> Exits {
get
{
// AsReadOnly so that consumers are not allowed to modify contents.
// there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.
return _exits?.AsReadOnly();
}
set{ _exits = value;}
}
}
Updating ExploreTheHouseForm
is also straight forward, simply change the type of the field currentLocation
the Location
parameters of ExploreTheHouseForm
, MoveToLocation
, and ShowGoThroughExteriorDoorButton
to ILocation
:
...
private ILocation _currentLocation;
...
public ExploreTheHouseForm(ILocation initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
...
private void MoveToLocation(ILocation location)
...
private void ShowGoThroughExteriorDoorButton(ILocation location)
The overall impact of this is that the locations are mutable during construction (by some factory) but once construction is complete, all you work with the read-only ILocation
// GenerateHouse returns the entry point of the house.
public ILocation GenerateHouse()
{
// Configure the locations
var livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
var kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
var diningRoom = new Room("dining room", "a crystal chandelier");
var frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
var backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
var garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new List<ILocation>() { diningRoom };
kitchen.Exits = new List<ILocation>() { diningRoom };
diningRoom.Exits = new List<ILocation> { livingRoom, kitchen };
frontYard.Exits = new List<ILocation> { backYard, garden };
backYard.Exits = new List<ILocation> { frontYard, garden };
garden.Exits = new List<ILocation> { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
// return entry point.
return livingRoom;
}
Location Connectivity
I agree with the other reviews and comments that pulling the concept of the location connection into a separate class/set of classes would allow for a better design. Locations can have any number of exits, and it is a property of the exit, not the location if that exit is an open archway, a door, or just an abstract dividing line (outdoor location to outdoor location) Comintern does a good job of covering this in their review so I won't go into it further.
$endgroup$
1
$begingroup$
Excellent answer. Separating the creation of the house, and presenting the form only with the interface it needs for navigation, solves the creation / immutability problem very nicely. (My edits were fairly minor, by the way).
$endgroup$
– BittermanAndy
Jan 18 at 8:26
$begingroup$
"there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.". At the very least I don't see any reason to not simply change the return value toIReadOnlyList
which makes the intent much clearer. Admittedly the better fix for the consumer would be to use IImmutableList from the immutable collections package which really guarantees that the content of the list won't change.
$endgroup$
– Voo
Jan 19 at 13:25
add a comment |
$begingroup$
RobH's review covers syntax and style well so I won't go into that. Instead I'd like to give my take on feedback given by Svek and BittermanAndy.
Separation Of Concerns
I think Svek's commentary about the CreateObjects
method is spot on, but I don't think it goes far enough. The need for such a method in the first place hints that the ExploreTheHouseForm
class is doing to much. With the current implementation, each of the rooms is a field on the form. This effectively makes the ExploreTheHouseForm
the house itself. As such it would be more aptly named ExplorableHouseForm
.
In general (and this becomes ever more important as you get into more complex projects) we want to separate the presentation of the data from the data itself.
The form is the UI it already has the duty to present the data to the user. It shouldn't also be the data. I would much rather the house be constructed elsewhere and passed to the form's constructor:
public ExploreTheHouseForm(Location initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
With this simple change, you can remove all the individual Location
fields from ExploreTheHouseForm
with the exception of currentLocation
. Also, if you desire, you can use the same form to explore any number of different houses without further modification.
Immutability
A fair amount of BittermanAndy's advice (as of the time of this writing, his post was updated at least once since I started) was to try to make your Location
class immutable. Since with the overall design as it is, locations need to reference each other, you run into a chicken & egg scenario preventing immutability where each Location
needs their neighbors to be created before them. I don't see a way around this, however if you have your locations implement an interface, and write your form to consume the interface rather than Location
you can get much of the same benefit of actual immutability.
public interface ILocation
{
public string Name { get; }
public IList<ILocation> Exits {get;}
public string Description { get;}
}
In ILocation
we only specify the get
portions of the properties. This means to consumers of ILocation
the properties are effectively read-only even if the implementing classes implement the set
. We also declare Exits
as a collection of ILocation
rather than Location
so that accessed members are also read-only to consumers.
You don't have to change much about Location
itself:
public abstract class Location: ILocation
{
...
//private field to back Exits property.
private IList<ILocation> _exits;
public IList<ILocation> Exits {
get
{
// AsReadOnly so that consumers are not allowed to modify contents.
// there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.
return _exits?.AsReadOnly();
}
set{ _exits = value;}
}
}
Updating ExploreTheHouseForm
is also straight forward, simply change the type of the field currentLocation
the Location
parameters of ExploreTheHouseForm
, MoveToLocation
, and ShowGoThroughExteriorDoorButton
to ILocation
:
...
private ILocation _currentLocation;
...
public ExploreTheHouseForm(ILocation initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
...
private void MoveToLocation(ILocation location)
...
private void ShowGoThroughExteriorDoorButton(ILocation location)
The overall impact of this is that the locations are mutable during construction (by some factory) but once construction is complete, all you work with the read-only ILocation
// GenerateHouse returns the entry point of the house.
public ILocation GenerateHouse()
{
// Configure the locations
var livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
var kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
var diningRoom = new Room("dining room", "a crystal chandelier");
var frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
var backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
var garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new List<ILocation>() { diningRoom };
kitchen.Exits = new List<ILocation>() { diningRoom };
diningRoom.Exits = new List<ILocation> { livingRoom, kitchen };
frontYard.Exits = new List<ILocation> { backYard, garden };
backYard.Exits = new List<ILocation> { frontYard, garden };
garden.Exits = new List<ILocation> { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
// return entry point.
return livingRoom;
}
Location Connectivity
I agree with the other reviews and comments that pulling the concept of the location connection into a separate class/set of classes would allow for a better design. Locations can have any number of exits, and it is a property of the exit, not the location if that exit is an open archway, a door, or just an abstract dividing line (outdoor location to outdoor location) Comintern does a good job of covering this in their review so I won't go into it further.
$endgroup$
1
$begingroup$
Excellent answer. Separating the creation of the house, and presenting the form only with the interface it needs for navigation, solves the creation / immutability problem very nicely. (My edits were fairly minor, by the way).
$endgroup$
– BittermanAndy
Jan 18 at 8:26
$begingroup$
"there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.". At the very least I don't see any reason to not simply change the return value toIReadOnlyList
which makes the intent much clearer. Admittedly the better fix for the consumer would be to use IImmutableList from the immutable collections package which really guarantees that the content of the list won't change.
$endgroup$
– Voo
Jan 19 at 13:25
add a comment |
$begingroup$
RobH's review covers syntax and style well so I won't go into that. Instead I'd like to give my take on feedback given by Svek and BittermanAndy.
Separation Of Concerns
I think Svek's commentary about the CreateObjects
method is spot on, but I don't think it goes far enough. The need for such a method in the first place hints that the ExploreTheHouseForm
class is doing to much. With the current implementation, each of the rooms is a field on the form. This effectively makes the ExploreTheHouseForm
the house itself. As such it would be more aptly named ExplorableHouseForm
.
In general (and this becomes ever more important as you get into more complex projects) we want to separate the presentation of the data from the data itself.
The form is the UI it already has the duty to present the data to the user. It shouldn't also be the data. I would much rather the house be constructed elsewhere and passed to the form's constructor:
public ExploreTheHouseForm(Location initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
With this simple change, you can remove all the individual Location
fields from ExploreTheHouseForm
with the exception of currentLocation
. Also, if you desire, you can use the same form to explore any number of different houses without further modification.
Immutability
A fair amount of BittermanAndy's advice (as of the time of this writing, his post was updated at least once since I started) was to try to make your Location
class immutable. Since with the overall design as it is, locations need to reference each other, you run into a chicken & egg scenario preventing immutability where each Location
needs their neighbors to be created before them. I don't see a way around this, however if you have your locations implement an interface, and write your form to consume the interface rather than Location
you can get much of the same benefit of actual immutability.
public interface ILocation
{
public string Name { get; }
public IList<ILocation> Exits {get;}
public string Description { get;}
}
In ILocation
we only specify the get
portions of the properties. This means to consumers of ILocation
the properties are effectively read-only even if the implementing classes implement the set
. We also declare Exits
as a collection of ILocation
rather than Location
so that accessed members are also read-only to consumers.
You don't have to change much about Location
itself:
public abstract class Location: ILocation
{
...
//private field to back Exits property.
private IList<ILocation> _exits;
public IList<ILocation> Exits {
get
{
// AsReadOnly so that consumers are not allowed to modify contents.
// there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.
return _exits?.AsReadOnly();
}
set{ _exits = value;}
}
}
Updating ExploreTheHouseForm
is also straight forward, simply change the type of the field currentLocation
the Location
parameters of ExploreTheHouseForm
, MoveToLocation
, and ShowGoThroughExteriorDoorButton
to ILocation
:
...
private ILocation _currentLocation;
...
public ExploreTheHouseForm(ILocation initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
...
private void MoveToLocation(ILocation location)
...
private void ShowGoThroughExteriorDoorButton(ILocation location)
The overall impact of this is that the locations are mutable during construction (by some factory) but once construction is complete, all you work with the read-only ILocation
// GenerateHouse returns the entry point of the house.
public ILocation GenerateHouse()
{
// Configure the locations
var livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
var kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
var diningRoom = new Room("dining room", "a crystal chandelier");
var frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
var backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
var garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new List<ILocation>() { diningRoom };
kitchen.Exits = new List<ILocation>() { diningRoom };
diningRoom.Exits = new List<ILocation> { livingRoom, kitchen };
frontYard.Exits = new List<ILocation> { backYard, garden };
backYard.Exits = new List<ILocation> { frontYard, garden };
garden.Exits = new List<ILocation> { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
// return entry point.
return livingRoom;
}
Location Connectivity
I agree with the other reviews and comments that pulling the concept of the location connection into a separate class/set of classes would allow for a better design. Locations can have any number of exits, and it is a property of the exit, not the location if that exit is an open archway, a door, or just an abstract dividing line (outdoor location to outdoor location) Comintern does a good job of covering this in their review so I won't go into it further.
$endgroup$
RobH's review covers syntax and style well so I won't go into that. Instead I'd like to give my take on feedback given by Svek and BittermanAndy.
Separation Of Concerns
I think Svek's commentary about the CreateObjects
method is spot on, but I don't think it goes far enough. The need for such a method in the first place hints that the ExploreTheHouseForm
class is doing to much. With the current implementation, each of the rooms is a field on the form. This effectively makes the ExploreTheHouseForm
the house itself. As such it would be more aptly named ExplorableHouseForm
.
In general (and this becomes ever more important as you get into more complex projects) we want to separate the presentation of the data from the data itself.
The form is the UI it already has the duty to present the data to the user. It shouldn't also be the data. I would much rather the house be constructed elsewhere and passed to the form's constructor:
public ExploreTheHouseForm(Location initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
With this simple change, you can remove all the individual Location
fields from ExploreTheHouseForm
with the exception of currentLocation
. Also, if you desire, you can use the same form to explore any number of different houses without further modification.
Immutability
A fair amount of BittermanAndy's advice (as of the time of this writing, his post was updated at least once since I started) was to try to make your Location
class immutable. Since with the overall design as it is, locations need to reference each other, you run into a chicken & egg scenario preventing immutability where each Location
needs their neighbors to be created before them. I don't see a way around this, however if you have your locations implement an interface, and write your form to consume the interface rather than Location
you can get much of the same benefit of actual immutability.
public interface ILocation
{
public string Name { get; }
public IList<ILocation> Exits {get;}
public string Description { get;}
}
In ILocation
we only specify the get
portions of the properties. This means to consumers of ILocation
the properties are effectively read-only even if the implementing classes implement the set
. We also declare Exits
as a collection of ILocation
rather than Location
so that accessed members are also read-only to consumers.
You don't have to change much about Location
itself:
public abstract class Location: ILocation
{
...
//private field to back Exits property.
private IList<ILocation> _exits;
public IList<ILocation> Exits {
get
{
// AsReadOnly so that consumers are not allowed to modify contents.
// there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.
return _exits?.AsReadOnly();
}
set{ _exits = value;}
}
}
Updating ExploreTheHouseForm
is also straight forward, simply change the type of the field currentLocation
the Location
parameters of ExploreTheHouseForm
, MoveToLocation
, and ShowGoThroughExteriorDoorButton
to ILocation
:
...
private ILocation _currentLocation;
...
public ExploreTheHouseForm(ILocation initialRoom)
{
InitializeComponent();
MoveToLocation(initialRoom);
}
...
private void MoveToLocation(ILocation location)
...
private void ShowGoThroughExteriorDoorButton(ILocation location)
The overall impact of this is that the locations are mutable during construction (by some factory) but once construction is complete, all you work with the read-only ILocation
// GenerateHouse returns the entry point of the house.
public ILocation GenerateHouse()
{
// Configure the locations
var livingRoom = new RoomWithDoor("living room", "an antique carpet", "an oak door with a brass knob");
var kitchen = new RoomWithDoor("kitchen", "stainless steel appliances", "a screen door");
var diningRoom = new Room("dining room", "a crystal chandelier");
var frontYard = new OutsideWithDoor("front yard", false, livingRoom.DoorDescription);
var backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
var garden = new Outside("garden", false);
// Configure the exits
livingRoom.Exits = new List<ILocation>() { diningRoom };
kitchen.Exits = new List<ILocation>() { diningRoom };
diningRoom.Exits = new List<ILocation> { livingRoom, kitchen };
frontYard.Exits = new List<ILocation> { backYard, garden };
backYard.Exits = new List<ILocation> { frontYard, garden };
garden.Exits = new List<ILocation> { frontYard, backYard };
// Configure exterior doors
livingRoom.DoorLocation = frontYard;
frontYard.DoorLocation = livingRoom;
kitchen.DoorLocation = backYard;
backYard.DoorLocation = kitchen;
// return entry point.
return livingRoom;
}
Location Connectivity
I agree with the other reviews and comments that pulling the concept of the location connection into a separate class/set of classes would allow for a better design. Locations can have any number of exits, and it is a property of the exit, not the location if that exit is an open archway, a door, or just an abstract dividing line (outdoor location to outdoor location) Comintern does a good job of covering this in their review so I won't go into it further.
answered Jan 18 at 0:30
Mr.MindorMr.Mindor
31614
31614
1
$begingroup$
Excellent answer. Separating the creation of the house, and presenting the form only with the interface it needs for navigation, solves the creation / immutability problem very nicely. (My edits were fairly minor, by the way).
$endgroup$
– BittermanAndy
Jan 18 at 8:26
$begingroup$
"there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.". At the very least I don't see any reason to not simply change the return value toIReadOnlyList
which makes the intent much clearer. Admittedly the better fix for the consumer would be to use IImmutableList from the immutable collections package which really guarantees that the content of the list won't change.
$endgroup$
– Voo
Jan 19 at 13:25
add a comment |
1
$begingroup$
Excellent answer. Separating the creation of the house, and presenting the form only with the interface it needs for navigation, solves the creation / immutability problem very nicely. (My edits were fairly minor, by the way).
$endgroup$
– BittermanAndy
Jan 18 at 8:26
$begingroup$
"there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.". At the very least I don't see any reason to not simply change the return value toIReadOnlyList
which makes the intent much clearer. Admittedly the better fix for the consumer would be to use IImmutableList from the immutable collections package which really guarantees that the content of the list won't change.
$endgroup$
– Voo
Jan 19 at 13:25
1
1
$begingroup$
Excellent answer. Separating the creation of the house, and presenting the form only with the interface it needs for navigation, solves the creation / immutability problem very nicely. (My edits were fairly minor, by the way).
$endgroup$
– BittermanAndy
Jan 18 at 8:26
$begingroup$
Excellent answer. Separating the creation of the house, and presenting the form only with the interface it needs for navigation, solves the creation / immutability problem very nicely. (My edits were fairly minor, by the way).
$endgroup$
– BittermanAndy
Jan 18 at 8:26
$begingroup$
"there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.". At the very least I don't see any reason to not simply change the return value to
IReadOnlyList
which makes the intent much clearer. Admittedly the better fix for the consumer would be to use IImmutableList from the immutable collections package which really guarantees that the content of the list won't change.$endgroup$
– Voo
Jan 19 at 13:25
$begingroup$
"there are other ways of accomplishing this that may be better overall, but ExploreTheHouseForm accesses Exits by index so we can only change it so much.". At the very least I don't see any reason to not simply change the return value to
IReadOnlyList
which makes the intent much clearer. Admittedly the better fix for the consumer would be to use IImmutableList from the immutable collections package which really guarantees that the content of the list won't change.$endgroup$
– Voo
Jan 19 at 13:25
add a comment |
$begingroup$
I have a general rule: if I see string concatenation with a loop I assume it's not the best way of doing it. Let's look at this:
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
I see this pattern a lot. What you need is Select
with string.Join
:
var exitList = string.Join(Environment.NewLine, Exits.Select(exit => $"— {exit.Name}"));
I've used Environment.NewLine
because I like using well-named constants. Random aside: r
is a Carriage Return and n
is a Line Feed. The terminology comes from physical printers. Another reason I prefer Environment.NewLine
is that it means you don't have to know and remember that.
Edit
A comment notes that Environment.NewLine
is different on Windows vs Linux/Unix. I think that's important to know and I really should have mentioned it the first time. As the comment exchange shows, it's really easy to get platforms and line-endings mixed up which I think illustrates the usefulness of the constant.
The Name
property of Location
is only set in the constructor. You can use a read-only auto property instead of a private set one:
public string Name { get; }
Outside.Description
could be using string interpolation like the other Description properties.
You're missing your access modifiers on your class declarations. MS guidelines state that they should always be specified.
I'm not aware of a convention for it but override public string
seems to be an odd order to me. I would expect the access modifier first: public override string
. The main thing is to keep that consistent though.
$endgroup$
2
$begingroup$
Environment.NewLine
is better for working with Windows or Linux because it contains a different value depending on the OS. For Windows,Environment.NewLine
is "rn" and on Linux, it is "r", but by usingEnvironment.NewLine
you do not need to worry about those details.
$endgroup$
– Rick Davin
Jan 17 at 15:26
1
$begingroup$
@RickDavin - yes, that's true. I think thatr
was old mac though, no? Unix/linux usen
AFAIK.
$endgroup$
– RobH
Jan 17 at 15:34
$begingroup$
My mistake. Yes Linux is "n". See stackoverflow.com/questions/1015766/…
$endgroup$
– Rick Davin
Jan 17 at 16:59
add a comment |
$begingroup$
I have a general rule: if I see string concatenation with a loop I assume it's not the best way of doing it. Let's look at this:
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
I see this pattern a lot. What you need is Select
with string.Join
:
var exitList = string.Join(Environment.NewLine, Exits.Select(exit => $"— {exit.Name}"));
I've used Environment.NewLine
because I like using well-named constants. Random aside: r
is a Carriage Return and n
is a Line Feed. The terminology comes from physical printers. Another reason I prefer Environment.NewLine
is that it means you don't have to know and remember that.
Edit
A comment notes that Environment.NewLine
is different on Windows vs Linux/Unix. I think that's important to know and I really should have mentioned it the first time. As the comment exchange shows, it's really easy to get platforms and line-endings mixed up which I think illustrates the usefulness of the constant.
The Name
property of Location
is only set in the constructor. You can use a read-only auto property instead of a private set one:
public string Name { get; }
Outside.Description
could be using string interpolation like the other Description properties.
You're missing your access modifiers on your class declarations. MS guidelines state that they should always be specified.
I'm not aware of a convention for it but override public string
seems to be an odd order to me. I would expect the access modifier first: public override string
. The main thing is to keep that consistent though.
$endgroup$
2
$begingroup$
Environment.NewLine
is better for working with Windows or Linux because it contains a different value depending on the OS. For Windows,Environment.NewLine
is "rn" and on Linux, it is "r", but by usingEnvironment.NewLine
you do not need to worry about those details.
$endgroup$
– Rick Davin
Jan 17 at 15:26
1
$begingroup$
@RickDavin - yes, that's true. I think thatr
was old mac though, no? Unix/linux usen
AFAIK.
$endgroup$
– RobH
Jan 17 at 15:34
$begingroup$
My mistake. Yes Linux is "n". See stackoverflow.com/questions/1015766/…
$endgroup$
– Rick Davin
Jan 17 at 16:59
add a comment |
$begingroup$
I have a general rule: if I see string concatenation with a loop I assume it's not the best way of doing it. Let's look at this:
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
I see this pattern a lot. What you need is Select
with string.Join
:
var exitList = string.Join(Environment.NewLine, Exits.Select(exit => $"— {exit.Name}"));
I've used Environment.NewLine
because I like using well-named constants. Random aside: r
is a Carriage Return and n
is a Line Feed. The terminology comes from physical printers. Another reason I prefer Environment.NewLine
is that it means you don't have to know and remember that.
Edit
A comment notes that Environment.NewLine
is different on Windows vs Linux/Unix. I think that's important to know and I really should have mentioned it the first time. As the comment exchange shows, it's really easy to get platforms and line-endings mixed up which I think illustrates the usefulness of the constant.
The Name
property of Location
is only set in the constructor. You can use a read-only auto property instead of a private set one:
public string Name { get; }
Outside.Description
could be using string interpolation like the other Description properties.
You're missing your access modifiers on your class declarations. MS guidelines state that they should always be specified.
I'm not aware of a convention for it but override public string
seems to be an odd order to me. I would expect the access modifier first: public override string
. The main thing is to keep that consistent though.
$endgroup$
I have a general rule: if I see string concatenation with a loop I assume it's not the best way of doing it. Let's look at this:
foreach (Location exit in Exits)
{
description += $"— {exit.Name}";
if (exit != Exits[Exits.Length - 1])
{
description += "rn";
}
}
I see this pattern a lot. What you need is Select
with string.Join
:
var exitList = string.Join(Environment.NewLine, Exits.Select(exit => $"— {exit.Name}"));
I've used Environment.NewLine
because I like using well-named constants. Random aside: r
is a Carriage Return and n
is a Line Feed. The terminology comes from physical printers. Another reason I prefer Environment.NewLine
is that it means you don't have to know and remember that.
Edit
A comment notes that Environment.NewLine
is different on Windows vs Linux/Unix. I think that's important to know and I really should have mentioned it the first time. As the comment exchange shows, it's really easy to get platforms and line-endings mixed up which I think illustrates the usefulness of the constant.
The Name
property of Location
is only set in the constructor. You can use a read-only auto property instead of a private set one:
public string Name { get; }
Outside.Description
could be using string interpolation like the other Description properties.
You're missing your access modifiers on your class declarations. MS guidelines state that they should always be specified.
I'm not aware of a convention for it but override public string
seems to be an odd order to me. I would expect the access modifier first: public override string
. The main thing is to keep that consistent though.
edited Jan 18 at 8:29
answered Jan 17 at 10:06
RobHRobH
14.7k42762
14.7k42762
2
$begingroup$
Environment.NewLine
is better for working with Windows or Linux because it contains a different value depending on the OS. For Windows,Environment.NewLine
is "rn" and on Linux, it is "r", but by usingEnvironment.NewLine
you do not need to worry about those details.
$endgroup$
– Rick Davin
Jan 17 at 15:26
1
$begingroup$
@RickDavin - yes, that's true. I think thatr
was old mac though, no? Unix/linux usen
AFAIK.
$endgroup$
– RobH
Jan 17 at 15:34
$begingroup$
My mistake. Yes Linux is "n". See stackoverflow.com/questions/1015766/…
$endgroup$
– Rick Davin
Jan 17 at 16:59
add a comment |
2
$begingroup$
Environment.NewLine
is better for working with Windows or Linux because it contains a different value depending on the OS. For Windows,Environment.NewLine
is "rn" and on Linux, it is "r", but by usingEnvironment.NewLine
you do not need to worry about those details.
$endgroup$
– Rick Davin
Jan 17 at 15:26
1
$begingroup$
@RickDavin - yes, that's true. I think thatr
was old mac though, no? Unix/linux usen
AFAIK.
$endgroup$
– RobH
Jan 17 at 15:34
$begingroup$
My mistake. Yes Linux is "n". See stackoverflow.com/questions/1015766/…
$endgroup$
– Rick Davin
Jan 17 at 16:59
2
2
$begingroup$
Environment.NewLine
is better for working with Windows or Linux because it contains a different value depending on the OS. For Windows, Environment.NewLine
is "rn" and on Linux, it is "r", but by using Environment.NewLine
you do not need to worry about those details.$endgroup$
– Rick Davin
Jan 17 at 15:26
$begingroup$
Environment.NewLine
is better for working with Windows or Linux because it contains a different value depending on the OS. For Windows, Environment.NewLine
is "rn" and on Linux, it is "r", but by using Environment.NewLine
you do not need to worry about those details.$endgroup$
– Rick Davin
Jan 17 at 15:26
1
1
$begingroup$
@RickDavin - yes, that's true. I think that
r
was old mac though, no? Unix/linux use n
AFAIK.$endgroup$
– RobH
Jan 17 at 15:34
$begingroup$
@RickDavin - yes, that's true. I think that
r
was old mac though, no? Unix/linux use n
AFAIK.$endgroup$
– RobH
Jan 17 at 15:34
$begingroup$
My mistake. Yes Linux is "n". See stackoverflow.com/questions/1015766/…
$endgroup$
– Rick Davin
Jan 17 at 16:59
$begingroup$
My mistake. Yes Linux is "n". See stackoverflow.com/questions/1015766/…
$endgroup$
– Rick Davin
Jan 17 at 16:59
add a comment |
$begingroup$
You've already made a decent effort to make your types immutable by setting its properties in a constructor and providing get-only access, not set - like you have with Location.Name
for example. This is good practice whenever it is reasonable to do so (because, among other things, this means you can pass objects around without ever worrying that something else will change their state unexpectedly). However, I note that Location.Exits
is a public field, which means it could be replaced with another array while the program is running - but the house is supposed to be fixed in structure. It would be better as a get-only public property, passed as another parameter in the constructor.
More subtly, not only could someone do something like currentLocation.Exits = ...
, which the above advice would prevent; someone could also do currentLocation.Exits[0] = ...
, and again they are changing something that is supposed to be fixed. (When I say "someone", understand that that could mean "you, by mistake", especially in a larger more complex program). Since you mentioned you have been learning about interfaces, the public get accessor for Location.Exits
should be an IEnumerable<Location>
, which lets things enumerate through the exits array to see what they are, but not change them. (If you've not used generics yet, don't worry too much about this for now).
So it would end up like this:
abstract class Location
{
public Location(string name, Location exits)
{
Name = name;
_exits = exits;
}
public string Name { get; }
private Location _exits;
public IEnumerable<Location> Exits => _exits;
// ...
}
(Secretly, I'm not super-happy about passing in exits as an array, either. Again, someone could change that array after creating the Location
. I'd rather pass in exits as an IEnumerable<Location>
and copy them into a new array or other container type private to the Location
. But that raises some design and object ownership questions that aren't too relevant here so let's not worry about that).
Digging deeper on locations/exits - a couple of things here, which are probably too large a change to worry about for this exercise, but something to think about in future.
Firstly, OutsideWithDoor
inherits from Outside
and implements the IHasExteriorDoor
interface. This works for your purposes, but means the question of whether or not an exit from one location to the next has a door depends on the type of the locations, whereas logically speaking it's a property of the connection between them. (It's also limited to only one door per location, and a few other tricky bits - prefer to avoid unnecessary inheritance, and prefer composition over inheritance). So, I would suggest a LocationConnection
type, where Location
s are joined by LocationConnection
s rather than directly to other Location
s, and a LocationConnection
can either have a door or not (a boolean property).
Secondly, Location
exits are bi-directional, that is, if you can go from one location to another, you can always go back, too. That makes sense (if you go from the kitchen to the dining room, it'd be very odd to be unable to go back to the kitchen!) but depends on your initialization code always getting that right, which is a common source of bugs. (What if the building was a mansion, with a hundred rooms!?) This problem could go away if that LocationConnection
type were well-implemented, someone could travel along it in either direction and it only needs to be coded once. Something to bear in mind in future: whenever you have to remember to write BA if you write AB, someone's going to forget to do it. Make their (your) lives easier!
Introducing that new type may be a bigger change than is really justified for this code review, but it could solve a couple of potential problems.
A couple of very minor comments on ShowGoThroughExteriorDoorButton
. Firstly, the name of the method is OK, but it sounds like it's always going to show that button. I'd call it ShowOrHide...
, though that's just my personal preference. Also, the if-statement in that method is a bit inelegant. I'd write simply:
GoThroughExteriorDoorButton.Visible = (location is IHasExteriorDoor);
...which gets rid of those naked true
and false
values, and also gets rid of that ugly return
halfway through the method. In general, prefer methods to have a single point of exit, at the end, though this isn't always practical, especially when you start to use exceptions.
Always specify access modifiers, especially private
for all those fields on the ExploreTheHouseForm
. Also, a common convention for private fields is to prefix them with an underscore, e.g. private Location _currentLocation;
, though this is not universally followed - I like it because it helps make obvious what's a parameter or local variable, and what's a member variable.
$endgroup$
1
$begingroup$
The desire to have theLocation
immutable and take the exits in the constructor is great, but it seems incompatible with two-way travel. The kitchen has an exit to the dining room, and the dining room has an exit to to the kitchen. In order to follow your implementation advice, both would have to be created before the other. The proposedLocationConnection
class seems to have the same chicken and egg problem preventing immutability. It will have to reference the locations it connects, and they will have to reference it. How would you resolve this conflict?
$endgroup$
– Mr.Mindor
Jan 17 at 21:45
1
$begingroup$
Fair point. One possible solution would be to create the Locations without exit information, then create a list/LUT of LocationConnections. The Location then wouldn't itself hold references to its exits - every time it wanted to know, it would have to check the LocationConnection list. Is that better or worse than just allowing the Location class to be mutable? It becomes a judgement call at that point and depends on how the classes will be used. Mutable is great "whenever it is reasonable to do so"... it isn't always.
$endgroup$
– BittermanAndy
Jan 17 at 22:34
1
$begingroup$
Very interesting answer, thank you! I'll be taking this into consideration in the future!
$endgroup$
– Phrancis
Jan 18 at 2:36
$begingroup$
Comintern's (good) answer provides some examples of what the exit/connection types might look like.
$endgroup$
– BittermanAndy
Jan 18 at 8:21
1
$begingroup$
@MrMindor The usual way around this (apart from redesigning to avoid circular references) is to use a builder pattern during creation which is mutable and generate immutable classes from that one.
$endgroup$
– Voo
Jan 19 at 13:08
add a comment |
$begingroup$
You've already made a decent effort to make your types immutable by setting its properties in a constructor and providing get-only access, not set - like you have with Location.Name
for example. This is good practice whenever it is reasonable to do so (because, among other things, this means you can pass objects around without ever worrying that something else will change their state unexpectedly). However, I note that Location.Exits
is a public field, which means it could be replaced with another array while the program is running - but the house is supposed to be fixed in structure. It would be better as a get-only public property, passed as another parameter in the constructor.
More subtly, not only could someone do something like currentLocation.Exits = ...
, which the above advice would prevent; someone could also do currentLocation.Exits[0] = ...
, and again they are changing something that is supposed to be fixed. (When I say "someone", understand that that could mean "you, by mistake", especially in a larger more complex program). Since you mentioned you have been learning about interfaces, the public get accessor for Location.Exits
should be an IEnumerable<Location>
, which lets things enumerate through the exits array to see what they are, but not change them. (If you've not used generics yet, don't worry too much about this for now).
So it would end up like this:
abstract class Location
{
public Location(string name, Location exits)
{
Name = name;
_exits = exits;
}
public string Name { get; }
private Location _exits;
public IEnumerable<Location> Exits => _exits;
// ...
}
(Secretly, I'm not super-happy about passing in exits as an array, either. Again, someone could change that array after creating the Location
. I'd rather pass in exits as an IEnumerable<Location>
and copy them into a new array or other container type private to the Location
. But that raises some design and object ownership questions that aren't too relevant here so let's not worry about that).
Digging deeper on locations/exits - a couple of things here, which are probably too large a change to worry about for this exercise, but something to think about in future.
Firstly, OutsideWithDoor
inherits from Outside
and implements the IHasExteriorDoor
interface. This works for your purposes, but means the question of whether or not an exit from one location to the next has a door depends on the type of the locations, whereas logically speaking it's a property of the connection between them. (It's also limited to only one door per location, and a few other tricky bits - prefer to avoid unnecessary inheritance, and prefer composition over inheritance). So, I would suggest a LocationConnection
type, where Location
s are joined by LocationConnection
s rather than directly to other Location
s, and a LocationConnection
can either have a door or not (a boolean property).
Secondly, Location
exits are bi-directional, that is, if you can go from one location to another, you can always go back, too. That makes sense (if you go from the kitchen to the dining room, it'd be very odd to be unable to go back to the kitchen!) but depends on your initialization code always getting that right, which is a common source of bugs. (What if the building was a mansion, with a hundred rooms!?) This problem could go away if that LocationConnection
type were well-implemented, someone could travel along it in either direction and it only needs to be coded once. Something to bear in mind in future: whenever you have to remember to write BA if you write AB, someone's going to forget to do it. Make their (your) lives easier!
Introducing that new type may be a bigger change than is really justified for this code review, but it could solve a couple of potential problems.
A couple of very minor comments on ShowGoThroughExteriorDoorButton
. Firstly, the name of the method is OK, but it sounds like it's always going to show that button. I'd call it ShowOrHide...
, though that's just my personal preference. Also, the if-statement in that method is a bit inelegant. I'd write simply:
GoThroughExteriorDoorButton.Visible = (location is IHasExteriorDoor);
...which gets rid of those naked true
and false
values, and also gets rid of that ugly return
halfway through the method. In general, prefer methods to have a single point of exit, at the end, though this isn't always practical, especially when you start to use exceptions.
Always specify access modifiers, especially private
for all those fields on the ExploreTheHouseForm
. Also, a common convention for private fields is to prefix them with an underscore, e.g. private Location _currentLocation;
, though this is not universally followed - I like it because it helps make obvious what's a parameter or local variable, and what's a member variable.
$endgroup$
1
$begingroup$
The desire to have theLocation
immutable and take the exits in the constructor is great, but it seems incompatible with two-way travel. The kitchen has an exit to the dining room, and the dining room has an exit to to the kitchen. In order to follow your implementation advice, both would have to be created before the other. The proposedLocationConnection
class seems to have the same chicken and egg problem preventing immutability. It will have to reference the locations it connects, and they will have to reference it. How would you resolve this conflict?
$endgroup$
– Mr.Mindor
Jan 17 at 21:45
1
$begingroup$
Fair point. One possible solution would be to create the Locations without exit information, then create a list/LUT of LocationConnections. The Location then wouldn't itself hold references to its exits - every time it wanted to know, it would have to check the LocationConnection list. Is that better or worse than just allowing the Location class to be mutable? It becomes a judgement call at that point and depends on how the classes will be used. Mutable is great "whenever it is reasonable to do so"... it isn't always.
$endgroup$
– BittermanAndy
Jan 17 at 22:34
1
$begingroup$
Very interesting answer, thank you! I'll be taking this into consideration in the future!
$endgroup$
– Phrancis
Jan 18 at 2:36
$begingroup$
Comintern's (good) answer provides some examples of what the exit/connection types might look like.
$endgroup$
– BittermanAndy
Jan 18 at 8:21
1
$begingroup$
@MrMindor The usual way around this (apart from redesigning to avoid circular references) is to use a builder pattern during creation which is mutable and generate immutable classes from that one.
$endgroup$
– Voo
Jan 19 at 13:08
add a comment |
$begingroup$
You've already made a decent effort to make your types immutable by setting its properties in a constructor and providing get-only access, not set - like you have with Location.Name
for example. This is good practice whenever it is reasonable to do so (because, among other things, this means you can pass objects around without ever worrying that something else will change their state unexpectedly). However, I note that Location.Exits
is a public field, which means it could be replaced with another array while the program is running - but the house is supposed to be fixed in structure. It would be better as a get-only public property, passed as another parameter in the constructor.
More subtly, not only could someone do something like currentLocation.Exits = ...
, which the above advice would prevent; someone could also do currentLocation.Exits[0] = ...
, and again they are changing something that is supposed to be fixed. (When I say "someone", understand that that could mean "you, by mistake", especially in a larger more complex program). Since you mentioned you have been learning about interfaces, the public get accessor for Location.Exits
should be an IEnumerable<Location>
, which lets things enumerate through the exits array to see what they are, but not change them. (If you've not used generics yet, don't worry too much about this for now).
So it would end up like this:
abstract class Location
{
public Location(string name, Location exits)
{
Name = name;
_exits = exits;
}
public string Name { get; }
private Location _exits;
public IEnumerable<Location> Exits => _exits;
// ...
}
(Secretly, I'm not super-happy about passing in exits as an array, either. Again, someone could change that array after creating the Location
. I'd rather pass in exits as an IEnumerable<Location>
and copy them into a new array or other container type private to the Location
. But that raises some design and object ownership questions that aren't too relevant here so let's not worry about that).
Digging deeper on locations/exits - a couple of things here, which are probably too large a change to worry about for this exercise, but something to think about in future.
Firstly, OutsideWithDoor
inherits from Outside
and implements the IHasExteriorDoor
interface. This works for your purposes, but means the question of whether or not an exit from one location to the next has a door depends on the type of the locations, whereas logically speaking it's a property of the connection between them. (It's also limited to only one door per location, and a few other tricky bits - prefer to avoid unnecessary inheritance, and prefer composition over inheritance). So, I would suggest a LocationConnection
type, where Location
s are joined by LocationConnection
s rather than directly to other Location
s, and a LocationConnection
can either have a door or not (a boolean property).
Secondly, Location
exits are bi-directional, that is, if you can go from one location to another, you can always go back, too. That makes sense (if you go from the kitchen to the dining room, it'd be very odd to be unable to go back to the kitchen!) but depends on your initialization code always getting that right, which is a common source of bugs. (What if the building was a mansion, with a hundred rooms!?) This problem could go away if that LocationConnection
type were well-implemented, someone could travel along it in either direction and it only needs to be coded once. Something to bear in mind in future: whenever you have to remember to write BA if you write AB, someone's going to forget to do it. Make their (your) lives easier!
Introducing that new type may be a bigger change than is really justified for this code review, but it could solve a couple of potential problems.
A couple of very minor comments on ShowGoThroughExteriorDoorButton
. Firstly, the name of the method is OK, but it sounds like it's always going to show that button. I'd call it ShowOrHide...
, though that's just my personal preference. Also, the if-statement in that method is a bit inelegant. I'd write simply:
GoThroughExteriorDoorButton.Visible = (location is IHasExteriorDoor);
...which gets rid of those naked true
and false
values, and also gets rid of that ugly return
halfway through the method. In general, prefer methods to have a single point of exit, at the end, though this isn't always practical, especially when you start to use exceptions.
Always specify access modifiers, especially private
for all those fields on the ExploreTheHouseForm
. Also, a common convention for private fields is to prefix them with an underscore, e.g. private Location _currentLocation;
, though this is not universally followed - I like it because it helps make obvious what's a parameter or local variable, and what's a member variable.
$endgroup$
You've already made a decent effort to make your types immutable by setting its properties in a constructor and providing get-only access, not set - like you have with Location.Name
for example. This is good practice whenever it is reasonable to do so (because, among other things, this means you can pass objects around without ever worrying that something else will change their state unexpectedly). However, I note that Location.Exits
is a public field, which means it could be replaced with another array while the program is running - but the house is supposed to be fixed in structure. It would be better as a get-only public property, passed as another parameter in the constructor.
More subtly, not only could someone do something like currentLocation.Exits = ...
, which the above advice would prevent; someone could also do currentLocation.Exits[0] = ...
, and again they are changing something that is supposed to be fixed. (When I say "someone", understand that that could mean "you, by mistake", especially in a larger more complex program). Since you mentioned you have been learning about interfaces, the public get accessor for Location.Exits
should be an IEnumerable<Location>
, which lets things enumerate through the exits array to see what they are, but not change them. (If you've not used generics yet, don't worry too much about this for now).
So it would end up like this:
abstract class Location
{
public Location(string name, Location exits)
{
Name = name;
_exits = exits;
}
public string Name { get; }
private Location _exits;
public IEnumerable<Location> Exits => _exits;
// ...
}
(Secretly, I'm not super-happy about passing in exits as an array, either. Again, someone could change that array after creating the Location
. I'd rather pass in exits as an IEnumerable<Location>
and copy them into a new array or other container type private to the Location
. But that raises some design and object ownership questions that aren't too relevant here so let's not worry about that).
Digging deeper on locations/exits - a couple of things here, which are probably too large a change to worry about for this exercise, but something to think about in future.
Firstly, OutsideWithDoor
inherits from Outside
and implements the IHasExteriorDoor
interface. This works for your purposes, but means the question of whether or not an exit from one location to the next has a door depends on the type of the locations, whereas logically speaking it's a property of the connection between them. (It's also limited to only one door per location, and a few other tricky bits - prefer to avoid unnecessary inheritance, and prefer composition over inheritance). So, I would suggest a LocationConnection
type, where Location
s are joined by LocationConnection
s rather than directly to other Location
s, and a LocationConnection
can either have a door or not (a boolean property).
Secondly, Location
exits are bi-directional, that is, if you can go from one location to another, you can always go back, too. That makes sense (if you go from the kitchen to the dining room, it'd be very odd to be unable to go back to the kitchen!) but depends on your initialization code always getting that right, which is a common source of bugs. (What if the building was a mansion, with a hundred rooms!?) This problem could go away if that LocationConnection
type were well-implemented, someone could travel along it in either direction and it only needs to be coded once. Something to bear in mind in future: whenever you have to remember to write BA if you write AB, someone's going to forget to do it. Make their (your) lives easier!
Introducing that new type may be a bigger change than is really justified for this code review, but it could solve a couple of potential problems.
A couple of very minor comments on ShowGoThroughExteriorDoorButton
. Firstly, the name of the method is OK, but it sounds like it's always going to show that button. I'd call it ShowOrHide...
, though that's just my personal preference. Also, the if-statement in that method is a bit inelegant. I'd write simply:
GoThroughExteriorDoorButton.Visible = (location is IHasExteriorDoor);
...which gets rid of those naked true
and false
values, and also gets rid of that ugly return
halfway through the method. In general, prefer methods to have a single point of exit, at the end, though this isn't always practical, especially when you start to use exceptions.
Always specify access modifiers, especially private
for all those fields on the ExploreTheHouseForm
. Also, a common convention for private fields is to prefix them with an underscore, e.g. private Location _currentLocation;
, though this is not universally followed - I like it because it helps make obvious what's a parameter or local variable, and what's a member variable.
edited Jan 17 at 22:36
answered Jan 17 at 18:59
BittermanAndyBittermanAndy
2263
2263
1
$begingroup$
The desire to have theLocation
immutable and take the exits in the constructor is great, but it seems incompatible with two-way travel. The kitchen has an exit to the dining room, and the dining room has an exit to to the kitchen. In order to follow your implementation advice, both would have to be created before the other. The proposedLocationConnection
class seems to have the same chicken and egg problem preventing immutability. It will have to reference the locations it connects, and they will have to reference it. How would you resolve this conflict?
$endgroup$
– Mr.Mindor
Jan 17 at 21:45
1
$begingroup$
Fair point. One possible solution would be to create the Locations without exit information, then create a list/LUT of LocationConnections. The Location then wouldn't itself hold references to its exits - every time it wanted to know, it would have to check the LocationConnection list. Is that better or worse than just allowing the Location class to be mutable? It becomes a judgement call at that point and depends on how the classes will be used. Mutable is great "whenever it is reasonable to do so"... it isn't always.
$endgroup$
– BittermanAndy
Jan 17 at 22:34
1
$begingroup$
Very interesting answer, thank you! I'll be taking this into consideration in the future!
$endgroup$
– Phrancis
Jan 18 at 2:36
$begingroup$
Comintern's (good) answer provides some examples of what the exit/connection types might look like.
$endgroup$
– BittermanAndy
Jan 18 at 8:21
1
$begingroup$
@MrMindor The usual way around this (apart from redesigning to avoid circular references) is to use a builder pattern during creation which is mutable and generate immutable classes from that one.
$endgroup$
– Voo
Jan 19 at 13:08
add a comment |
1
$begingroup$
The desire to have theLocation
immutable and take the exits in the constructor is great, but it seems incompatible with two-way travel. The kitchen has an exit to the dining room, and the dining room has an exit to to the kitchen. In order to follow your implementation advice, both would have to be created before the other. The proposedLocationConnection
class seems to have the same chicken and egg problem preventing immutability. It will have to reference the locations it connects, and they will have to reference it. How would you resolve this conflict?
$endgroup$
– Mr.Mindor
Jan 17 at 21:45
1
$begingroup$
Fair point. One possible solution would be to create the Locations without exit information, then create a list/LUT of LocationConnections. The Location then wouldn't itself hold references to its exits - every time it wanted to know, it would have to check the LocationConnection list. Is that better or worse than just allowing the Location class to be mutable? It becomes a judgement call at that point and depends on how the classes will be used. Mutable is great "whenever it is reasonable to do so"... it isn't always.
$endgroup$
– BittermanAndy
Jan 17 at 22:34
1
$begingroup$
Very interesting answer, thank you! I'll be taking this into consideration in the future!
$endgroup$
– Phrancis
Jan 18 at 2:36
$begingroup$
Comintern's (good) answer provides some examples of what the exit/connection types might look like.
$endgroup$
– BittermanAndy
Jan 18 at 8:21
1
$begingroup$
@MrMindor The usual way around this (apart from redesigning to avoid circular references) is to use a builder pattern during creation which is mutable and generate immutable classes from that one.
$endgroup$
– Voo
Jan 19 at 13:08
1
1
$begingroup$
The desire to have the
Location
immutable and take the exits in the constructor is great, but it seems incompatible with two-way travel. The kitchen has an exit to the dining room, and the dining room has an exit to to the kitchen. In order to follow your implementation advice, both would have to be created before the other. The proposed LocationConnection
class seems to have the same chicken and egg problem preventing immutability. It will have to reference the locations it connects, and they will have to reference it. How would you resolve this conflict?$endgroup$
– Mr.Mindor
Jan 17 at 21:45
$begingroup$
The desire to have the
Location
immutable and take the exits in the constructor is great, but it seems incompatible with two-way travel. The kitchen has an exit to the dining room, and the dining room has an exit to to the kitchen. In order to follow your implementation advice, both would have to be created before the other. The proposed LocationConnection
class seems to have the same chicken and egg problem preventing immutability. It will have to reference the locations it connects, and they will have to reference it. How would you resolve this conflict?$endgroup$
– Mr.Mindor
Jan 17 at 21:45
1
1
$begingroup$
Fair point. One possible solution would be to create the Locations without exit information, then create a list/LUT of LocationConnections. The Location then wouldn't itself hold references to its exits - every time it wanted to know, it would have to check the LocationConnection list. Is that better or worse than just allowing the Location class to be mutable? It becomes a judgement call at that point and depends on how the classes will be used. Mutable is great "whenever it is reasonable to do so"... it isn't always.
$endgroup$
– BittermanAndy
Jan 17 at 22:34
$begingroup$
Fair point. One possible solution would be to create the Locations without exit information, then create a list/LUT of LocationConnections. The Location then wouldn't itself hold references to its exits - every time it wanted to know, it would have to check the LocationConnection list. Is that better or worse than just allowing the Location class to be mutable? It becomes a judgement call at that point and depends on how the classes will be used. Mutable is great "whenever it is reasonable to do so"... it isn't always.
$endgroup$
– BittermanAndy
Jan 17 at 22:34
1
1
$begingroup$
Very interesting answer, thank you! I'll be taking this into consideration in the future!
$endgroup$
– Phrancis
Jan 18 at 2:36
$begingroup$
Very interesting answer, thank you! I'll be taking this into consideration in the future!
$endgroup$
– Phrancis
Jan 18 at 2:36
$begingroup$
Comintern's (good) answer provides some examples of what the exit/connection types might look like.
$endgroup$
– BittermanAndy
Jan 18 at 8:21
$begingroup$
Comintern's (good) answer provides some examples of what the exit/connection types might look like.
$endgroup$
– BittermanAndy
Jan 18 at 8:21
1
1
$begingroup$
@MrMindor The usual way around this (apart from redesigning to avoid circular references) is to use a builder pattern during creation which is mutable and generate immutable classes from that one.
$endgroup$
– Voo
Jan 19 at 13:08
$begingroup$
@MrMindor The usual way around this (apart from redesigning to avoid circular references) is to use a builder pattern during creation which is mutable and generate immutable classes from that one.
$endgroup$
– Voo
Jan 19 at 13:08
add a comment |
$begingroup$
The other reviews covered many of the main points I'd raise, but there are a handful of others.
Constructors in abstract classes should be protected
, not public
:
public Location(string name) => Name = name;
You can't create a new instance of the abstract class, so it is for all intents and purposes protected
anyway. Make the access modifier match the behavior.
I'm not sure that I like some of the naming. For example, CreateObjects()
gives only the slightest clue as to what it is doing. I'd probably go with something more along the lines of GenerateMap()
. A couple of the member names are also a little ambiguous as to how they function - for example, which room is IHasExteriorDoor.DoorLocation
relative to?
Speaking of doors (keeping in mind that I'm not sure how much was proscribed by the exercise), I also think I find it a bit more natural to have the exits be the primary motive object. It's more natural to me to think of "using a door" than it is to think of "leaving a room". To me, a room is a static thing - it has exits, but you don't really use a room by leaving it. I'd consider building your map from the standpoint of the connections between the locations instead of the locations themselves. Something more like this:
public interface ILocation
{
string Name { get; }
string Description { get; }
}
public interface IExit
{
// TODO: my naming sucks too.
ILocation LocationOne { get; }
ILocation LocationTwo { get; }
// Takes the room you're exiting as a parameter, returns where you end up.
ILocation Traverse(ILocation from);
}
public abstract class Location : ILocation
{
private readonly IReadOnlyList<IExit> _exits;
protected Location(string name, string description, IEnumerable<IExit> exits)
{
_exits = exits.ToList();
}
public IEnumerable<IExit> Exits => _exits;
// ...other properties...
}
That lets you concentrate on the spatial relationships between the locations from a more natural direction (IMO and no pun intended). You'll likely find this to be more easily extensible down the road when you need to, say close or lock a door:
public interface Door : IExit
{
bool IsOpen { get; }
bool IsLocked { get; }
}
$endgroup$
add a comment |
$begingroup$
The other reviews covered many of the main points I'd raise, but there are a handful of others.
Constructors in abstract classes should be protected
, not public
:
public Location(string name) => Name = name;
You can't create a new instance of the abstract class, so it is for all intents and purposes protected
anyway. Make the access modifier match the behavior.
I'm not sure that I like some of the naming. For example, CreateObjects()
gives only the slightest clue as to what it is doing. I'd probably go with something more along the lines of GenerateMap()
. A couple of the member names are also a little ambiguous as to how they function - for example, which room is IHasExteriorDoor.DoorLocation
relative to?
Speaking of doors (keeping in mind that I'm not sure how much was proscribed by the exercise), I also think I find it a bit more natural to have the exits be the primary motive object. It's more natural to me to think of "using a door" than it is to think of "leaving a room". To me, a room is a static thing - it has exits, but you don't really use a room by leaving it. I'd consider building your map from the standpoint of the connections between the locations instead of the locations themselves. Something more like this:
public interface ILocation
{
string Name { get; }
string Description { get; }
}
public interface IExit
{
// TODO: my naming sucks too.
ILocation LocationOne { get; }
ILocation LocationTwo { get; }
// Takes the room you're exiting as a parameter, returns where you end up.
ILocation Traverse(ILocation from);
}
public abstract class Location : ILocation
{
private readonly IReadOnlyList<IExit> _exits;
protected Location(string name, string description, IEnumerable<IExit> exits)
{
_exits = exits.ToList();
}
public IEnumerable<IExit> Exits => _exits;
// ...other properties...
}
That lets you concentrate on the spatial relationships between the locations from a more natural direction (IMO and no pun intended). You'll likely find this to be more easily extensible down the road when you need to, say close or lock a door:
public interface Door : IExit
{
bool IsOpen { get; }
bool IsLocked { get; }
}
$endgroup$
add a comment |
$begingroup$
The other reviews covered many of the main points I'd raise, but there are a handful of others.
Constructors in abstract classes should be protected
, not public
:
public Location(string name) => Name = name;
You can't create a new instance of the abstract class, so it is for all intents and purposes protected
anyway. Make the access modifier match the behavior.
I'm not sure that I like some of the naming. For example, CreateObjects()
gives only the slightest clue as to what it is doing. I'd probably go with something more along the lines of GenerateMap()
. A couple of the member names are also a little ambiguous as to how they function - for example, which room is IHasExteriorDoor.DoorLocation
relative to?
Speaking of doors (keeping in mind that I'm not sure how much was proscribed by the exercise), I also think I find it a bit more natural to have the exits be the primary motive object. It's more natural to me to think of "using a door" than it is to think of "leaving a room". To me, a room is a static thing - it has exits, but you don't really use a room by leaving it. I'd consider building your map from the standpoint of the connections between the locations instead of the locations themselves. Something more like this:
public interface ILocation
{
string Name { get; }
string Description { get; }
}
public interface IExit
{
// TODO: my naming sucks too.
ILocation LocationOne { get; }
ILocation LocationTwo { get; }
// Takes the room you're exiting as a parameter, returns where you end up.
ILocation Traverse(ILocation from);
}
public abstract class Location : ILocation
{
private readonly IReadOnlyList<IExit> _exits;
protected Location(string name, string description, IEnumerable<IExit> exits)
{
_exits = exits.ToList();
}
public IEnumerable<IExit> Exits => _exits;
// ...other properties...
}
That lets you concentrate on the spatial relationships between the locations from a more natural direction (IMO and no pun intended). You'll likely find this to be more easily extensible down the road when you need to, say close or lock a door:
public interface Door : IExit
{
bool IsOpen { get; }
bool IsLocked { get; }
}
$endgroup$
The other reviews covered many of the main points I'd raise, but there are a handful of others.
Constructors in abstract classes should be protected
, not public
:
public Location(string name) => Name = name;
You can't create a new instance of the abstract class, so it is for all intents and purposes protected
anyway. Make the access modifier match the behavior.
I'm not sure that I like some of the naming. For example, CreateObjects()
gives only the slightest clue as to what it is doing. I'd probably go with something more along the lines of GenerateMap()
. A couple of the member names are also a little ambiguous as to how they function - for example, which room is IHasExteriorDoor.DoorLocation
relative to?
Speaking of doors (keeping in mind that I'm not sure how much was proscribed by the exercise), I also think I find it a bit more natural to have the exits be the primary motive object. It's more natural to me to think of "using a door" than it is to think of "leaving a room". To me, a room is a static thing - it has exits, but you don't really use a room by leaving it. I'd consider building your map from the standpoint of the connections between the locations instead of the locations themselves. Something more like this:
public interface ILocation
{
string Name { get; }
string Description { get; }
}
public interface IExit
{
// TODO: my naming sucks too.
ILocation LocationOne { get; }
ILocation LocationTwo { get; }
// Takes the room you're exiting as a parameter, returns where you end up.
ILocation Traverse(ILocation from);
}
public abstract class Location : ILocation
{
private readonly IReadOnlyList<IExit> _exits;
protected Location(string name, string description, IEnumerable<IExit> exits)
{
_exits = exits.ToList();
}
public IEnumerable<IExit> Exits => _exits;
// ...other properties...
}
That lets you concentrate on the spatial relationships between the locations from a more natural direction (IMO and no pun intended). You'll likely find this to be more easily extensible down the road when you need to, say close or lock a door:
public interface Door : IExit
{
bool IsOpen { get; }
bool IsLocked { get; }
}
answered Jan 17 at 23:55
CominternComintern
3,82711124
3,82711124
add a comment |
add a comment |
$begingroup$
In this code block here:
...
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects(); // <--- bleh
MoveToLocation(livingRoom);
}
This is call to the CreateObject()
method, is something I don't like to see in code (it could be a personal style issue) but if you are constructing an object, then all code related to the construction of an object should just stay in the constructor...
I would prefer that it ended up looking like
...
private readonly OutsideWithDoor _backYard; // now it can be readonly
private readonly Outside _garden;
public ExploreTheHouseForm()
{
InitializeComponent();
...
_backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
_garden = new Outside("garden", false);
MoveToLocation(livingRoom);
}
$endgroup$
1
$begingroup$
"all code related to the construction of an object should just stay in the constructor" I have the disagree with that. If it's a one-liner? Do it inline - no point creating helper methods. But for more complex initializations separating logically independent parts into their own methods keeps methods small, simple and self-documenting. Otherwise we end up with a giant constructor method that requires comments to separate the different parts. Now you certainly do want to keep things readonly, but the usual solution is to simply return the created object and assign it in the constructor.
$endgroup$
– Voo
Jan 19 at 13:20
add a comment |
$begingroup$
In this code block here:
...
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects(); // <--- bleh
MoveToLocation(livingRoom);
}
This is call to the CreateObject()
method, is something I don't like to see in code (it could be a personal style issue) but if you are constructing an object, then all code related to the construction of an object should just stay in the constructor...
I would prefer that it ended up looking like
...
private readonly OutsideWithDoor _backYard; // now it can be readonly
private readonly Outside _garden;
public ExploreTheHouseForm()
{
InitializeComponent();
...
_backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
_garden = new Outside("garden", false);
MoveToLocation(livingRoom);
}
$endgroup$
1
$begingroup$
"all code related to the construction of an object should just stay in the constructor" I have the disagree with that. If it's a one-liner? Do it inline - no point creating helper methods. But for more complex initializations separating logically independent parts into their own methods keeps methods small, simple and self-documenting. Otherwise we end up with a giant constructor method that requires comments to separate the different parts. Now you certainly do want to keep things readonly, but the usual solution is to simply return the created object and assign it in the constructor.
$endgroup$
– Voo
Jan 19 at 13:20
add a comment |
$begingroup$
In this code block here:
...
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects(); // <--- bleh
MoveToLocation(livingRoom);
}
This is call to the CreateObject()
method, is something I don't like to see in code (it could be a personal style issue) but if you are constructing an object, then all code related to the construction of an object should just stay in the constructor...
I would prefer that it ended up looking like
...
private readonly OutsideWithDoor _backYard; // now it can be readonly
private readonly Outside _garden;
public ExploreTheHouseForm()
{
InitializeComponent();
...
_backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
_garden = new Outside("garden", false);
MoveToLocation(livingRoom);
}
$endgroup$
In this code block here:
...
OutsideWithDoor backYard;
Outside garden;
public ExploreTheHouseForm()
{
InitializeComponent();
CreateObjects(); // <--- bleh
MoveToLocation(livingRoom);
}
This is call to the CreateObject()
method, is something I don't like to see in code (it could be a personal style issue) but if you are constructing an object, then all code related to the construction of an object should just stay in the constructor...
I would prefer that it ended up looking like
...
private readonly OutsideWithDoor _backYard; // now it can be readonly
private readonly Outside _garden;
public ExploreTheHouseForm()
{
InitializeComponent();
...
_backYard = new OutsideWithDoor("back yard", true, kitchen.DoorDescription);
_garden = new Outside("garden", false);
MoveToLocation(livingRoom);
}
answered Jan 17 at 16:32
SvekSvek
786212
786212
1
$begingroup$
"all code related to the construction of an object should just stay in the constructor" I have the disagree with that. If it's a one-liner? Do it inline - no point creating helper methods. But for more complex initializations separating logically independent parts into their own methods keeps methods small, simple and self-documenting. Otherwise we end up with a giant constructor method that requires comments to separate the different parts. Now you certainly do want to keep things readonly, but the usual solution is to simply return the created object and assign it in the constructor.
$endgroup$
– Voo
Jan 19 at 13:20
add a comment |
1
$begingroup$
"all code related to the construction of an object should just stay in the constructor" I have the disagree with that. If it's a one-liner? Do it inline - no point creating helper methods. But for more complex initializations separating logically independent parts into their own methods keeps methods small, simple and self-documenting. Otherwise we end up with a giant constructor method that requires comments to separate the different parts. Now you certainly do want to keep things readonly, but the usual solution is to simply return the created object and assign it in the constructor.
$endgroup$
– Voo
Jan 19 at 13:20
1
1
$begingroup$
"all code related to the construction of an object should just stay in the constructor" I have the disagree with that. If it's a one-liner? Do it inline - no point creating helper methods. But for more complex initializations separating logically independent parts into their own methods keeps methods small, simple and self-documenting. Otherwise we end up with a giant constructor method that requires comments to separate the different parts. Now you certainly do want to keep things readonly, but the usual solution is to simply return the created object and assign it in the constructor.
$endgroup$
– Voo
Jan 19 at 13:20
$begingroup$
"all code related to the construction of an object should just stay in the constructor" I have the disagree with that. If it's a one-liner? Do it inline - no point creating helper methods. But for more complex initializations separating logically independent parts into their own methods keeps methods small, simple and self-documenting. Otherwise we end up with a giant constructor method that requires comments to separate the different parts. Now you certainly do want to keep things readonly, but the usual solution is to simply return the created object and assign it in the constructor.
$endgroup$
– Voo
Jan 19 at 13:20
add a comment |
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f211662%2fhouse-plan-design-head-first-c%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
$begingroup$
The winforms screen gives the option to go through the exterior door, but whatever is behind the exterior door doesn't get listed in the available places? What happens if the kitchen had multiple exterior doors to multiple places? What if the entire house was just a kitchen?
$endgroup$
– Mast
Jan 17 at 15:15
4
$begingroup$
If you are interested in the general problem of how to write text-based adventures, I encourage you to look into
Inform7
, an exceedingly clever language for writing such adventures. If you're interested in learning about virtual machines for implementing such adventures, I did a series on my blog a few years ago about the Z-machine.$endgroup$
– Eric Lippert
Jan 17 at 22:21
3
$begingroup$
As a bonus, on a Z-machine code for this problem will occupy less than a kilobyte. You won't be able to compile the above code into a .NET assembly that takes up that much. (This is another way of saying "in Ye Olde Days we didn't have no interfaces and abstract classes". However, we were also much more likely to be eaten by a grue, so it all evens out in the end.)
$endgroup$
– Jeroen Mostert
Jan 18 at 13:36