Лучшая практика в том, что включать в базовый класс?

Мне трудно найти передовой опыт в С# для «базовых» классов.

Верно ли, что базовые классы должны содержать только те переменные/свойства, которые используются ВСЕМИ, кто их наследует?

В моем примере свойство используется 3 из 7 классов, которые наследуют базовый класс.

public class UpgradeBase
{
   Public bool IsToggleable { get; set; } = false;
}

public class UpgradeBuilding : UpgradeBase
{
   //This class needs to access IsToggleable
}

public class UpgradeScience : UpgradeBase
{
   //This class does NOT need to access IsToggleable and never uses it
}

Должен ли я включать это свойство только в классы, которые будут его использовать (3 из 7 классов, которые действительно нуждаются в этом свойстве), или «хорошо» определить его в базовом классе (даже если 4 из 7 выиграли никогда не заботился об этом)?

Вы бы никогда не представили базовый класс для введения свойства. Вместо этого вы должны определить интерфейс. Классы обычно содержат логику (методы).

BionicCode 27.11.2022 20:59

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

mason 27.11.2022 21:34

@mason «Классы обычно используются для хранения данных, и они обычно используются для логики». - это не по делу. Я не отрицал этого. Но это частный случай, когда базовый класс используется только для определения свойства, которое должны иметь определенные классы — классический контракт, классический случай для введения интерфейса. Наследование — очень проблематичное понятие. Если вы хотите, чтобы у классов было определенное свойство, интерфейс — правильный путь. Если вы хотите повторно использовать логику/поведение, это сделает общий базовый класс. В общем, хороший дизайн класса избегает наследования в пользу композиции.

BionicCode 27.11.2022 21:58

@BionicCode Ваше утверждение «Вы бы никогда не ввели базовый класс для введения свойства» неверно. Они обычно используются для этой цели. Поэтому, пожалуйста, удалите свое некорректное утверждение. Если вы хотите сказать что-то вроде «в этом случае использование базового класса для вашего свойства не имеет смысла, потому что <причины>», то это нормально.

mason 27.11.2022 23:16

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

Mathieu Guindon 28.11.2022 00:21

Я удивлен, что никто не заметил, что в коде OP указано, что ЭТОТ класс должен получить доступ к IsToggleable .... в случае шляпы я бы предпочел иметь базовый класс и protected bool IsToggleable {get; set;} и вообще не выставлять это свойство внешнему миру ... вот что интерфейс был бы для

derHugo 28.11.2022 08:13
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
132
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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

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

Что-то вроде:

public class UpgradeBase
{

}

public class UpgradeToggleable : UpgradeBase
{
   public bool IsToggleable { get; set; } = false;
}

public class UpgradeBuilding : UpgradeToggleable
{
   //This class needs to access IsToggleable
}

public class UpgradeScience : UpgradeBase
{
   //This class does NOT need to access IsToggleable and never uses it
}

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

Другой подход заключается в том, чтобы отдать предпочтение композиции, а не наследованию. Общие функции, которые плохо вписываются в строгую иерархию, могут быть реализованы в собственном классе компонентов. Классы, которым нужны эти функции, могут включать поле этого типа компонента. Это одна из идей, лежащих в основе так называемых архитектур Entity/Component/System.

Это классический сценарий для интерфейса. Вы бы никогда не использовали наследование для введения каких-либо свойств.

BionicCode 27.11.2022 21:09

@BionicCode никогда в основном всегда неверен, когда речь идет о кодировании ^^ Это зависит ... есть обстоятельства, когда это может быть необходимо;) В Unity (с которым этот вопрос помечен), например. когда речь идет о сценариях редактора, это мне часто требуется, потому что интерфейсы не сериализованы => вам нужно иметь базовый класс для ваших типов, если вы хотите реализовать для них общий ящик инспектора или ссылаться на них в сериализованных полях и т. д.

derHugo 28.11.2022 08:08

Поскольку код OP говорит This class needs to access IsToggleabl .. не внешний класс, а ЭТОТ класс, я бы скорее сказал, что должен быть базовый класс с protected bool IsToggleable {get; set;}, а не интерфейс, который открывает его внешнему миру;)

derHugo 28.11.2022 08:10

@derHugo Да, есть причины за и против любого решения в зависимости от деталей проблемы. У нас недостаточно деталей, чтобы ответить на этот вопрос.

BionicCode 28.11.2022 08:14

@BionicCode ... тогда почему ты ... и с довольно предвзятым заявлением ;)

derHugo 28.11.2022 08:16

@BionicCode с использованием интерфейса не решает проблему реализации. Конечно, можно было бы придумать набор из N интерфейсов, каждый из которых описывает какие-то независимые функции, но... это довольно быстро превратится в кашу. И, опять же, это не отвечает на поставленный вопрос. Где должен быть реализован интерфейс? В базовом классе? Ниже по иерархии? В каждом терминальном дочернем классе? Это просто подмена одной проблемы другой.

3Dave 28.11.2022 22:14
You would never use inheritance to introduce properties of any kind. Да, это просто неправильно. Свойства — это синтаксический сахар для полей с геттерами и сеттерами. Ваше утверждение означает, что поля не имеют места в наследовании. Не дай Бог, мне потребуется общее поле, например «Имя» или «ID».
3Dave 28.11.2022 22:22

"Правда ли, что базовые классы должны содержать только переменные/свойства? которые используются ВСЕМ, что его наследует?"

Не совсем. Базовые классы являются "обычными" классами. Базовый класс — это просто суперкласс подкласса. Они содержат любой член. Как и классы в целом, они должны реализовывать все, чтобы выполнять свои обязанности.

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

