Тестовая задачка с собеседования на C# — реализовать метод как можно универсальнее
Задачка с собеса С#
, уровень middle/senior (ИМХО) на подумать и покодить классы.
Я изменил формулировку, но суть не меняется. Также предоставлю ниже своё решение.
Существует три метода, которые меняют информацию о товаре:
- Изменяющий состояние;
- Изменяющий состов тегов;
- Задающий вес.
Так как в дальнейшем планируется добавить еще 10-20 методов, которые меняют информацю о товаре (сами методы могут быть реализованы как
nuget
в другом сервисе), нужно реализовать единственный методDoSomethingWithItem
, который в зависимости от входных параметров мог бы вызывать любое изменение товара.То есть методу может быть сказано "Поменяй состояние" или "Задай вес" и так далее, и его реализация уже сама вызовет один из методов
StateChanger.Change
илиWeightSetter.Set
и т. д.Классы
StateChanger
,TagsSetter
иWeightSetter
можно менять. Главное сохранить бизнес-функциональность.
Образец кода:
namespace ProductsService
{
public sealed class StateChanger
{
public enum State
{
Active,
InActive
}
private void Change(int itemId, State newState)
{
}
}
public sealed class TagsSetter
{
public void Set(int itemId, string[] tags)
{
}
}
public sealed class WeightSetter
{
public void Set(int itemId, int weight)
{
}
}
public sealed class ProductsService
{
public void DoSomeActionWithProdcut()
{
}
}
}
Вопросы следующие:
- Хорошо ли мое решение (c учётом того что "собес")?
- Может можно по-другому, по-лучше?
Накидывайте своё решение — хочется увидеть и другие варианты.
Ответы (3 шт):
namespace ProductsService
{
public interface ISetter<T>
{
void Set(int itemId, T data);
}
public sealed class StateChanger : ISetter<StateChanger.State>
{
public enum State
{
Active,
InActive
}
public void Set(int itemId, State newState) => Change(itemId, newState);
private static void Change(int itemId, State newState)
=> Console.WriteLine($"Call StateChanger.Set(itemId = {itemId}, newState = {newState})");
}
public sealed class TagsSetter : ISetter<string[]>
{
public void Set(int itemId, string[] tags)
=> Console.WriteLine($"Call TagsSetter.Set(itemId = {itemId}, tags = {string.Join(",", tags)})");
}
public sealed class WeightSetter : ISetter<int>
{
public void Set(int itemId, int weight)
=> Console.WriteLine($"Call WeightSetter.Set(itemId = {itemId}, weight = {weight})");
}
public sealed class ProductsService
{
//Isetter - конкретный сеттер itemId - айди товара data - данные для изменения
//Тут лучше шаблонизировать - это уберегаетот неправильного вызова типа: DoSomeActionWithProduct(new StateChanger, 1, "some")
public void DoSomeActionWithProduct<T>(ISetter<T> setter, int itemId, T data)
=> setter.Set(itemId, data);
}
}
Давайте сначала про то, что вам сказано в задании:
нужно реализовать единственный метод DoSomethingWithItem который в зависимости от входных параметров мог бы вызывать любое изменение товара.
То есть реализация этого метода есть критерий законченности ответа.
Далее, в качестве принципов решения:
- Мы должны ограничить набор изменений на нашей стороне, мы позволяем клиенту менять только то, что разрешено нами.
- Код должен позволять будущие изменения чего угодно. Мы должны иметь возможность расширить набор потенциальных изменений в продукте.
- Введение нового типа изменения в продукте не должно отразиться на сущесутвующем коде клиентов
- Делать изменение транзакционным или нет - это будет деталь реализации
DoSomeActionWithProdcut
, которую мы можем поменять в любой момент. - Мы ставим в приоритет композицию наследованию как основной постулат изменений
Далее, нам нужен объект, котрый предсатвляет собой изменение продукта. Требования к объекту:
- ИД продукта должен быть обязательно предоставлен
- Должен поддерживать несколько изменений сразу
- Легко добавить новые типы изменений в будущем
public sealed class ProductUpdate
{
public int ProductId { get; private set; }
public int? Weight { get; private set; }
public string[] Tags { get; private set; }
public StateChanger.State? state { get; private set; }
private ProductUpdate(int productId)
{
this.ProductId = productId;
}
public static ProductUpdate NewUpdate(int productId)
{
return new ProductUpdate(productId);
}
public ProductUpdate WithWeight(int weight)
{
this.Weight = weight;
return this;
}
public ProductUpdate WithTags(string[] tags)
{
this.Tags = tags;
return this;
}
public ProductUpdate WithState(StateChanger.State state)
{
this.state = state;
return this;
}
}
Класс выше предсталяет собой что то типа Билдера для изменения продукта. Все возможные изменения контролируются отдельными статическими методами. Почему не просто свойствами? Можно и просто свойствами, но мы потенциально можем иметь сложную логику создания подобного объекта (например, что если какие то поля меняются только в паре?), потому использовать Builder для подобного кажется разумным.
После этого, ProductService выглядит тривиально
public sealed class ProductsService
{
public void DoSomeActionWithProdcut(ProductUpdate update)
{
if (update.Weight.HasValue) new WeightSetter().Set(update.ProductId, update.Weight.Value);
if (update.Tags != null) new TagsSetter().Set(update.ProductId, update.Tags);
if (update.state.HasValue) new StateChanger().Change(update.ProductId, update.state.Value);
}
}
Если нужна транзакционность, можно добавить это в DoSomeActionWithProdcut
. Поддержка нового типа изменения будет простой - добавлением поля в ProductUpdate
и нового класса xxxChanger
.
Пример использования
var productUpdate = ProductUpdate.NewUpdate(1)
.WithState(UserQuery.StateChanger.State.Active)
.WithWeight(10);
var productService = new ProductsService();
productService.DoSomeActionWithProdcut(productUpdate);
Преимущества решения:
- Клиенту нет шанса ошибиться, ему надо только подготовить нужные изменения в
ProductUpdate
и отправить его вProductService
, всё. - Мы контролируем и типы изменений и как они выполняются
- Добавление нового типа изменений не ломает существующих клиентов
- Никаких сложностей с наследованиями для клиентов
- Способ, как выполняется изменение, скрыт от клиента - это просто деталь реализации нашего
ProductService
Так как в дальнейшем планируется добавить еще 10-20 методов, которые меняют информацю о товаре (сами методы могут быть реализованы как nuget в другом сервисе)
Другие методы в другом сервисе могут использовать ProductUpdate
для подготовки любой комбинации разрешенных на продукте изменений. Добавление новых типов изменений должно быть контролируемо только нами как владельцами модели продукта.
Пример
public ProductUpdate ImMethodFromOtherNuGetPackage(ProductUpdate update)
{
return update
.WithState(UserQuery.StateChanger.State.Active)
.WithWeight(100);
}
Кажется, всё из задания учел. Для пуристов, можно сделать прямо отдельный ProductUpdateBuilder
следуя всем канонам ООП - пусть будет как домашнее задание.
Предлогаю использовать лямбды.
Дано:
public class Product
{
public int Weight { get; set; }
public State State { get; set; }
public string[] Tags { get; set; }
}
public enum State
{
Active,
InActive
}
Сервис с одним методом:
public class ProductService
{
public void DoSomething(Product product, Action<Product> action)
{
action(product);
}
}
Используем:
var product = new Product { Weight = 10, State = State.InActive, Tags = [] };
Console.WriteLine($"{product.Weight} {product.State} {string.Join(',', product.Tags)}");
var service = new ProductService();
service.DoSomething(product, p => p.Weight += 20);
service.DoSomething(product, p => p.State = State.Active);
service.DoSomething(product, p => p.Tags = ["Foo", "Bar"]);
Console.WriteLine($"{product.Weight} {product.State} {string.Join(',', product.Tags)}");
Конечно, получилось масло масляное, т. к. можно выкинуть этот сервис и писать просто:
product.Weight += 20;
product.State = State.Active;
product.Tags = ["Foo", "Bar"];
Но если действие встроено в пайплайн обработки, то это имеет смысл:
public void DoSomething(Action<Product> action)
{
var product = productRepository.Get();
action(product);
productRepository.Save(product);
}