следует ли вводить новое поведение с помощью композиции или с помощью других средств? - PullRequest
2 голосов
/ 02 декабря 2010

Я решил представить какое-то новое поведение, используя композицию, а не вставлять новый объект в мой код потребителей ИЛИ заставлять потребителя самостоятельно реализовывать это новое поведение. Я принял плохое дизайнерское решение?

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

Вот предположения, с которыми я работал ...

  • У меня есть интерфейс с именем IFileManager, который позволяет разработчикам управлять различными типами файлов
  • У меня есть фабрика, которая возвращает конкретную реализацию IFileManager
  • У меня есть 3 реализации IFileManager, это (LocalFileManager, DfsFileManager, CloudFileManager)
  • У меня есть новые требования, согласно которым мне нужно управлять разрешениями только для файлов, управляемых CloudFileManager, поэтому поведение для управления разрешениями уникально для CloudFileManager

Вот тест, который привел меня к коду, который я написал ...

[TestFixture]
public class UserFilesRepositoryTest
{
    public interface ITestDouble : IFileManager, IAclManager { }

    [Test]
    public void CreateResume_AddsPermission()
    {
        factory.Stub(it => it.GetManager("cloudManager")).Return(testDouble);

        repository.CreateResume();

        testDouble.AssertWasCalled(it => it.AddPermission());
    }

    [SetUp]
    public void Setup()
    {
        testDouble = MockRepository.GenerateStub<ITestDouble>();
        factory = MockRepository.GenerateStub<IFileManagerFactory>();
        repository = new UserFileRepository(factory);
    }

    private IFileManagerFactory factory;
    private UserFileRepository repository;
    private ITestDouble testDouble;
}

Вот оболочка моего дизайна (это просто базовая схема, а не весь шибан) ...

public class UserFileRepository
{
    // this is the consumer of my code...
    public void CreateResume()
    {
        var fileManager = factory.GetManager("cloudManager");
        fileManager.AddFile();

        // some would argue that I should inject a concrete implementation 
        // of IAclManager into the repository, I am not sure that I agree...
        var permissionManager = fileManager as IAclManager;
        if (permissionManager != null)
            permissionManager.AddPermission();
        else
            throw new InvalidOperationException();
    }

    public UserFileRepository(IFileManagerFactory factory)
    {
        this.factory = factory;
    }

    private IFileManagerFactory factory;
}

public interface IFileManagerFactory
{
    IFileManager GetManager(string managerName);
}

public class FileManagerFactory : IFileManagerFactory
{
    public IFileManager GetManager(string managerName)
    {
        IFileManager fileManager = null;
        switch (managerName) {
            case "cloudManager":
                fileManager = new CloudFileManager();
                break;
            // other managers would be created here...
        }

        return fileManager;
    }
}

public interface IFileManager
{
    void AddFile();
    void DeleteFile();
}

public interface IAclManager
{
    void AddPermission();
    void RemovePermission();
}

/// <summary>
/// this class has "special" behavior
/// </summary>
public class CloudFileManager : IFileManager, IAclManager
{
    public void AddFile() { 
        // implementation elided...
    }

    public void DeleteFile(){ 
        // implementation elided...
    }

    public void AddPermission(){
        // delegates to the real implementation
        aclManager.AddPermission();
    }

    public void RemovePermission() {
        // delegates to the real implementation
        aclManager.RemovePermission();
    }

    public CloudFileManager(){
        aclManager = new CloudAclManager();
    }

    private IAclManager aclManager;
}

public class LocalFileManager : IFileManager
{
    public void AddFile() { }

    public void DeleteFile() { }
}

public class DfsFileManager : IFileManager
{
    public void AddFile() { }

    public void DeleteFile() { }
}

/// <summary>
/// this class exists to manage permissions 
/// for files in the cloud...
/// </summary>
public class CloudAclManager : IAclManager
{
    public void AddPermission() { 
        // real implementation elided...
    }

    public void RemovePermission() { 
        // real implementation elided...
    }
}

Ответы [ 3 ]

4 голосов
/ 02 декабря 2010

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

Что с этим не так?

  1. Вы разделили свои файловые менеджеры и использовали IFileManager, но вы не сделали то же самое с IAclManager. Хотя у вас есть фабрика для создания различных файловых менеджеров, вы автоматически сделали CloudAclManager IAclManager из CloudFileManager. Итак, какой смысл иметь IAclManager?
  2. Что еще хуже, ты инициализировать новый CloudAclManager внутри CloudFileManager каждый раз, когда вы пытаетесь получить его ACL менеджер - вы только что дали фабрику обязанности к вашему CloudFileManager.
  3. У вас есть CloudFileManager орудия IAclManager поверх его как свойства. Вы просто перенесли правило, согласно которому разрешения CloudFileManager уникальны, на уровень модели, а не на уровень бизнес-правил. Это также приводит к поддержке ненужного потенциал круговой ссылки между собой и собственностью.
  4. Даже если бы вы хотели CloudFileManager делегировать функциональность разрешения для CloudAclManager, зачем вводить в заблуждение других классы думать, что CloudFileManager обрабатывает свои собственные наборы разрешений? Вы только что сделали свой Модель класса выглядит как фасад.

