Как мне справиться с двумя ситуациями, которые могут быть кандидатами для решения по шаблону стратегии? - PullRequest
0 голосов
/ 05 июня 2019

Я проектирую клиента, который будет вызывать методы на основе определенных входных данных. Я буду отправлять в перечисление биллинговой системы и вызывать конечную точку, чтобы определить, какая биллинговая система подходит для существующего пациента. Как только я получу биллинговую систему, я должен проверить, какой тип операции мне нужно выполнить, и выполнить вызов API на основе биллинговой системы.

Например, если мне нужно обновить запись пациента, а пациент находится в BillingSystemA, мне нужно вызвать метод API для BillingSystemA на основе PUT.

Мне нужно иметь методы CRUD для каждой биллинговой системы.

Выбор между двумя биллинговыми системами и возможность будущего роста заставили меня подумать, что шаблон стратегии хорошо подходит. Кажется, стратегия работает для биллинговой системы, но как быть с операциями CRUD?

У меня есть BillingStrategy абстрактный класс, который имеет методы Create, Update, Get и Delete, но мне нужны эти методы для работы с различными типами. Могу ли я просто сделать методы общими, например T Create<T> или bool Update<T>, или мне нужна стратегия в рамках стратегии для управления этим? Я проанализировал себя в углу и мог бы использовать несколько советов.

1 Ответ

0 голосов
/ 05 июня 2019

Вот грубая иллюстрация. Я придумал много специфики, а названия не так уж хороши. Я склонен пересматривать имена по мере рефакторинга. Суть в том, чтобы проиллюстрировать, как можно разбить проблему на части.

Предполагается, что существуют классы для Patient и Treatment и перечисление для InsuranceType. Цель состоит в том, чтобы выставить пациенту счет за лечение и определить, куда отправить счет на основании страховки пациента.

Вот класс:

public class PatientBilling
{
    private readonly IBillingHandlerByInsuranceSelector _billingHandlerSelector;
    private readonly IBillingHandler _directPatientBilling;

    public PatientBilling(
        IBillingHandlerByInsuranceSelector billingHandlerSelector, 
        IBillingHandler directPatientBilling)
    {
        _billingHandlerSelector = billingHandlerSelector;
        _directPatientBilling = directPatientBilling;
    }

    public void BillPatientForTreatment(Patient patient, Treatment treatment)
    {
        var billingHandler = _billingHandlerSelector.GetBillingHandler(patient.Insurance);
        var result = billingHandler.BillSomeone(patient, treatment);
        if (!result.Accepted)
        {
            _directPatientBilling.BillSomeone(patient, treatment);
        }
    }
}

и несколько интерфейсов:

public interface IBillingHandler
{
    BillSomeoneResult BillSomeone(Patient patient, Treatment treatment);
}

public interface IBillingHandlerByInsuranceSelector
{
    IBillingHandler GetBillingHandler(InsuranceType insurance);
}

Как вы можете видеть, это сильно зависит от внедрения зависимостей. Этот класс прост, потому что он вообще ничего не знает о различных видах страхования.

Все, что он делает, это

  • Выбор обработчика счетов в зависимости от типа страховки
  • попробуйте выставить счет на страхование
  • если отклонено, выставьте счет пациенту

Это не знает или не волнует, как реализован какой-либо из этих счетов. Это может быть вызов базы данных, вызов API или что-то еще. Это делает этот класс очень простым для чтения и тестирования. Мы отложили все, что не связано с этим классом. Это облегчит решение будущих проблем по одному.

Реализация IBillingHandlerByInsuranceSelector может быть абстрактной фабрикой, которая будет создавать и возвращать правильную реализацию IBillingHandler в соответствии со страховкой пациента. (Я замалчиваю это, но есть много информации о том, как создавать абстрактные фабрики с контейнерами внедрения зависимостей.)

В некотором смысле мы могли бы сказать, что первая часть этой проблемы решена (хотя мы, вероятно, еще немного реорганизовали бы ее). Причина в том, что мы можем написать для нее модульные тесты и любую работу, специфичную для один тип страхования или другой будет в разных классах.

Далее мы можем написать эти страховые реализации. Предположим, одним из видов страхования является WhyCo, и теперь нам нужно создать для них IBillingHandler. По сути, мы собираемся повторить тот же процесс.

Для иллюстрации скажем, что отправка счета в WhyCo выполняется в два этапа. Сначала мы должны сделать запрос, чтобы проверить право, а затем мы должны представить счет. Может быть, другие страховые API делают это за один шаг. Это нормально, потому что никакие две реализации не должны иметь ничего общего друг с другом. Они просто реализуют интерфейс.

На данный момент мы имеем дело со спецификой одной конкретной страховой компании, поэтому где-то здесь нам нужно преобразовать нашу Patient и Treatment информацию в любые данные, которые они ожидают получить.

public class WhyCoBillingHandler : IBillingHandler
{
    private readonly IWhyCoService _whyCoService;

    public WhyCoBillingHandler(IWhyCoService whyCoService)
    {
        _whyCoService = whyCoService;
    }

