Как мне смоделировать мой код, чтобы максимизировать повторное использование кода в этой конкретной ситуации?
Обновлено: см. Конец вопроса о том, как я реализовал решение.
Извините за плохо сформулированный вопрос, но я не был уверен, как лучше его задать. Я не уверен, как спроектировать решение, которое можно будет повторно использовать, когда большая часть кода будет одинаковой при каждой реализации, но часть реализации будет меняться каждый раз, но следовать аналогичным шаблонам. Я пытаюсь избежать копирования и вставки кода.
У нас есть внутренняя система обмена данными для обновления таблиц в базах данных на разных машинах. Мы расширяем нашу службу обмена сообщениями для отправки данных внешним поставщикам, и я хочу написать простое решение, которое можно использовать повторно, если мы решим отправить данные более чем одному поставщику. Код будет скомпилирован в EXE-файл и будет регулярно выполняться для отправки сообщений в службу данных поставщика.
Вот примерный план того, что делает код:
public class OutboxManager
{
private List<OutboxMsg> _OutboxMsgs;
public void DistributeOutboxMessages()
{
try {
RetrieveMessages();
SendMessagesToVendor();
MarkMessagesAsProcessed();
}
catch Exception ex {
LogErrorMessageInDb(ex);
}
}
private void RetrieveMessages()
{
//retrieve messages from the database; poplate _OutboxMsgs.
//This code stays the same in each implementation.
}
private void SendMessagesToVendor() // <== THIS CODE CHANGES EACH IMPLEMENTATION
{
//vendor-specific code goes here.
//This code is specific to each implementation.
}
private void MarkMessagesAsProcessed()
{
//If SendMessageToVendor() worked, run this method to update this db.
//This code stays the same in each implementation.
}
private void LogErrorMessageInDb(Exception ex)
{
//This code writes an error message to the database
//This code stays the same in each implementation.
}
}
Я хочу написать этот код таким образом, чтобы я мог повторно использовать части, которые не меняются, не прибегая к копированию, вставке и заполнению кода для SendMessagesToVendor()
, Я хочу, чтобы разработчик мог использовать OutboxManager
и весь код базы данных уже написан, но он вынужден предоставить собственную реализацию отправки данных поставщику.
Я уверен, что есть хорошие объектно-ориентированные принципы, которые могут помочь мне решить эту проблему, но я не уверен, какой из них лучше всего использовать.
Это решение, к которому я пришел, вдохновлено ответом Виктора и ответом Рида (и комментариями) на использование модели интерфейса. Существуют все те же методы, но теперь они спрятаны в интерфейсах, которые потребитель может обновить при необходимости.
Я не осознавал всю мощь реализации интерфейса, пока не понял, что разрешаю потребителю класса подключать свои собственные классы для доступа к данным (IOutboxMgrDataProvider
) и регистрация ошибок (IErrorLogger
). Хотя я по-прежнему предоставляю реализации по умолчанию, так как не ожидаю, что этот код изменится, потребитель все равно может переопределить их своим собственным кодом. За исключением написания нескольких конструкторов (которые я могу изменить на именованные и необязательные параметры), действительно не потребовалось много времени, чтобы изменить мою реализацию.
public class OutboxManager
{
private IEnumerable<OutboxMsg> _OutboxMsgs;
private IOutboxMgrDataProvider _OutboxMgrDataProvider;
private IVendorMessenger _VendorMessenger;
private IErrorLogger _ErrorLogger;
//This is the default constructor, forcing the consumer to provide
//the implementation of IVendorMessenger.
public OutboxManager(IVendorMessenger messenger)
{
_VendorMessenger = messenger;
_OutboxMgrDataProvider = new DefaultOutboxMgrDataProvider();
_ErrorLogger = new DefaultErrorLogger();
}
//... Other constructors here that have parameters for DataProvider
// and ErrorLogger.
public void DistributeOutboxMessages()
{
try {
_OutboxMsgs = _OutboxMgrDataProvider.RetrieveMessages();
foreach om in _OutboxMsgs
{
if (_VendorMessenger.SendMessageToVendor(om))
_OutboxMgrDataProvider.MarkMessageAsProcessed(om)
}
}
catch Exception ex {
_ErrorLogger.LogErrorMessage(ex)
}
}
}
//...interface code: IVendorMessenger, IOutboxMgrDataProvider, IErrorLogger
//...default implementations: DefaultOutboxMgrDataProvider(),
// DefaultErrorLogger()
7 ответов
Я бы сказал, использовать Dependecy Injection. По сути, вы передаете абстракцию метода send.
Что-то вроде:
interface IVendorMessageSender
{
void SendMessage(Vendor v);
}
public class OutboxManager
{
IVendorMessageSender _sender;
public OutboxManager(IVendorMessageSender sender)
{
this._sender = sender; //Use it in other methods to call the concrete implementation
}
...
}
Другой подход, как уже упоминалось, наследование.
В любом случае: попробуйте удалить код извлечения БД из этого класса. Для этого используйте другую абстракцию (например, передавая интерфейс IDataProvider или что-то подобное в конструктор). Это сделает ваш код более тестируемым.
Есть два очень простых подхода:
Делать
OutboxManager
абстрактный класс и предоставить подкласс для каждого поставщика.SendMessagesToVendor
может быть помечен как абстрактный, заставляя его переопределяться каждым поставщиком. Этот подход прост, хорошо соответствует принципам ОО, а также имеет то преимущество, что позволяет вам предоставлять реализацию для других методов, но при этом позволяет переопределять версию для конкретного поставщика, если вы хотите разрешить это позже.Есть
OutboxManager
инкапсулировать некоторый другой класс или интерфейс, который предоставляет специфичную для поставщика информацию, требуемую вSendMessagesToVendor
, Это может быть небольшой интерфейс, реализованный для каждого поставщика, иSendMessagesToVendor
может использовать эту реализацию интерфейса для отправки своих сообщений. Преимущество этого состоит в том, что вы можете написать часть кода здесь, что потенциально уменьшает дублирование между поставщиками. Это также потенциально позволяет вашемуSendMessagesToVendor
метод должен быть более последовательным и более легко тестируемым, поскольку вам нужно полагаться только на функциональность конкретного поставщика, требуемую здесь. Это также может быть потенциально реализовано как делегат, переданный как связанный (но немного другой) подход (однако, я лично предпочитаю, чтобы интерфейс был реализован поверх делегата).
Если вы сделаете это абстрактным базовым классом, чтобы он был унаследован, вы можете заставить этот метод быть реализованным в конкретном объекте.
using System;
using System.Collections.Generic;
public abstract class OutboxManagerBase
{
private List<string> _OutboxMsgs;
public DistributeOutboxMessages()
{
try {
RetrieveMessages();
SendMessagesToVendor();
MarkMessagesAsProcessed();
}
catch Exception ex {
LogErrorMessageInDb(ex);
}
}
private void RetrieveMessages()
{
//retrieve messages from the database; poplate _OutboxMsgs.
//This code stays the same in each implementation.
}
protected abstract void SendMessagesToVendor();
private void MarkMessagesAsProcessed()
{
//If SendMessageToVendor() worked, run this method to update this db.
//This code stays the same in each implementation.
}
private void LogErrorMessageInDb(Exception ex)
{
//This code writes an error message to the database
//This code stays the same in each implementation.
}
}
public class OutBoxImp1 : OutboxManagerBase
{
protected override void SendMessagesToVendor()
{
throw new NotImplementedException();
}
}
Один из способов сделать это - использовать интерфейсы.
public interface IVendorSender
{
IEnumerable<OutboxMsg> GetMessages();
}
Затем в вашем конструкторе возьмите экземпляр в качестве параметра.
public class OutboxManager
{
private readonly IVendorSender _vendorSender;
public OutboxManager(IVendorSender vendorSender)
{
_vendorSender = vendorSender ?? new DefaultSender();
}
private void SendMessagesToVendor() // <== THIS CODE CHANGES EACH IMPLEMENTATION
{
_vendorSender.GetMessages(); // Do stuff...
}
}
Создать абстрактный базовый класс и иметь метод, который необходимо изменить как защищенный абстрактный, например
public abstract class OutboxManager
{
private List<OutboxMsg> _OutboxMsgs;
public void DistributeOutboxMessages()
{
try {
RetrieveMessages();
SendMessagesToVendor();
MarkMessagesAsProcessed();
}
catch (Exception ex) {
LogErrorMessageInDb(ex);
}
}
private void RetrieveMessages()
{
//retrieve messages from the database; poplate _OutboxMsgs.
//This code stays the same in each implementation.
}
protected abstract void SendMessagesToVendor(); // <== THIS CODE CHANGES EACH IMPLEMENTATION
private void MarkMessagesAsProcessed()
{
//If SendMessageToVendor() worked, run this method to update this db.
//This code stays the same in each implementation.
}
private void LogErrorMessageInDb(Exception ex)
{
//This code writes an error message to the database
//This code stays the same in each implementation.
}
}
Каждая реализация наследуется от этого абстрактного класса, но обеспечивает реализацию только для SendMessagesToVendor(), общая реализация которого определена в абстрактном базовом классе.
То же самое и мистеру Копси. Манифест № 1 действительно подклассификация. Вы, будь то удача или умение, уже структурировали свой код, чтобы его было легко реализовать.
В зависимости от характера различий между поставщиками, если имеется много общих функций, другой альтернативой может быть наличие базы данных с записями для каждого поставщика и наличие пары флагов, управляющих обработкой. Если вы можете разбить его на следующее: "если flag1 - истина, то делать что-то A, а что-то делать B; всегда делать что-то C; если flag2 - true, делать то, что D, мы уже сделали", вместо того, чтобы повторять связку кода между поставщиками, вы может быть в состоянии позволить данным контролировать обработку.
О, и я мог бы добавить, возможно, очевидное: если единственная разница - это значения данных, то, конечно, просто храните значения данных где-то. Как и в простейшем примере, если единственное различие между поставщиками - это имя домена, к которому вы подключаетесь, просто создайте таблицу с вендором и именем домена, прочитайте значение и подключите его.
Мне кажется, ты там почти весь.
Некоторые основные шаги:
1 Выясните, какие части вашего кода одинаковы, независимо от поставщика.
2 Запишите их в модуль многократного использования (вероятно, в.dll)
3 Определите, какие изменения для каждого поставщика.
4 Определите, что (из вышеперечисленного) является кодом - напишите специальные модули для этого.
5 Определите, что (из вышеперечисленного) является конфигурацией - создайте схему конфигурации для них.
Ваш.exe будет затем фактически вызывать соответствующий OutboxManager
Объект для правильного продавца.