Если Vehicle имеет Engine и может Move(), то он может быть базовым классом Car, Truck, Airplane, Train и т. д.

Как видите, подкласс расширяет базовый класс. Следуя хорошему дизайну класса, подклассы не должны наследовать бесполезные члены. Например, класс Airplane не должен наследовать функционал Dive(), потому что он бесполезен. Дизайн вашего класса должен учитывать такие случаи. Разумнее было бы переделать дерево наследования и ввести базовый класс для морских и воздушных судов.

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

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

Наследование является основой объектно-ориентированного программирования. Вы найдете достаточно ресурсов в Интернете, таких как электронные книги, блоги или учебные пособия, чтобы научить вас.

"Должен ли я включать это свойство только в классы, которые будут используя его (3 из 7 классов, которые действительно нуждаются в этом свойстве), или можно ли определить его в базовом классе (даже если 4 из 7 никогда не будет заботиться об этом)?"

Вы должны определить интерфейс IToggleable. Затем реализуйте этот интерфейс для тех типов, которые должны быть переключаемыми.

@MickyD «Если интерфейс только когда-либо реализован и никогда не используется где-либо еще» - пожалуйста, перестаньте придумывать истории. Я не обсуждаю предположения. Вы должны понимать, что нет правила, где и зачем использовать интерфейсы. Если типу требуется определенное свойство для участия в моей инфраструктуре, я могу заставить его реализовать свойство. Некоторые называют это контрактом. Не уверен, почему вы считаете, что это не разрешено. Наследование — это только одно из решений. И это в значительной степени зависит от личного мнения, является ли наследование хорошим выбором дизайна в целом. Не уверен, что вы троллите.

BionicCode 28.11.2022 00:30
Ответ принят как подходящий

Вы должны использовать базовый класс для класса, которому необходимо реализовать одни и те же свойства/методы, и использовать интерфейс в вашем производном классе, которому нужны "специальные свойства"

public class UpgradeBase
{

// Implement common properties and method
    }

public interface IToggleable 
{
   bool IsToggleable { get; set; }
}

public class UpgradeBuilding : UpgradeBase, IToggleable
{
   //This class needs to access IsToggleable
   public bool IsToggleable { get; set; } // Interface implementation
}

public class UpgradeScience : UpgradeBase
{
   //This class does NOT need to access IsToggleable and never uses it
}

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

Nathan Bingham-Thomas 27.11.2022 22:50

@MickyD Бывают случаи, когда интерфейс определяет не методы, а свойства. Это особенно актуально для интерфейсов, связанных с фреймворком, где фреймворку необходимо установить/пометить клиентские объекты, которые участвуют в инфраструктуре фреймворка. INotifyPropertyChnaged WPF — хороший пример. Или IAsyncResult. Интерфейсы в целом — это просто контракт, гарантирующий, что разработчик определяет определенные члены.

BionicCode 27.11.2022 22:58

@MickyD Все идиомы «является», «имеет» или «может делать» полезны для начинающих, но они не охватывают всю концепцию или возможности. Эти идиомы пытаются описать отношения, а не шаблоны или лучшие практики.

BionicCode 27.11.2022 22:58

@MickyD С точки зрения полиморфного поведения мы можем сказать, что интерфейс также описывает отношение «является». Хотя интерфейс не обеспечивает классического наследования.

BionicCode 27.11.2022 23:01

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

BionicCode 27.11.2022 23:10

@MickyD «вы поймете, что INotifyPropertyChnaged представляет собой контракт события, который предназначен для использования в приемниках событий. Вы делали это все эти годы, не осознавая» - вам нравится искажать реальность до тех пор, пока она не будет соответствовать вашей потребности троллить других людей. Я привел вам пример интерфейса, который не содержит методов, потому что вы утверждали, что существует правило, согласно которому «интерфейсы также включают методы», подразумевая, что это обязательно. Тогда как насчет IAsyncResult? Вы полностью проигнорировали это, потому что это не соответствует вашим аргументам? en.wikipedia.org/wiki/Straw_man помните?

BionicCode 28.11.2022 00:37

@MickyD Будьте благословенны

BionicCode 28.11.2022 07:38

И теперь, если вам нужен UpgradeEnvironment, который является IToggleable, вам придется повторно реализовать поле и любые методы доступа — или наследовать от UpgradeBuilding. Это не решает общий случай.

3Dave 28.11.2022 22:17

Если вы думаете о «базовом» классе, то я думаю, что базовый класс будет именно таким — классом, который заботится и знает только о себе. Он должен быть минималистичным в том смысле, что он включает только те функции, которые делают его жизнеспособным.

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

Вот тут-то и пригодится интерфейс. Давайте определим интерфейс для описания этой «характеристики»:

public interface IToggleable
{
    // Define a readable property.
    bool isToggleable { get; }
}

Теперь ваши примеры случаев будут выглядеть так:

public class UpgradeBase
{
   // core base features ..
}

public class UpgradeBuilding : UpgradeBase, IToggleable
{
   public bool isToggleable { get; private set; }
}

public class UpgradeScience : UpgradeBase
{
   // ..
}

Мы могли бы даже расширить идею «характеристики», определив новый интерфейс:

public interface IMovable { }

Если бы мы теперь применили этот интерфейс к производному классу:

public class NewClass : UpgradeBase, IMoveable
{
    // a UpgradeBase that is also a IMoveable
}

Например, в нашем коде теперь мы можем хранить список UpgradeBase и проверять, не являются ли какие-либо из них также IMoveable такими:

// Populate this list with all UpgradeBase and derived class instances
List<UpgradeBase> list;

foreach ( var item in list )
{
    if ( item is IMoveable )
    {
        // this item is moveable.
    }

}

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

Другие вопросы по теме