Я делал некоторое чтение о шаблонах разработки и хотел некоторую перспективу. Рассмотрите следующее:
Dim objGruntWorker as IGruntWorker
if SomeCriteria then
objGruntWorker = new GoFor()
else if SomeOtherCriteria then
objGruntWorker = new Newb()
else if SomeCriteriaAndTheKitchenSink then
objGruntWorker = new CubeRat()
end if
objGruntWorker.GetBreakfast()
system.threading.thread.sleep(GetMilliSecondsFromHours(4))
objGruntWorker.GetLunch()
Вышеупомянутый код растет каждый раз возникают, новые Критерии. Я видел, что код как это повсеместно и в незнании записал часть его сам. Как это должно быть решено? Этот вид антишаблона имеет более "формальное" имя?Спасибо за помощь!
Править: Другое соображение, я хочу избежать необходимости перекомпилировать существующие реализации IGruntWorker
просто добавить новую реализацию.
Если вы используете .NET, вы можете вместо этого построить его с помощью отражения. Например, если вы создавали систему плагинов, у вас была бы папка для размещения библиотек DLL плагинов. Затем ваша фабрика будет проверять доступные библиотеки DLL, проверять каждую на наличие соответствующих атрибутов отражения, а затем сопоставлять эти атрибуты с любой строкой, которая была передана, чтобы решить, какой объект выбрать и вызвать.
Это избавляет вас от необходимости перекомпилировать основное приложение, хотя вам придется создавать своих рабочих процессов в других библиотеках DLL, а затем иметь способ указать своей фабрике, какую из них использовать.
Вот очень быстрый и грязный псевдокод, чтобы понять суть:
Предполагая, что у вас есть сборка DLL с именем Workers.DLL
Настройте атрибут с именем WorkerTypeAttribute со строковым свойством с именем Name и конструктором для иметь возможность установить это свойство Name.
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class WorkerTypeAttribute : Attribute
{
string _name;
public string Name { get { return _name; } }
public WorkerTypeAttribute(string Name)
{
_name = Name;
}
}
Затем вы примените этот атрибут к любому рабочему классу, который вы определили, например:
[WorkerType("CogWorker")]
public class CogWorker : WorkerBase {}
Затем в рабочей фабрике вашего приложения вы должны написать такой код:
public void WorkerFactory(string WorkerType)
{
Assembly workers = Assembly.LoadFile("Workers.dll");
foreach (Type wt in workers.GetTypes())
{
WorkerTypeAttribute[] was = (WorkerTypeAttribute[])wt.GetCustomAttributes(typeof(WorkerTypeAttribute), true);
if (was.Count() == 1)
{
if (was[0].Name == WorkerType)
{
// Invoke the worker and do whatever to it here.
}
}
}
}
Я уверен, что есть другие примеры того, как чтобы сделать это там, но если вам нужны дополнительные указатели, дайте мне знать. Ключ в том, что у всех ваших воркеров должен быть общий родительский элемент или интерфейс, чтобы вы могли вызывать их одинаково. (То есть всем вашим воркерам нужен общий метод Execute или что-то, что может вызываться из фабрики или где бы вы ни использовали объект.
Тип шаблона, который подходит для вышеприведенного решения, будет Factory Pattern . У вас есть ситуация, когда вам не нужно знать конкретный тип объекта, который вам нужен, просто необходимо реализовать IGruntWorker
. Итак, вы создаете фабрику, которая принимает критерии и на основе этих критериев возвращает конкретный объект IGruntWorker
. Обычно рекомендуется сопоставить критерии с некоторым идентификатором, например, перечислением или константой для удобства чтения, например
public enum WorkerType
{
Newbie,
Average,
Expert
}
public class WorkerFactory
{
public static IGruntWorker GetWorker(WorkerType type)
{
switch (type)
{
case WorkerType.Newbie:
return new NewbieWorker();
case WorkerType.Average:
return new AverageWorker();
case WorkerType.Expert:
return new ExpertWorker();
}
}
}
Итак, в вашем случае у вас может быть небольшой вспомогательный метод, который подбирает нужный тип Worker'а на основе критериев. Это можно даже заключить в свойство только для чтения, которое вы просто передаете в фабрику.
Если вы можете определить объект с методом checkCriteria
, то вы можете сделать этот код управляемым таблицей. Я не знаю C#, так что потерпите меня с синтаксисом:
public class WorkerFactory {
IGruntWorker makeWorkerIfCriteria(criteria_parameters parms);
}
extern WorkerFactory worker_factories[]; /* table with factories in order */
IGruntWorker makeJustTheRightWorker(criteria_parameters actual_critera) {
for (i = 0; i < worker_factories.length(); i++) {
IGruntWorwer w = worker_factories[i].makeWorker(actual_criteria);
if (!null(w)) return w;
}
--- grim error --- /* table not initiailized correctly */
}
Тогда некоторые объекты в таблице будут выглядеть так
public class MakeGoFor(critera_parameters cp) {
if SomeCriteria then
return new GoFor();
else
return NULL;
}
Вы можете перекомпилировать таблицу в отдельный модуль без необходимости перекомпилировать код выбора. На самом деле, если вы будете амбициозны, вы даже можете построить таблицу во время выполнения на основе аргументов командной строки или содержимого файла...
Я думаю, что этот шаблон хорош, пока ваши критерии и операции представляют собой отдельные строки/вызовы методов. Это легко читается и точно отражает вашу логику:
if (ConditionOne())
{
BuildTheWidget();
}
else if (ConditionTwo())
{
RaiseTheAlarm();
}
else if (ConditionThree())
{
EverybodyGetsARaise();
}
Даже если есть 20 различных условий, это, вероятно, является точным отражением некоторой сложной бизнес-логики вашего приложения.
С другой стороны, это катастрофа для читабельности
if ( ((A && B) || C &&
(D == F) || (F == A)))
{
AA;
BB;
//200 lines of code
}
else if ( (A || D) && B)
{
// 200 more lines
}
Я знаю, что у вас .NET, но вот как я делаю нечто подобное в веб-приложении Java, где мои "if-thens" росли.... все еще требует перекомпиляции, но легко добавить другие действия или в вашем случае работников grunt.
private HashMap actionMap = new HashMap();
actionMap.put("cubeRat", new CubeRatAction());
actionMap.put("newb", new NewbAction());
actionMap.put("goFor", new goForAction());
actionMap.put("other", new otherAction());
String op = request.getParameter("criteria"); // not sure how your criteria is passed in but this is through a parameter in my URL.
ControllerAction action = (ControllerAction) actionMap.get(op);
if (action != null) {
action.GetBreakfast();
action.Sleep();
action.GetLunch();
} else {
String url = "views/errorMessage_v.jsp";
String errMessage = "Operation '" + op + "' not valid for in '" + request.getServletPath() + "' !!";
request.setAttribute("message", errMessage);
request.getRequestDispatcher(url).forward(request, response);
}
Вы можете использовать отражение, чтобы найти конструктор заданного типа и создать экземпляр с помощью конструктора. Конечно, конструкторы должны следовать определенному шаблону. В приведенном выше примере все конструкторы по умолчанию.
Я думаю, многое зависит от того, насколько предсказуемы ваши "условия". Ваш "растущий IF" по сути является фабрикой, и, возможно, рефакторинг его в собственный метод или класс поможет, но это все равно может быть растущим IF. Если ваши условия - это то, что вы не можете предсказать, например, "если Джо.горит" или "если x==2" или "если !шаттл.запущен", то вы застряли с IF'ами.
Одно плохое свойство этих супер-IF'ов заключается в том, что они могут распространяться на ваше приложение. То есть, что вам нужно вызвать/потрогать/проверить, чтобы определить, какое "if" должно быть истинным? В итоге вы можете получить тонны глобального мусора или множество параметров, которые нужно передать вашей "фабрике". Некоторое время назад я сделал одну вещь, которая помогла решить эту проблему: создал своего рода фабрику, содержащую массив булевых делегатов (Func) и типов. Я регистрировал булевы делегаты и типы во время инициализации и просматривал список в фабрике, вызывая каждый делегат, пока не получал "true", а затем инстанцировал этот тип. Это хорошо работало для меня, потому что я мог "регистрировать" новые условия без редактирования фабрики.
Просто идея
Подобная логика часто инкапсулируется с использованием шаблона метода Factory . (См. Пример ImageReaderFactory в разделе Инкапсуляция .)
Не могли бы вы вместо этого использовать вариант шаблона посетителя? Назовите это посетителем фабрики (возможно)
извините за псевдокод, но мой VB ржавый
Dim objGruntWorker as IGruntWorker
objGruntWorker = null
// all your objects implement IFactoryVisitor
Dim factory as IFactoryVisitor
while objGruntWorker == null
factory = factoryCollection.GetNext
objGruntWorker = factory.TryBuild(...)
end
objGruntWorker.GetBreakfast()
system.threading.thread.sleep(GetMilliSecondsFromHours(4))
objGruntWorker.GetLunch()
Вы можете создавать фабрики для каждого типа объекта, и эти фабрики могут иметь функцию, которая принимает критерии в качестве параметра и возвращает IGruntWorker, если параметры удовлетворены (или null в противном случае).
Затем вы можете создать список этих фабрик и просмотреть их в цикле, например (извините, я с # парень):
Dim o as IGruntWorker;
foreach (IGruntWorkerFactory f in factories)
{
o = f.Create(criterias);
if (o != null)
break;
}
Когда нужен новый критерий, вы только добавляете его в список фабрик, не нужно изменить цикл.
Есть, наверное, еще более красивые способы
Мои 2 цента
Я думаю, пока ваши наиболее вероятные критерии упорядочены первыми, чтобы позволить среде выполнения перескочить с остальных случаев, это нормально.
Если вас интересует только удобочитаемость, вы можете использовать тернарный оператор или, если оценки критериев равны ==, вы можете использовать оператор switch.