这不是一个简单的任务,所以我要做的第一件事就是定义一个接口来看看我在这里需要什么:
interface IViolationFinder
{
IEnumerable<Violation> Search(
IEnumerable<RuleDefinition> ruleDefinitions,
IEnumerable<Request> requests);
}
现在我们清楚地看到我们需要实现什么。因为您的搜索逻辑非常复杂,我认为您不应该使用单个 linq 来表达它。你可以,但你不应该。嵌入了 linq 的两个嵌套 foreach 循环非常讨厌,我认为使用 linq 本身不会更干净。
你需要的是在你的实现中创建更多的方法。它将增加可读性。所以天真的实现是这样的(这是你的):
class ViolationFinder : IViolationFinder
{
public IEnumerable<Violation> Search(IEnumerable<RuleDefinition> ruleDefinitions, IEnumerable<Request> requests)
{
var violations = new List<Violation>();
foreach (var rule in ruleDefinitions)
{
var requestsMatchingType = requests.Where(r => r.TypeOfThisRequest == rule.ConcerningRequestType);
foreach (var firstRequest in requestsMatchingType)
{
var collidingRequest = requests.FirstOrDefault(secondRequest =>
secondRequest.TimeIndex > firstRequest.TimeIndex &&
Math.Abs(secondRequest.TimeIndex - firstRequest.TimeIndex) < rule.MinimumDistanceBetweenRequests);
if (collidingRequest != null)
{
violations.Add(new Violation
{
ViolatedRule = rule,
FirstRequest = firstRequest,
SecondRequest = collidingRequest
});
}
}
}
return violations;
}
}
你可以开始重构这个。让我们提取最明显的部分,而不是用一种方法思考:
class ViolationFinder : IViolationFinder
{
public IEnumerable<Violation> Search(IEnumerable<RuleDefinition> ruleDefinitions, IEnumerable<Request> requests)
{
var violations = new List<Violation>();
foreach (RuleDefinition rule in ruleDefinitions)
{
IEnumerable<Request> requestsMatchingType = requests.Where(r => r.TypeOfThisRequest == rule.ConcerningRequestType);
violations.AddRange(
FindViolationsInRequests(requestsMatchingType, requests, rule));
}
return violations;
}
private IEnumerable<Violation> FindViolationsInRequests(
IEnumerable<Request> matchingRequests,
IEnumerable<Request> allRequest,
RuleDefinition rule)
{
foreach (Request firstRequest in matchingRequests)
{
var collidingRequest = allRequest.FirstOrDefault(secondRequest =>
secondRequest.TimeIndex > firstRequest.TimeIndex &&
Math.Abs(secondRequest.TimeIndex - firstRequest.TimeIndex) < rule.MinimumDistanceBetweenRequests);
if (collidingRequest != null)
{
yield return new Violation
{
ViolatedRule = rule,
FirstRequest = firstRequest,
SecondRequest = collidingRequest
};
}
}
}
}
搜索几乎是干净的,但我们看到 FindViolationsInRequests 获取每个请求和规则,因此传递过滤的请求列表是毫无用处的。所以我们这样做:
class ViolationFinder : IViolationFinder
{
public IEnumerable<Violation> Search(IEnumerable<RuleDefinition> ruleDefinitions, IEnumerable<Request> requests)
{
var violations = new List<Violation>();
foreach (RuleDefinition rule in ruleDefinitions)
{
violations.AddRange(FindViolationsInRequests(requests, rule));
}
return violations;
}
private IEnumerable<Violation> FindViolationsInRequests(
IEnumerable<Request> allRequest,
RuleDefinition rule)
{
foreach (Request firstRequest in FindMatchingRequests(allRequest, rule))
{
var collidingRequest = allRequest.FirstOrDefault(secondRequest =>
secondRequest.TimeIndex > firstRequest.TimeIndex &&
Math.Abs(secondRequest.TimeIndex - firstRequest.TimeIndex) < rule.MinimumDistanceBetweenRequests);
if (collidingRequest != null)
{
yield return new Violation
{
ViolatedRule = rule,
FirstRequest = firstRequest,
SecondRequest = collidingRequest
};
}
}
}
private IEnumerable<Request> FindMatchingRequests(IEnumerable<Request> requests, RuleDefinition rule)
{
return requests.Where(r => r.TypeOfThisRequest == rule.ConcerningRequestType);
}
}
现在接下来的事情是
var collidingRequest = allRequest.FirstOrDefault(secondRequest =>
secondRequest.TimeIndex > firstRequest.TimeIndex &&
Math.Abs(secondRequest.TimeIndex - firstRequest.TimeIndex) < rule.MinimumDistanceBetweenRequests);
足够复杂,可以为它制定一些方法:
class ViolationFinder : IViolationFinder
{
public IEnumerable<Violation> Search(IEnumerable<RuleDefinition> ruleDefinitions, IEnumerable<Request> requests)
{
var violations = new List<Violation>();
foreach (RuleDefinition rule in ruleDefinitions)
{
violations.AddRange(FindViolationsInRequests(requests, rule));
}
return violations;
}
private IEnumerable<Violation> FindViolationsInRequests(
IEnumerable<Request> allRequest,
RuleDefinition rule)
{
foreach (Request firstRequest in FindMatchingRequests(allRequest, rule))
{
Request collidingRequest = FindCollidingRequest(allRequest, firstRequest, rule.MinimumDistanceBetweenRequests);
if (collidingRequest != null)
{
yield return new Violation
{
ViolatedRule = rule,
FirstRequest = firstRequest,
SecondRequest = collidingRequest
};
}
}
}
private IEnumerable<Request> FindMatchingRequests(IEnumerable<Request> requests, RuleDefinition rule)
{
return requests.Where(r => r.TypeOfThisRequest == rule.ConcerningRequestType);
}
private Request FindCollidingRequest(IEnumerable<Request> requests, Request firstRequest, int minimumDistanceBetweenRequests)
{
return requests.FirstOrDefault(secondRequest => IsCollidingRequest(firstRequest, secondRequest, minimumDistanceBetweenRequests));
}
private bool IsCollidingRequest(Request firstRequest, Request secondRequest, int minimumDistanceBetweenRequests)
{
return secondRequest.TimeIndex > firstRequest.TimeIndex &&
Math.Abs(secondRequest.TimeIndex - firstRequest.TimeIndex) < minimumDistanceBetweenRequests;
}
}
好的,越来越干净了。我几乎可以很容易地说出每种方法的目的。再做一点工作,你最终会得到这样的结果:
class ViolationFinder : IViolationFinder
{
public IEnumerable<Violation> Search(IEnumerable<RuleDefinition> ruleDefinitions, IEnumerable<Request> requests)
{
List<Request> requestList = requests.ToList();
return ruleDefinitions.SelectMany(rule => FindViolationsInRequests(requestList, rule));
}
private IEnumerable<Violation> FindViolationsInRequests(IEnumerable<Request> allRequest, RuleDefinition rule)
{
return FindMatchingRequests(allRequest, rule)
.Select(firstRequest => FindSingleViolation(allRequest, firstRequest, rule))
.Where(violation => violation != null);
}
private Violation FindSingleViolation(IEnumerable<Request> allRequest, Request request, RuleDefinition rule)
{
Request collidingRequest = FindCollidingRequest(allRequest, request, rule.MinimumDistanceBetweenRequests);
if (collidingRequest != null)
{
return new Violation
{
ViolatedRule = rule,
FirstRequest = request,
SecondRequest = collidingRequest
};
}
return null;
}
private IEnumerable<Request> FindMatchingRequests(IEnumerable<Request> requests, RuleDefinition rule)
{
return requests.Where(r => r.TypeOfThisRequest == rule.ConcerningRequestType);
}
private Request FindCollidingRequest(IEnumerable<Request> requests, Request firstRequest, int minimumDistanceBetweenRequests)
{
return requests.FirstOrDefault(secondRequest => IsCollidingRequest(firstRequest, secondRequest, minimumDistanceBetweenRequests));
}
private bool IsCollidingRequest(Request firstRequest, Request secondRequest, int minimumDistanceBetweenRequests)
{
return secondRequest.TimeIndex > firstRequest.TimeIndex &&
Math.Abs(secondRequest.TimeIndex - firstRequest.TimeIndex) < minimumDistanceBetweenRequests;
}
}
请注意,单一责任原则也适用于方法。除了 Search 方法之外,一切都是私有实现的一部分,但正如您所见,每个处理部分都有一个带有名称的方法。每种方法都有其单一职责,因此您可以更轻松地阅读实现。
- 搜索(公众)
- FindViolationsInRequests
- FindSingleViolation
- 查找匹配请求
- 查找碰撞请求
- IsCollidingRequest
这些是此实现的单元。
如果您为原始实现编写单元测试并且只有在此之后才开始重构,重构过程会更加安全。然后你总是知道你不会打破你的逻辑。如果您针对第一个变体(当我将您的完整代码放入 Search 方法时)编写单元测试,那么您的单元测试将是可以的,因此针对接口。
另一个小但重要的部分是:
public IEnumerable<Violation> Search(IEnumerable<RuleDefinition> ruleDefinitions, IEnumerable<Request> requests)
{
List<Request> requestList = requests.ToList();
return ruleDefinitions.SelectMany(rule => FindViolationsInRequests(requestList, rule));
}
我从项目中列出一个列表,所以我绝对确定我不会多次枚举 IEnumerable(这可能会导致某些实现出现问题,请考虑 IQueryable)。