Is there any possibility of refactoring this code?
In class A:
public List<Item> GetItems() {
var result = new List<Item>();
foreach(var item in repo.GetItems1()) {
var x = repo.GetOtherItems1(item.Id, "param1", param2); // this part is different
if (x.Value > 5)
result.Add(x);
}
return result;
}
In class B:
public List<Item> GetItems() {
var result = new List<Item>();
foreach(var item in repo.GetItems2()) {
var x = repo.GetOtherItems2(param1, param2, item.Id); // this part is different
if (x.Value > 5)
result.Add(x);
}
return result;
}
I tried to use template method, but due to other params in GetOtherItemsX(...)
, is it now possible?
-
\$\begingroup\$ Is it the same repository type you are work with in both classes? Otherwise it doesn't make much sense to try and re-work. \$\endgroup\$WestDiscGolf– WestDiscGolf2011年12月12日 11:04:27 +00:00Commented Dec 12, 2011 at 11:04
4 Answers 4
Well, you could receive a delegate for getting the elements - that is, to make the action that is different:
public List<Item> GetItems(Func<Item, Repository> getOtherItems) {
if( getOtherItems == null ) {
throw new ArgumentException();
}
var result = new List<Item>();
foreach(var item in repo.GetItems2()) {
var x = getOtherItems();
if (x.Value > 5)
result.Add(x);
}
return result;
}
And then you could call it like so:
// In class A
this.GetItems( repo => repo.GetOtherItems1(item.Id, "param1", param2) );
// In class B
this.GetItems( repo => repo.GetOtherItems2(param1, param2, item.Id) );
But I notice that you also use repo.GetItems1()
in A and repo.GetItems2()
in B.
So the difference you pointed out is not the only one.
With this in mind, I would advise rethinking your class structure. These two could inherit from the same base class, and merely extend it. (Or it could be polymorphism, or...)
Maybe:
public interface IMyList<Item>
{
List<int> GetItems();
// If it returns a single item, the name should NOT be pluralized!!
Item GetOtherItems();
}
Or maybe an abstract class?
edit: read @JoeGeeky 's comment on the performance impact of using delegates; this may become relevant if the delegate is used on a very intensive cycle or under high loads.
-
\$\begingroup\$ I like the idea of a base abstract class which has the GetItems method and then the classes which implement it can pass in the desired Func<>. \$\endgroup\$WestDiscGolf– WestDiscGolf2011年12月12日 12:58:07 +00:00Commented Dec 12, 2011 at 12:58
-
\$\begingroup\$ These are not bad ideas but will come with a performance penalty... You didn't mention this as an issue, but it's worth considering. \$\endgroup\$JoeGeeky– JoeGeeky2011年12月17日 23:11:12 +00:00Commented Dec 17, 2011 at 23:11
-
\$\begingroup\$ @JoeGeeky What comes with performance penalty, using the delegate? Would this penalty be addressed by instead deriving from an interface or abstract class? \$\endgroup\$ANeves– ANeves2011年12月19日 10:04:36 +00:00Commented Dec 19, 2011 at 10:04
-
1\$\begingroup\$ Invoking a method (especially an inlined method) is substantially cheaper then invoking a delegate chain with one subscribed member. By way of example... invoking an inlined instance method costs ~0.2ns while invoking a delegate costs ~41ns. The numbers will vary depending on your hardware but the relative difference will essentially remain the same. Func<> and Action<> invokations have very similar costs. If this is occuring in tight loops or high loads, this can start to add up. \$\endgroup\$JoeGeeky– JoeGeeky2011年12月19日 10:17:20 +00:00Commented Dec 19, 2011 at 10:17
I would make a couple suggestions:
- Consider making the return type of your GetItems methods
IEnumerable<Item>
,ICollection<Item>
, orIList<Item>
, depending on how you intend to use the result of your function. This will make refactoring later easier. - Consider renaming your
repo.GetOtherItemsN
methods. Their names suggest that multiple items are being returned rather than a single item, but they appear to be returning single items. (ANeves mentions this in the comments for his interface) - Consider updating the signature of both your
repo.GetOtherItemsN
methods to take in parameters in a similar order. It seems strange that one takes inId
, thenparam1
(as a string), thenparam2
, while the other takes inparam1
(unknown type), thenparam2
, thenId
. Id should probably come first. - Consider using a constant/enum value in place of 5 within your if (x.Value> 5) statements. If both classes A and B have a common base class (suggested, given the other answers), I would put it there.
- Building on ANeves answer, I would also pass in an
IEnumerable<Item>
parameter to theGetItems
function, since you userepo.GetItems1
versusrepo.GetItems2
in the two classes and you do not do anything more involved than enumerating the collection.
If you are a LINQ-fan, you could also transform this to a LINQ statement:
public IEnumerable<Item> GetItems(IEnumerable<Items> source, Func<Item, Repository> getOtherItems)
{
if (getOtherItems == null)
{
throw new ArgumentException();
}
var result = from item in source
let x = getOtherItems()
where x.Value > MIN_VAL
select x;
}
If you need it evaluated early rather than lazy-loaded, you just need to toss on a .ToList ()
or .ToArray()
before returning.
After taking Dan and the others suggestions for cleaning up the repository interface, you could also have some benefit from using a common expression for a Linq where statement.
[TestMethod]
public void CanUseCommonWhereExpression()
{
var repo = SetupRepo();
Assert.AreEqual(1, GetItems1(repo).Where(ValueMoreThan(5)).Count());
Assert.AreEqual(2, GetItems2(repo).Where(ValueMoreThan(5)).Count());
Assert.AreEqual(
2,
GetItems2(repo).AsQueryable().Where(QueryableValueMoreThan(5)).Count());
}
// Fine for in-memory linq
private Func<Item, bool> ValueMoreThan(int value)
{
return i => i.Value > value;
}
// Also fine for database query translation
private Expression<Func<Item, bool>> QueryableValueMoreThan(int value)
{
return i => i.Value > value;
}
private static IEnumerable<Item> GetItems2(IRepository repo)
{
return repo.GetItems2().SelectMany(i => repo.GetOtherItems2(i.Id, 0));
}
private static IEnumerable<Item> GetItems1(IRepository repo)
{
return repo.GetItems1().SelectMany(i => repo.GetOtherItems1(0, i.Id));
}
private static IRepository SetupRepo()
{
var repo = MockRepository.GenerateMock<IRepository>();
repo.Stub(r => r.GetItems1()).Return(new[] { new Item { Id = 1, Value = 3 } });
repo.Stub(
r => r.GetOtherItems1(0, 1)).Return(new[] { new Item { Id = 2, Value = 3 },
new Item { Id = 3, Value = 6 } });
repo.Stub(r => r.GetItems2()).Return(new[] { new Item { Id = 4, Value = 3 } });
repo.Stub(
r => r.GetOtherItems2(4, 0)).Return(new[] { new Item { Id = 5, Value = 6 },
new Item { Id = 6, Value = 7 } });
return repo;
}
public interface IRepository
{
IEnumerable<Item> GetItems1();
IEnumerable<Item> GetOtherItems1(int foo, int id);
IEnumerable<Item> GetItems2();
IEnumerable<Item> GetOtherItems2(int id, int bar);
}
public class Item
{
public int Id { get; set; }
public int Value { get; set; }
}
-
\$\begingroup\$ Welcome to the Code Review, which is part of the Stack Exchange network. A network wide convention is not to add greetings or footers in your posts. Please refrain from doing so in the future. Thanks. \$\endgroup\$Steven Jeuris– Steven Jeuris2011年12月13日 13:39:09 +00:00Commented Dec 13, 2011 at 13:39
Create a class that holds your parameters and instead of passing individual parameters, pass in the class object. something like:
class MyClass
{
public MyClass(){}
public int ItemId { get; set;}
public string ItemParam1 {get; set;}
....
}
-
7\$\begingroup\$ This could be made cleaner by using auto properties. \$\endgroup\$WestDiscGolf– WestDiscGolf2011年12月12日 11:22:16 +00:00Commented Dec 12, 2011 at 11:22