Хорошо, так что мне делать вместо этого?

Во-первых, вы назвали свой класс CloudFileManager, и это правильно, потому что его единственная ответственность - управлять файлами для облака. Теперь, когда наборы разрешений также должны управляться для облака, действительно ли правильно для CloudFileManager взять на себя эти новые обязанности? Ответ - нет.

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

К сожалению, если вы переименуете свой класс, это отрицательно скажется на тех, кто в данный момент использует ваш класс. Так как насчет использования композиции, но без изменения CloudFileManager?

Мое предложение было бы сделать следующее:

1. Сохраните IAclManager и создайте IFileSystemManager

public interface IFileSystemManager {
    public IAclManager AclManager { get; }
    public IFileManager FileManager { get; }
}

или

public interface IFileSystemManager : IAclManager, IFileManager {

}

2. Создать CloudFileSystemManager

public class CloudFileSystemManager : IFileSystemManager {
    // implement  IFileSystemManager
    // 
    // How each manager is set is up to you (i.e IoC, DI, simple setters, 
    // constructor parameter, etc.).
    // 
    // Either way you can just delegate to the actual IAclManager/IFileManager
    // implementations.
}

Почему?

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

РЕДАКТИРОВАТЬ - Включая уточняющие вопросы Аскера

Если я создам IFileSystemManager : IFileManager, IAclManager, будет ли хранилище по-прежнему использовать FileManagerFactory и возвращать экземпляр CloudFileSystemManager?

Нет, FileManagerFactory не должно возвращать FileSystemManager. Ваша оболочка должна будет обновиться, чтобы использовать новые интерфейсы / классы. Возможно что-то вроде следующего:

private IAclManagerFactory m_aclMgrFactory;
private IFileManagerFactory m_fileMgrFactory;

public UserFileRepository(IAclManagerFactory aclMgrFactory, IFileManagerFactory fileMgrFactory) {
    this.m_aclMgrFactory = aclMgrFactory;
    this.m_fileMgrFactory = fileMgrFactory;
}

public void CreateResume() {
    // I understand that the determination of "cloudManager"
    // is non-trivial, but that part doesn't change. For
    // your example, say environment = "cloudManager"
    var environment = GetEnvMgr( ... );

    var fileManager = m_fileMgrFactory.GetManager(environment);
    fileManager.AddFile();

    // do permission stuff - see below
}

Что касается вызова разрешений, у вас есть несколько вариантов:

// can use another way of determining that a "cloud" environment
// requires permission stuff to be done
if(environment == "cloudManager") {
    var permissionManager = m_aclMgrFactory.GetManager(environment);
    permissionManager.AddPermission();
}

или

// assumes that if no factory exists for the environment that
// no permission stuff needs to be done
var permissionManager = m_aclMgrFactory.GetManager(environment);
if (permissionManager != null) {
    permissionManager.AddPermission();
}
2 голосов
/ 02 декабря 2010

Я думаю, что композиция - это как раз то, что нужно для такого рода уловок.Но я думаю, что вы должны сделать его более простым (KISS) и просто сделать свойство IAclManager в IFileManager и установить для него значение по умолчанию, равное null, и установить реализацию SecurityManager для облачной службы там.

  • Вы можете проверить, нужно ли проверять разрешения, установив нулевое значение свойства securityManager.Таким образом, если не нужно выполнять permissionsManaging (как в системе localfile), у вас не появятся исключения.Например:
if (fileManager.permissionsManager != null)
    fileManager.permissionsManager.addPermission();

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

  • Позже вы сможете внедрить больше IAclManager для других IFileManager, когда ваш клиент в следующий раз изменит требования так же, как и сейчас.

  • О, и тогда вы не будетеиметь такую ​​запутанную иерархию, когда кто-то еще смотрит на код; -)

0 голосов
/ 02 декабря 2010

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

Если это так, вы можете рассмотреть возможность добавления перегрузки к вашему методу GetManager (), в котором вы можете указать требуемый интерфейс, и у фабрики может быть код, который выдает исключение, если ононе находит нужный файловый менеджер.Для этого вы можете добавить другой интерфейс, который будет пустым, но реализует как IAclManager, так и IFileManager:

public interface IAclFileManager : IFileManager, IAclManager {}

, а затем добавьте следующий метод к фабрике:

public T GetManager<T>(string name){ /* implementation */}

GetManager сгенерируетисключение, если менеджер с указанным именем не реализует T (вы также можете проверить, является ли он производным от типа T).

Все это, если AddPermissions не принимает никаких параметров (не уверен, что вы только что сделали это для публикации), почему бы просто не вызвать метод AddPermissions () из CloudFileManager.AddFile () и полностью инкапсулировать его от пользователя (устраняя необходимость в новом интерфейсе IAclManager)?

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

Удачи!

...