    public BillSomeoneResult BillSomeone(Patient patient, Treatment treatment)
    {
        // populate this from the patient and treatment
        WhyCoEligibilityRequest eligibilityRequest = ...;
        var elibility = _whyCoService.CheckEligibility(eligibilityRequest);
        if(!elibility.IsEligible)
            return new BillSomeoneResult(false, elibility.Reason);

        // create the bill
        WhyCoBillSubmission bill = ...;
        _whyCoService.SubmitBill(bill);
        return new BillSomeoneResult(true);
    }
}

public interface IWhyCoService
{
    WhyCoEligibilityResponse CheckEligibility(WhyCoEligibilityRequest request);
    void SubmitBill(WhyCoBillSubmission bill);
}

На данный момент мы еще не написали ни одного кода, который взаимодействует с API WhyCo. Это облегчает юнит-тестирование WhyCoBillingHandler. Теперь мы можем написать реализацию IWhyCoService, которая вызывает фактический API. Мы можем написать модульные тесты для WhyCoBillingHandler и интеграционные тесты для реализации IWhyCoService.

(Возможно, было бы лучше, если бы перевод наших Patient и Treatment данных в то, что они ожидали, произошел еще ближе к конкретной реализации.)

На каждом шаге мы пишем части кода, тестируем их и откладываем части на потом. Класс API может быть последним шагом в реализации биллинга WhyCo. Тогда мы можем перейти к следующей страховой компании.

На каждом шаге мы также решаем, сколько должно идти в каждый класс. Предположим, нам нужно написать приватный метод, и этот метод оказывается настолько сложным, что он больше, чем открытый метод, который его вызывает, и его сложно протестировать. В этом случае мы можем заменить этот закрытый метод другой зависимостью (абстракцией), которую мы внедряем в класс.

Или мы могли бы заранее понять, что некоторые новые функциональные возможности должны быть разделены на его собственный класс, и мы можем просто начать с этого.


Причина, по которой я проиллюстрировал это следующим образом:

Я проанализировал себя в углу

Легко стать парализованным, когда наш код должен делать так много вещей.Это помогает избежать паралича, потому что оно постоянно дает нам путь вперед.Мы пишем часть этого, чтобы зависеть от абстракций, и затем эта часть сделана (вроде.) Затем мы реализуем эти абстракции.Реализации требуют большего количества абстракций, и мы повторяем (написание модульных тестов до конца.)

Это не навязывает лучшие практики и принципы, но мягко ведет нас к ним.Мы пишем небольшие классы с одиночной ответственностью.Они зависят от абстракций.Мы определяем эти абстракции (в данном случае интерфейсы) с точки зрения классов, которые в них нуждаются, что приводит к разделению интерфейса.И каждый класс прост для модульного тестирования.

Некоторые отмечают, что легко увлечься всеми абстракциями и создать слишком много интерфейсов и слишком много уровней абстракции, и они верны.Но это нормально.На каждом шагу мы можем немного потерять равновесие, так или иначе.

Как видите, проблемы, возникающие, когда нам приходится иметь дело с разницей между биллинговыми системами, становятся проще.Мы просто создаем каждую реализацию по-разному.

Стратегия, кажется, работает для биллинговой системы, но как насчет операций CRUD?

Тот факт, что все они имеют разные операции CRUDЭто хорошо.Мы сделали компоненты похожими там, где они должны быть похожими (интерфейсы, через которые мы взаимодействуем с ними), но внутренние реализации могут быть настолько разными, насколько это необходимо.

Мы также обошли вопроскакие шаблоны проектирования использовать, за исключением того, что IBillingHandlerByInsuranceSelector является абстрактной фабрикой.Это тоже хорошо, потому что мы не хотим начинать с того, чтобы сосредоточиться на шаблоне дизайна.Если бы это было реальное приложение, я бы предположил, что многое из того, что я делаю, нужно будет реорганизовать.Так как классы небольшие, тестируются модульно и зависят от абстракций, проще представить шаблоны проектирования, когда их использование станет очевидным.Когда это произойдет, мы можем выделить их для классов, которые в них нуждаются.Или, если мы только что поняли, что мы пошли не в том направлении, все еще проще провести рефакторинг.Если только мы не увидим будущего, которое наверняка произойдет.

Стоит потратить некоторое время на предварительное изучение различных деталей реализации, чтобы убедиться, что способ работы с ними согласуется с кодом, который вы используете.пишу.(Другими словами, нам все еще нужно потратить некоторое время на разработку.) Например, если ваши поставщики счетов не дают вам немедленных ответов - вам приходится ждать часы или дни - тогда код, который моделирует его как немедленный ответ, не будет 'Это не имеет смысла.

Еще одно преимущество заключается в том, что это помогает вам работать параллельно с другими разработчиками.Если вы определили, что данная абстракция является хорошим началом для ваших страховых компаний, и, возможно, вы написали несколько, то легко передать другие разработчикам.Им не нужно думать обо всей системе.Они могут просто написать реализацию интерфейса, создавая любые необходимые им зависимости.

...