Состав и обобщение С# OOP одновременно
Это может быть простой/основной вопрос ООП, но я до сих пор не могу понять, как его решить.
Во время интервью у меня была следующая проблема: сделайте диаграмму классов UML и напишите базовый код для "умного" телефона, который содержит функциональные возможности телефона и mp3-плеера. У нас есть следующее (принятое) решение:
class Telephone
{
public string name { get; set; }
public Telephone()
{
name = "name telephone";
}
}
class MP3
{
public string name { get; set; }
public MP3()
{
name = "name mp3";
}
}
И "умный" класс телефона:
class TelephoneMP3
{
public Telephone tel;
public MP3 mp3;
public TelephoneMP3()
{
tel = new Telephone();
mp3 = new MP3();
}
}
Как вы можете видеть, у нас есть соотношение композиций между телефонами и телефонами /MP 3.
Но, используя этот код, TelephoneMP3 не является телефоном, а PhoneMP3 не является MP3, что не является логичным. Итак, какие изменения я должен внести, чтобы сделать это действительным? Например, такой тип теста:
if (telMp3 is Telephone)
{
Console.WriteLine("TelephoneMP3 is telephone");
}
if (telMp3 is MP3)
{
Console.WriteLine("TelephoneMP3 is mp3");
}
Модификации могут быть сделаны с использованием следующих замечаний:
- Телефон/MP3/ТелефонMP3 должны оставаться классами (все 3 из них)
- При необходимости я мог бы добавить интерфейсы/другие классы
- ТелефонMP3 не должен дублировать все функции из телефона /MP 3 (например, во время наследования от интерфейса, где TelephoneMP3 будет обязан писать код со всех членов интерфейса).
Заранее благодарю
Ответы
Ответ 1
С# не поддерживает множественное наследование, вам нужно использовать интерфейсы и абстрактный класс для общих реализаций. Вы можете выполнить следующее:
Изменить: я добавлю больше деталей к своему ответу
abstract class BaseDevice
{
public string name { get; set; }
public void Print()
{
Console.WriteLine("{0}", name );
}
}
public interface IPhone
{
void DoPhone();
}
public interface IMP3
{
void DoMP3();
}
class Telephone :BaseDevice , IPhone
{
public Telephone()
{
name = "name telephone";
}
}
class MP3 : BaseDevice , IMP3
{
public MP3()
{
name = "name mp3";
}
}
class telMp3 : BaseDevice , IMP3, IPhone
{
private Telephone _tel;
private MP3 _mp3;
public telMp3()
{
name = "name telMp3";
}
public void DoPhone()
{
_tel.DoPhone();
}
public void DoMP3()
{
_mp3.DoMP3();
}
}
Ответ 2
Так как С# не поддерживает множественное наследование, рассмотрите вместо этого использование интерфейсов:
public interface Phone{ ... }
public interface Mp3{ ... }
public class Telephone : Phone{ ... }
public class Mp3Player : Mp3{ ... }
public class Smartphone : Phone, Mp3{ ... }
Этот способ Smartphone
равен Phone
и Mp3
. Если вам нужно написать метод, который работает с Telephone
, используйте вместо этого интерфейс Phone
. Таким образом вы сможете передать либо Telephone
, либо Smartphone
в качестве аргумента.
Ответ 3
Это почти похоже на другие ответы, но..
Я думаю, что он имеет лучшую точность в отношении иерархии наследования.
internal class Program
{
private static void Main(string[] args)
{
var telephone = new Telephone();
Console.WriteLine(telephone.Name);
telephone.OutboundCall("+1 234 567");
Console.WriteLine("Am I a Telephone? {0}", telephone is Telephone);
Console.WriteLine("Am I a MP3? {0}", telephone is MediaPlayer3);
Console.WriteLine("Am I a Smartphone? {0}", telephone is Smartphone);
Console.WriteLine("Do I Have Telephone Capabilities? {0}", telephone is ITelephone);
Console.WriteLine("Do I Have MP3 Capabilities? {0}", telephone is IMediaPlayer3);
Console.WriteLine();
var mp3 = new MediaPlayer3();
Console.WriteLine(mp3.Name);
mp3.PlaySong("Lalala");
Console.WriteLine("Am I a Telephone? {0}", mp3 is Telephone);
Console.WriteLine("Am I a MP3? {0}", mp3 is MediaPlayer3);
Console.WriteLine("Am I a Smartphone? {0}", mp3 is Smartphone);
Console.WriteLine("Do I Have Telephone Capabilities? {0}", mp3 is ITelephone);
Console.WriteLine("Do I Have MP3 Capabilities? {0}", mp3 is IMediaPlayer3);
Console.WriteLine();
var smartphone = new Smartphone();
Console.WriteLine(smartphone.Name);
smartphone.OutboundCall("+1 234 567");
smartphone.PlaySong("Lalala");
Console.WriteLine("Am I a Telephone? {0}", smartphone is Telephone);
Console.WriteLine("Am I a MP3? {0}", smartphone is MediaPlayer3);
Console.WriteLine("Am I a Smartphone? {0}", smartphone is Smartphone);
Console.WriteLine("Do I Have Telephone Capabilities? {0}", smartphone is ITelephone);
Console.WriteLine("Do I Have MP3 Capabilities? {0}", smartphone is IMediaPlayer3);
Console.ReadKey();
}
public interface IDevice
{
string Name { get; }
}
public interface ITelephone : IDevice
{
void OutboundCall(string number);
}
public interface IMediaPlayer3 : IDevice
{
void PlaySong(string filename);
}
public class Telephone : ITelephone
{
public string Name { get { return "Telephone"; } }
public void OutboundCall(string number)
{
Console.WriteLine("Calling {0}", number);
}
}
public class MediaPlayer3 : IMediaPlayer3
{
public string Name { get { return "MP3"; } }
public void PlaySong(string filename)
{
Console.WriteLine("Playing Song {0}", filename);
}
}
public class Smartphone : ITelephone, IMediaPlayer3
{
private readonly Telephone telephone;
private readonly MediaPlayer3 mp3;
public Smartphone()
{
telephone = new Telephone();
mp3 = new MediaPlayer3();
}
public string Name { get { return "Smartphone"; } }
public void OutboundCall(string number)
{
telephone.OutboundCall(number);
}
public void PlaySong(string filename)
{
mp3.PlaySong(filename);
}
}
}
Выход программы:
Telephone
Calling +1 234 567
Am I a Telephone? True
Am I a MP3? False
AM I a Smartphone? False
Do I Have Telephone Capabilities? True
Do I Have MP3 Capabilities? False
MP3
Playing Song Lalala
Am I a Telephone? False
Am I a MP3? True
AM I a Smartphone? False
Do I Have Telephone Capabilities? False
Do I Have MP3 Capabilities? True
Smartphone
Calling +1 234 567
Playing Song Lalala
Am I a Telephone? False
Am I a MP3? False
AM I a Smartphone? True
Do I Have Telephone Capabilities? True
Do I Have MP3 Capabilities? True
Ответ 4
Здесь есть хорошие ответы. Ответы, которые говорят об использовании интерфейсов, хороши, и то, что, возможно, ищет интервьюер. Тем не менее, я бы счел просто отрицающим предпосылку, что отношения "в некотором роде", которые удовлетворяются, являются хорошей идеей. Скорее, я бы подумал об использовании организации поставщика услуг:
public interface ITelephone { ... }
internal class MyTelephone : ITelephone { ... }
public interface IMusicPlayer { ... }
internal class MyPlayer : IMusicPlayer { ... }
public interface IServiceProvider
{
T QueryService<T>() where T : class;
}
internal class MyDevice : IServiceProvider
{
MyTelephone phone = new MyTelephone();
MyPlayer player = new MyPlayer();
public T QueryService<T>() where T : class
{
if (typeof(T) == typeof(ITelephone)) return (T)(object)phone;
if (typeof(T) == typeof(IPlayer)) return (T)(object)player;
return null;
}
}
Теперь у вызывающего есть MyDevice
в руке через его интерфейс IServiceProvider
. Вы спрашиваете его
ITelephone phone = myDevice.QueryService<ITelephone>();
и если phone
не является нулевым, устройство может действовать как телефон. Но
myDevice is ITelephone
является ложным. Устройство не является телефоном, оно знает, как найти то, что действует как телефон.
Для большего количества в этом ключе изучите плагины, такие как MAF.
Ответ 5
Я думаю, что этот вопрос интервью не является (как и все вопросы интервью) о самой проблеме. В упражнении по объединению двух классов с помощью композиции можно получить ответ с помощью учебника. Этот вызов - это тонкий вопрос, и я предлагаю, чтобы вы решили обсудить, почему. По крайней мере, это то, что я хотел бы получить от моих собеседников.
Этот тест:
if(telMp3 is Telephone && telMp3 is MP3) {
... является реальной проблемой. Почему вы должны соответствовать этим критериям? Этот тест полностью исключает цель создания объектов вне композиции. Он требует, чтобы объекты были реализованы определенным образом. Он показывает, что существующие реализации классов уже тесно связаны с кодовой базой (если их невозможно устранить). Эти требования означают, что SOLID принципы не соблюдались, потому что вы не можете просто выполнять методы базового типа, вы должны фактически быть базой тип. Это нехорошо.
Как говорили другие ответы, решение было бы использовать интерфейсы. Затем вы можете передать свой объект любому методу, требующему интерфейса. Для такого использования потребуется тест, например:
if (telMp3 is IPhone && telMp3 is IMp3) {
... но вы не можете этого сделать из-за ограничения своей задачи. Это означает, что в остальной части вашего кода люди пишут методы, которые явно зависят от конкретных типов Telephone
и MP3
. Это реальная проблема.
По моему мнению, правильный ответ на эту проблему состоит в том, чтобы сказать, что кодовая база не проходит тест. Конкретные последствия в вашей проблеме неоправданны; вам необходимо изменить требования к проблеме, прежде чем вы сможете ее решить правильно. Интервьюер, который признает этот факт, прошел бы тест с летными цветами.
Ответ 6
Вы можете использовать явные реализации интерфейса, а также ограничить использование общей переменной Name
. Таким образом, вам нужно будет перейти к интерфейсу для доступа к нему. У вас все еще есть общедоступные свойства/методы из интерфейса.
Композиция все еще используется, но SmartPhone
имеет контроль над реализациями своих свойств/методов.
Для меня это была бы самая простая реализация для работы, потому что я редко хочу использовать как реализацию из mp3player, так и телефона, но, скорее, один из них. Кроме того, я все еще полностью контролирую, что происходит, когда методы интерфейса вызываются в SmartPhone
.
class User
{
void UseSmartPhone(SmartPhone smartPhone)
{
// Cannot access private property 'Name' here
Console.WriteLine(smartPhone.Name);
// Cannot access explicit implementation of 'IMp3Player.Play'
smartPhone.Play();
// You can send the phone to the method that accepts an IMp3Player though
PlaySong(smartPhone);
// This works fine. You are sure to get the Phone name here.
Console.WriteLine(((IPhone)smartPhone).Name);
// This works fine, since the Call is public in SmartPhone.
smartPhone.Call();
}
void CallSomeone(IPhone phone)
{
phone.Call();
}
void PlaySong(IMp3Player player)
{
player.Play();
}
}
class SmartPhone : IPhone, IMp3Player
{
private Phone mPhone;
private Mp3Player mMp3Player;
public SmartPhone()
{
mPhone = new Phone();
mMp3Player = new Mp3Player();
}
public void Call()
{
mPhone.Call();
}
string IPhone.Name
{
get { return mPhone.Name; }
}
string IMp3Player.Name
{
get { return mMp3Player.Name; }
}
void IMp3Player.Play()
{
mMp3Player.Play();
}
}
class Mp3Player
{
public string Name { get; set; }
public void Play()
{
}
}
class Phone
{
public string Name { get; set; }
public void Call()
{
}
}
interface IPhone
{
string Name { get; }
void Call();
}
interface IMp3Player
{
string Name { get; }
void Play();
}
Ответ 7
Как насчет этого решения:
public interface ITelephone
{
string Name{get;}
void MakeCall();
}
public interface IMp3
{
string Name { get; }
void Play(string filename);
}
public abstract class BaseTelephone : ITelephone
{
public virtual string Name { get { return "Telephone"; } }
void MakeCall()
{
// code to make a call.
}
}
public class MyMp3Player : IMp3
{
public string Name { get { return "Mp3 Player"; } }
public void Play(string filename)
{
// code to play an mp3 file.
}
}
public class SmartPhone : BaseTelephone, IMp3
{
public override string Name { get { return "SmartPhone"; } }
private IMp3 Player { get { return _Player; } set { _Player = value; } }
private IMp3 _Player = new MyMp3Player();
public void Play(string filename) { Player.Play(filename); }
}
Таким образом, смартфон также может быть Mp3-плеером, но внутри он имеет Mp3-плеер, который он использует для воспроизведения музыки. Внутренний проигрыватель может быть заменен на новый (например, обновление) с использованием свойства SmartPhone Player
.
Код телефона записывается только один раз в базовом телефоне. Код для Mp3-плеера записывается только один раз - в класс MyMp3Player.
Ответ 8
Используйте шаблон стратегии (используйте некоторые ярлыки ниже, вы получите суть).
public class Device {
private List<App> apps;
public Device() {
this.apps = new List<App>();
this.apps.Add(new Mp3Player());
this.apps.Add(new Telephone());
}
}
public class Mp3Player implements App {...}
public class Telephone implements App {...}
public interface App {...}
Отказ от ответственности: мой родной язык - это PHP, простите мне любые стандарты кодирования С# и т.д. и т.д.
Ответ 9
Вы можете использовать неявное литье
class TelephoneMP3
{
public Telephone tel;
public MP3 mp3;
public TelephoneMP3()
{
tel = new Telephone();
mp3 = new MP3();
}
public static implicit operator Telephone(TelephoneMP3 telemp3) {
return telemp3.tel;
}
public static implicit operator MP3(TelephoneMP3 telemp3) {
return telemp3.mp3;
}
}
Это не будет проходить точный тест, который вы предложили, но вы можете сделать
var teleMp3 = new TelephoneMP3();
Telephone t = teleMp3;
Ответ 10
Вы пытаетесь смоделировать иерархию продуктов, в которой данный продукт может иметь свои собственные специфические свойства, а также состоит из стандартных подэлементов. Это действительно пример структуры композиции. Я предлагаю ввести базовый интерфейс для любого компонента продукта, а затем создать конкретные интерфейсы для телефонов, MP3-плееров и продуктов для смартфонов.
В традиционном шаблоне композиции каждый node может содержать произвольный список компонентов, к которым могут добавляться или удаляться подкомпоненты, однако в вашей модели данных представляется более полезным для каждого конкретного типа продукта указывать его точных детей, затем предоставить общий метод для итерации по ним. Это позволяет с легкостью запрашивать определенные (вспомогательные) компоненты указанного типа/интерфейса во всей иерархии продуктов.
Я также представил интерфейс для продукта GPS, поскольку все новые телефоны содержат встроенные GPS-приемники - просто чтобы проиллюстрировать, как работать с рекурсивными иерархиями компонентов.
public interface IProductComponent
{
string Name { get; set; }
IEnumerable<IProductComponent> ChildComponents { get; }
IEnumerable<IProductComponent> WalkAllComponents { get; }
TProductComponent UniqueProductComponent<TProductComponent>() where TProductComponent : class, IProductComponent;
}
public interface ITelephone : IProductComponent
{
IGps Gps { get; }
}
public interface IMp3Player : IProductComponent
{
}
public interface IGps : IProductComponent
{
double AltitudeAccuracy { get; }
}
public interface ISmartPhone : IProductComponent
{
ITelephone Telephone { get; }
IMp3Player Mp3Player { get; }
}
Затем эти интерфейсы могут быть реализованы с помощью параллельного набора классов:
public abstract class ProductComponentBase : IProductComponent
{
string name;
protected ProductComponentBase(string name)
{
this.name = name;
}
#region IProductComponent Members
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public virtual IEnumerable<IProductComponent> ChildComponents
{
get
{
return Enumerable.Empty<IProductComponent>();
}
}
public IEnumerable<IProductComponent> WalkAllComponents
{
get
{
yield return this;
foreach (var child in ChildComponents)
{
foreach (var subChild in child.WalkAllComponents)
yield return subChild;
}
}
}
public TProductComponent UniqueProductComponent<TProductComponent>() where TProductComponent : class, IProductComponent
{
TProductComponent foundComponent = null;
foreach (var child in WalkAllComponents.OfType<TProductComponent>())
{
if (foundComponent == null)
foundComponent = child;
else
throw new Exception("Duplicate products found of type " + typeof(TProductComponent).Name);
}
return foundComponent;
}
#endregion
}
public class Telephone : ProductComponentBase, ITelephone
{
IGps gps = new Gps();
public Telephone()
: base("telephone")
{
}
#region ITelephone Members
public IGps Gps
{
get
{
return gps;
}
}
#endregion
IEnumerable<IProductComponent> BaseChildComponents
{
get
{
return base.ChildComponents;
}
}
public override IEnumerable<IProductComponent> ChildComponents
{
get
{
if (Gps != null)
yield return Gps;
foreach (var child in BaseChildComponents)
yield return child;
}
}
}
public class Gps : ProductComponentBase, IGps
{
public Gps()
: base("gps")
{
}
#region IGps Members
public double AltitudeAccuracy
{
get { return 100.0; }
}
#endregion
}
public class TelephoneMP3 : ProductComponentBase, ISmartPhone
{
ITelephone telephone;
IMp3Player mp3Player;
public TelephoneMP3()
: base("TelephoneMP3")
{
this.telephone = new Telephone();
this.mp3Player = new MP3();
}
IEnumerable<IProductComponent> BaseChildComponents
{
get
{
return base.ChildComponents;
}
}
public override IEnumerable<IProductComponent> ChildComponents
{
get
{
if (Telephone != null)
yield return Telephone;
if (Mp3Player != null)
yield return Mp3Player;
foreach (var child in BaseChildComponents)
yield return child;
}
}
#region ISmartPhone Members
public ITelephone Telephone
{
get { return telephone; }
}
public IMp3Player Mp3Player
{
get { return mp3Player; }
}
#endregion
}
public class MP3 : ProductComponentBase, IMp3Player
{
public MP3()
: base("mp3Player")
{
}
}
Когда новые типы компонентов продукта добавляются (или подклассы), они переопределяют "ChildComponents" своего родителя и возвращают дочерние дочерние объекты домена.
Сделав это, вы можете (рекурсивно) запросить иерархию продуктов для компонентов определенного типа для вашего использования. Например:
var accuracy = smartPhone.UniqueProductComponent<IGps>().AltitudeAccuracy
или
bool hasPhone = (component.UniqueProductComponent<ITelephone>() != null)
Эта комбинация обобщения и композиции позволяет избежать дублирования кода, делая явным тип подкомпонентов, которые должны быть найдены в любом заданном продукте. Это также позволяет избежать того, что все продукты более высокого уровня проксируют интерфейсы своих стандартных детей, передавая им все вызовы.
Ответ 11
Вопреки всем остальным ответам, я уверен, что вопрос, который задают этот вопрос, делает невозможным. Причина в следующем:
Вы явно указываете
Но, используя этот код, TelephoneMP3 не является телефоном, а PhoneMP3 не является MP3, что не является логичным. Итак, какие изменения я должен внести, чтобы сделать это действительным?
Увидев слово "is", я сразу же думаю о "is". Я сразу же предполагаю, что это то, что вы действительно хотите.
Затем переходите к следующему:
Телефон/MP3/ТелефонMP3 должны оставаться классами (все 3 из них)
Мы уверены, что можем сделать следующее:
interface ITelephone { }
class Telephone
{
public string name { get; set; }
public Telephone()
{
name = "name telephone";
}
}
interface IMP3 { }
class MP3 : IMP3
{
public string name { get; set; }
public MP3()
{
name = "name mp3";
}
}
class TelephoneMP3 : ITelephone, IMP3
{
public Telephone tel;
public MP3 mp3;
public TelephoneMP3()
{
tel = new Telephone();
mp3 = new MP3();
}
}
Но у нас все еще есть одна проблема. Слово "есть". Поскольку мы должны держать классы TelephoneMP3, Телефон и MP3 и С# не поддерживают многократное наследование, это просто невозможно.
Чтобы проиллюстрировать мою точку зрения:
public class Program
{
static void Main(string[] args)
{
TelephoneMP3 t = new TelephoneMP3();
Console.WriteLine((t is TelephoneMP3)? true:false);
Console.WriteLine((t is ITelephone) ? true : false);
Console.WriteLine((t is IMP3) ? true : false);
Console.WriteLine((t is Telephone) ? true : false);
Console.WriteLine((t is MP3) ? true : false);
Console.ReadLine();
}
}
Это даст вам
True
True
True
False
False
Другими словами TelephoneMP3 "является" ITelephone. TelephoneMP3 "является" IMP3; однако телефон TelephoneMP3 не может быть как MP3, так и телефоном.