Чому "остаточне" ключове слово коли-небудь буде корисним?


54

Здається, у Java було повноваження оголошувати класи незмінними для віків, і тепер у C ++ також є. Однак, з огляду на принцип "Відкрити / закрити" в SOLID, чому це було б корисно? Для мене finalключове слово звучить так само friend- воно легальне, але якщо ви його використовуєте, швидше за все, дизайн неправильний. Наведіть декілька прикладів, коли невід'ємний клас буде частиною чудової архітектури чи дизайну.


43
Чому ви вважаєте, що клас неправильно розроблений, якщо на нього зазначається final? Багато людей (включаючи мене) вважають, що це гарний дизайн, щоб зробити кожен не абстрактний клас final.
Виявлено

20
Улюблена композиція над спадщиною, і у вас може бути кожен неабразований клас final.
Енді

24
Принцип відкриття / закриття є в певному сенсі анахронізмом XX століття, коли мантра повинна була скласти ієрархію класів, які успадковувались від класів, які в свою чергу успадковувались від інших класів. Це було приємно для викладання об’єктно-орієнтованого програмування, але виявилося, що він створює заплутані, незрозумілі помилки при застосуванні до реальних проблем. Сформувати клас, який можна розширити, важко.
Девід Хаммен

34
@DavidArno Не будь смішним. Спадщина - це умова не об'єктно-орієнтованого програмування, і воно ніде не є таким складним і безладним, як люблять проповідувати деякі надмірно догматичні особи. Це інструмент, як і будь-який інший, і хороший програміст знає, як правильно використовувати інструмент для роботи.
Мейсон Уілер

48
Як розробник, що одужує, я, начебто, згадую, що остаточний був блискучим інструментом для запобігання потрапляння шкідливої ​​поведінки у критичні секції. Я також, здається, згадую, що спадщина є потужним інструментом у різних напрямках. Це майже як якби GASP різних інструментів мають свої плюси і мінуси , і ми як інженери повинні збалансувати ці чинники , як ми виробляємо наше програмне забезпечення!
corsiKa

Відповіді:


136

final висловлює наміри . Він повідомляє користувачеві класу, методу чи змінної: "Цей елемент не повинен змінюватися, і якщо ви хочете його змінити, ви не зрозуміли існуючий дизайн".

Це важливо, тому що архітектура програми була б дійсно, дуже складно, якби ви передбачили, що кожен клас та кожен метод, який ви коли-небудь пишете, можуть бути змінені, щоб зробити щось зовсім інше підкласом. Набагато краще заздалегідь визначити, які елементи можуть бути змінними, а які ні, та застосувати незмінність через final.

Ви також можете це зробити за допомогою коментарів та архітектурних документів, але завжди краще дозволити компілятору виконувати всі необхідні речі, ніж сподіватися, що майбутні користувачі прочитають та підкоряться документації.


14
Так би і я. Але кожен, хто коли-небудь писав широко використовуваний базовий клас (наприклад, фреймворк або медіатека), знає краще, ніж сподіватися, що програмісти прикладних програм поводяться здорово. Вони підірватимуть, неправильно використовуватимуть і спотворюватимуть ваш винахід так, як ви навіть не вважали можливим, якщо не зафіксуєте його залізною ручкою.
Кіліан Фот

8
@KilianFoth Добре, але чесно, як у вас проблема, якою займаються програмісти програм?
coredump

20
@coredump Люди, які використовують мою бібліотеку, погано створюють погані системи. Погані системи породжують погану репутацію. Користувачі не зможуть відрізнити чудовий код Кіліана від жахливо нестабільного додатка Фреда Рандома. Результат: Я програю в програмах кредитів та клієнтів. Чому ваш код важко зловживати - це питання доларів та центів.
Кіліан Фот

21
Заява "Цей елемент не повинен змінюватися, і якщо ви хочете його змінити, ви не зрозуміли існуючий дизайн". це неймовірно зарозуміло і не таке ставлення, яке я хотів би, як частина будь-якої бібліотеки, з якою працював. Якби я мав десятиліття щоразу, коли якась смішна перенакопчена бібліотека не залишала мені механізму змінити якийсь фрагмент внутрішнього стану, який потрібно змінити, оскільки автору не вдалося передбачити важливий випадок використання ...
Мейсон Уілер

31
@JimmyB Правило: якщо ви думаєте, що знаєте, для чого буде використовуватися ваше творіння, ви вже помиляєтесь. Белл задумав телефон як по суті музакську систему. Kleenex був винайдений з метою більш зручного зняття макіяжу. Томас Уотсон, президент IBM, одного разу сказав: "Я думаю, що існує світовий ринок, можливо, для п'яти комп'ютерів". Представник Джим Сенсенбреннер, який представив закон про PATRIOT в 2001 році, говорить про те, що це було спеціально покликане запобігти НСА робити те, що вони роблять, "з повноваженнями PATRIOT". І так далі ...
Мейсон Уілер

59

Це дозволяє уникнути нестабільної проблеми базового класу . Кожен клас має набір неявних або явних гарантій та інваріантів. Принцип заміщення Ліскова передбачає, що всі підтипи цього класу також повинні забезпечувати всі ці гарантії. Однак порушити це дійсно легко, якщо ми не використовуємо final. Наприклад, давайте перевіримо пароль:

public class PasswordChecker {
  public boolean passwordIsOk(String password) {
    return password == "s3cret";
  }
}

Якщо ми дозволимо скасувати цей клас, одна реалізація може заблокувати всіх, а інша може дати доступ усім:

public class OpenDoor extends PasswordChecker {
  public boolean passwordIsOk(String password) {
    return true;
  }
}

Зазвичай це не нормально, оскільки зараз у підкласах є поведінка, яка дуже несумісна з оригіналом. Якщо ми дійсно маємо намір розширити клас іншою поведінкою, краще буде ланцюжок відповідальності:

PasswordChecker passwordChecker =
  new DefaultPasswordChecker(null);
// or:
PasswordChecker passwordChecker =
  new OpenDoor(null);
// or:
PasswordChecker passwordChecker =
 new DefaultPasswordChecker(
   new OpenDoor(null)
 );

public interface PasswordChecker {
  boolean passwordIsOk(String password);
}

public final class DefaultPasswordChecker implements PasswordChecker {
  private PasswordChecker next;

  public DefaultPasswordChecker(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    if ("s3cret".equals(password)) return true;
    if (next != null) return next.passwordIsOk(password);
    return false;
  }
}

public final class OpenDoor implements PasswordChecker {
  private PasswordChecker next;

  public OpenDoor(PasswordChecker next) {
    this.next = next;
  }

  @Override
  public boolean passwordIsOk(String password) {
    return true;
  }
}

Проблема стає більш очевидною, коли складніший клас називає власні методи, і ці методи можуть бути відмінені. Іноді я стикаюся з цим, коли гарненько друкує структуру даних або пише HTML. Кожен метод відповідає за якийсь віджет.

public class Page {
  ...;

  @Override
  public String toString() {
    PrintWriter out = ...;
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    writeHeader(out);
    writeMainContent(out);
    writeMainFooter(out);
    out.print("</body>");

    out.print("</html>");
    ...
  }

  void writeMainContent(PrintWriter out) {
    out.print("<div class='article'>");
    out.print(htmlEscapedContent);
    out.print("</div>");
  }

  ...
}

Тепер я створюю підклас, який додає трохи більше стилів:

class SpiffyPage extends Page {
  ...;


  @Override
  void writeMainContent(PrintWriter out) {
    out.print("<div class='row'>");

    out.print("<div class='col-md-8'>");
    super.writeMainContent(out);
    out.print("</div>");

    out.print("<div class='col-md-4'>");
    out.print("<h4>About the Author</h4>");
    out.print(htmlEscapedAuthorInfo);
    out.print("</div>");

    out.print("</div>");
  }
}

Тепер на хвилину ігноруючи, що це не дуже хороший спосіб генерування HTML-сторінок, що станеться, якщо я хочу ще раз змінити макет? Я мав би створити SpiffyPageпідклас, який якимось чином перекриває цей вміст. Тут ми можемо побачити випадкове застосування шаблону методу шаблону. Методи шаблонів - це чітко визначені точки розширення в базовому класі, які призначені для перевизначення.

А що станеться, якщо базовий клас зміниться? Якщо вміст HTML зміниться занадто сильно, це може порушити макет, передбачений підкласами. Тому змінити базовий клас після цього не дуже безпечно. Це не очевидно, якщо всі ваші заняття в одному проекті, але дуже помітно, якщо базовий клас є частиною опублікованого програмного забезпечення, яке розробляють інші люди.

Якби ця стратегія розширення була призначена, ми могли б дозволити користувачу змінювати спосіб створення кожної частини. Будь-яка стратегія для кожного блоку може бути надана зовні. Або ми могли гніздити декораторів. Це було б еквівалентно вищевказаному коду, але набагато більш чітко і набагато гнучкішим:

Page page = ...;
page.decorateLayout(current -> new SpiffyPageDecorator(current));
print(page.toString());

public interface PageLayout {
  void writePage(PrintWriter out, PageLayout top);
  void writeMainContent(PrintWriter out, PageLayout top);
  ...
}

public final class Page {
  private PageLayout layout = new DefaultPageLayout();

  public void decorateLayout(Function<PageLayout, PageLayout> wrapper) {
    layout = wrapper.apply(layout);
  }

  ...
  @Override public String toString() {
    PrintWriter out = ...;
    layout.writePage(out, layout);
    ...
  }
}

public final class DefaultPageLayout implements PageLayout {
  @Override public void writeLayout(PrintWriter out, PageLayout top) {
    out.print("<!DOCTYPE html>");
    out.print("<html>");

    out.print("<head>");
    out.print("</head>");

    out.print("<body>");
    top.writeHeader(out, top);
    top.writeMainContent(out, top);
    top.writeMainFooter(out, top);
    out.print("</body>");

    out.print("</html>");
  }

  @Override public void writeMainContent(PrintWriter out, PageLayout top) {
    ... /* as above*/
  }
}

public final class SpiffyPageDecorator implements PageLayout {
  private PageLayout inner;

  public SpiffyPageDecorator(PageLayout inner) {
    this.inner = inner;
  }

  @Override
  void writePage(PrintWriter out, PageLayout top) {
    inner.writePage(out, top);
  }

  @Override
  void writeMainContent(PrintWriter out, PageLayout top) {
    ...
    inner.writeMainContent(out, top);
    ...
  }
}

(Додатковий topпараметр необхідний для того, щоб переконатися, що дзвінки writeMainContentпроходять у верхній частині ланцюжка декораторів. Це імітує особливість підкласифікації, що називається відкритою рекурсією .)

Якщо у нас є кілька декораторів, то тепер ми можемо вільніше їх перемішувати.

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

final class Thingies implements Iterable<Thing> {
  private ArrayList<Thing> thingList = new ArrayList<>();

  @Override public Iterator<Thing> iterator() {
    return thingList.iterator();
  }

  public void add(Thing thing) {
    thingList.add(thing);
  }

  ... // custom methods
}

Натомість вони створили підклас:

class Thingies extends ArrayList<Thing> {
  ... // custom methods
}

Це раптом означає, що весь інтерфейс ArrayListстав частиною нашого інтерфейсу. Користувачі можуть remove()робити речі чи get()речі за певними показниками. Це було призначено саме так? ГАРАЗД. Але часто ми не ретельно продумуємо всі наслідки.

Тому доцільно

  • ніколи не extendклас без ретельної думки.
  • завжди позначайте свої класи як finalза винятком випадків, коли ви плануєте перекрити будь-який метод.
  • створити інтерфейси, де потрібно замінити реалізацію, наприклад для тестування одиниць.

Існує багато прикладів, коли це "правило" потрібно порушувати, але воно, як правило, спрямовує вас на хороший, гнучкий дизайн і дозволяє уникнути помилок через ненавмисні зміни базових класів (або ненавмисне використання підкласу як екземпляр базового класу ).

Деякі мови мають більш жорсткі механізми правозастосування:

  • Усі методи за замовчуванням є остаточними і мають бути позначені явно як virtual
  • Вони забезпечують приватне успадкування, яке не успадковує інтерфейс, а лише імплементацію.
  • Вони вимагають, щоб методи базового класу були позначені як віртуальні, і вимагають також позначати всі зміни. Це дозволяє уникнути проблем, коли підклас визначав новий метод, але метод з тією ж підписом пізніше був доданий до базового класу, але не призначений як віртуальний.

3
Ви заслуговуєте принаймні +100 на згадку про "тендітну проблему базового класу". :)
Девід Арно

6
Я не переконаний у наведених тут пунктах. Так, крихкий базовий клас є проблемою, але остаточний не вирішує всі проблеми зі зміною реалізації. Ваш перший приклад поганий, оскільки ви припускаєте, що знаєте всі можливі випадки використання пароля PasswordChecker ("заблокувати всіх або дозволити всім доступ ... не в порядку" - каже хто?). Ваш останній список "тому доцільно ..." насправді поганий - ви в основному виступаєте за те, щоб нічого не продовжувати, і все позначати як остаточне - що повністю знищує корисність OOP, успадкування та повторного використання коду.
Адельф

4
Ваш перший приклад не є прикладом крихкої проблеми базового класу. У крихкій проблемі базового класу зміни базового класу розбивають підклас. Але в цьому прикладі ваш підклас не відповідає договору підкласу. Це дві різні проблеми. (Крім того, насправді розумно, що за певних обставин ви можете відключити перевірку паролів (скажімо, для розробки))
Winston Ewert

5
"Це дозволяє уникнути крихкої проблеми базового класу" - таким же чином, як і вбивство, і уникнути голоду.
користувач253751

9
@immibis, це більше схоже на те, щоб уникати їжі, щоб уникнути отруєння їжею. Звичайно, ніколи не їсти було б проблемою. Але їсти лише в тих місцях, яким ви довіряєте, має багато сенсу.
Вінстон Еверт

33

Я здивований, що ще ніхто не згадав « Ефективна Java», 2-е видання Джошуа Блоха (яке потрібно прочитати хоча б для кожного розробника Java). Пункт 17 у книзі детально обговорює це і має назву: " Дизайн та документ на спадщину чи інше забороняють ".

Я не повторю всіх хороших порад у книзі, але ці конкретні пункти здаються актуальними:

А як щодо звичайних занять з бетону? Традиційно вони не є ні остаточними, ні розробленими та задокументованими для підкласу, але такий стан справ є небезпечним. Кожен раз, коли відбувається зміна в такому класі, є ймовірність, що класи клієнтів, що розширюють клас, зламаються. Це не просто теоретична проблема. Не рідкість отримання звітів про помилки, пов’язані з підкласифікацією, після зміни внутрішніх класів нефінального конкретного класу, які не були розроблені та задокументовані для успадкування.

Найкраще рішення цієї проблеми - заборонити підкласи в класах, які не розроблені та задокументовані для безпечного підкласу. Є два способи заборонити субкласифікацію. Простіше з них - оголосити клас остаточним. Альтернативою є зробити всі конструктори приватними або приватними пакетами та замість конструкторів додати загальнодоступні статичні заводи. Ця альтернатива, яка забезпечує гнучкість використання підкласів внутрішньо, розглядається в пункті 15. Будь-який підхід прийнятний.


21

Однією з причин finalкорисної є те, що вона гарантує, що ви не можете підкласирувати клас таким чином, що порушить контракт батьківського класу. Такий підклас був би порушенням SOLID (найбільше "L"), а створення класу finalзаважає цьому.

Одним із типових прикладів є неможливість підкласу незмінного класу таким чином, що зробить підклас змінним. У певних випадках така зміна поведінки може призвести до дуже дивних ефектів, наприклад, коли ви використовуєте щось як клавіші на карті, думаючи, що ключ є незмінним, а насправді ви використовуєте підклас, який є змінним.

У Java могло бути представлено багато цікавих проблем безпеки, якщо ви змогли підкласирувати Stringта зробити його мінливим (або змусили його передзвонити додому, коли хтось викликає його методи, таким чином, можливо, витягуючи конфіденційні дані із системи), коли ці об’єкти передаються навколо деякого внутрішнього коду, пов’язаного з завантаженням класу та безпекою.

Фінал також іноді корисний для запобігання простих помилок, таких як повторне використання однієї і тієї ж змінної для двох речей у методі тощо. У Scala вам пропонується використовувати лише ті, valщо приблизно відповідають кінцевим змінним на Java, а фактично будь-яке використання varабо на не остаточну змінну дивляться з підозрою.

Нарешті, компілятори можуть, принаймні теоретично, здійснити деякі додаткові оптимізації, коли вони знають, що клас або метод є остаточним, оскільки, коли ви викликаєте метод на остаточному класі, ви точно знаєте, який метод буде викликаний і не потрібно йти через віртуальну таблицю методів для перевірки спадкування.


6
Нарешті, компілятори можуть, принаймні теоретично => Я особисто переглянув пропуск девіатуризації Кланг, і я підтверджую, що він використовується на практиці.
Матьє М.

Але чи не може компілятор заздалегідь сказати, що ніхто не перевершує клас чи метод незалежно від того, чи є його відмічений остаточний?
JesseTG

3
@JesseTG Якщо він, мабуть, має доступ до всього коду одночасно. А як щодо окремої компіляції файлів?
Відновіть Моніку

3
Девіртуалізація @JesseTG або (мономорфне / поліморфне) вбудоване кешування - це звичайна техніка в компіляторах JIT, оскільки система знає, які класи завантажуються в даний час, і може деоптимізувати код, якщо припущення про відсутність переважаючих методів виявиться помилковим. Однак достроковий компілятор не може. Коли я компілюю один клас Java і код, який використовує цей клас, я можу згодом скласти підклас і передати екземпляр споживчому коду. Найпростіший спосіб - додати ще одну банку на передню частину класного шляху. Компілятор не може знати про все це, оскільки це відбувається під час виконання.
амон

5
@MTilsted Ви можете припустити, що рядки незмінні, оскільки мутація рядків за допомогою відображення може бути заборонена через a SecurityManager. Більшість програм не використовують його, але вони також не виконують жодного ненадійного коду. +++ Ви повинні вважати, що рядки незмінні, оскільки в іншому випадку ви отримуєте нульову безпеку та купу довільних помилок як бонус. Будь-який програміст, припускаючи, що рядки Java можуть змінити продуктивний код, безумовно, божевільний.
maaartinus

7

Друга причина - продуктивність . Перша причина полягає в тому, що деякі класи мають важливі поведінки або стани, які не слід змінювати, щоб система могла працювати. Наприклад, якщо у мене є клас "PasswordCheck" і для створення цього класу я найняв команду експертів з безпеки, і цей клас спілкується із сотнями банкоматів з добре вивченими та визначеними протоколами. Дозволити новому найманому хлопцеві, що не в університеті, зробити клас "TrustMePasswordCheck", який розширює вищевказаний клас, може бути дуже шкідливим для моєї системи; ці методи не слід перекривати, ось і все.


1
bancomat = Банкомат: en.wikipedia.org/wiki/Cash_machine
тар

JVM досить розумний, щоб розглядати класи як кінцеві, якщо в них немає підкласів, навіть якщо вони не оголошені як остаточні.
користувач253751

Оскаржений, тому що заява про ефективність була спростована стільки разів.
Пол Сміт

7

Коли мені потрібен клас, я напишу клас. Якщо мені не потрібні підкласи, я не дбаю про підкласи. Я переконуюсь, що мій клас поводиться так, як задумано, а місця, де я використовую клас, припускають, що клас поводиться так, як задумано.

Якщо хтось хоче субкласифікувати свій клас, я хочу повністю заперечувати будь-яку відповідальність за те, що відбувається. Я досягаю цього, роблячи клас "остаточним". Якщо ви хочете підкласирувати його, пам’ятайте, що я не брав до уваги підкласинг під час написання класу. Тож вам доведеться взяти вихідний код класу, видалити "остаточний", і з цього моменту все, що трапляється, є повністю вашою відповідальністю .

Ви думаєте, що це "не об'єктно орієнтоване"? Мені заплатили за те, щоб зробити клас, який робить те, що він повинен робити. Ніхто не платив мені за те, щоб зробити клас, який можна було б класифікувати. Якщо вам платять, щоб зробити мій клас багаторазовим, ви можете це зробити. Почніть з видалення «остаточного» ключового слова.

. і може замінити динамічну диспетчеризацію на статичну (крихітна вигода) і часто замінити статичну диспетчеру на вкладену (можливо, величезну користь)).

adelphus: Що так важко зрозуміти про "якщо ви хочете підкласирувати його, візьміть вихідний код, видаліть" остаточний ", і це ваша відповідальність"? "final" дорівнює "справедливому попередження".

І мені не платять за те, щоб зробити багаторазовий код. Мені платять писати код, який робить те, що він повинен робити. Якщо мені платять, щоб зробити два подібних біта коду, я витягую загальні частини, тому що це дешевше, і я не плачу витрачати свій час. Перетворення коду для багаторазового використання, який не використовується повторно, - це марна трата мого часу.

M4ks: Ви завжди робите все приватне, що не має доступу до нього ззовні. Знову ж таки, якщо ви хочете підклас, ви берете вихідний код, змінюєте речі на "захищені", якщо вам потрібно, і берете на себе відповідальність за те, що ви робите. Якщо ви вважаєте, що вам потрібно отримати доступ до речей, які я позначив приватними, то краще знайте, що ви робите.

І те й інше: Підкласифікація - це крихітна, крихітна частина повторного використання коду. Створення будівельних блоків, які можна адаптувати без підкласифікації, набагато потужніше та значно виграє від "остаточного", оскільки користувачі блоків можуть розраховувати на те, що отримують.


4
-1 Ця відповідь описує все, що не так у розробників програмного забезпечення. Якщо хтось хоче повторно використовувати ваш клас шляхом підкласифікації, дозвольте їм. Чому саме ви відповідаєте за те, як вони використовують (або зловживають) ним? В основному, ви використовуєте фінал так, як вам, ви не використовуєте мій клас. * "Ніхто не заплатив мені за те, щоб зробити клас, який можна було б класифікувати" . Ти серйозно? Ось саме для цього працюють інженери програмного забезпечення - для створення міцного коду, який може використовуватись повторно
Адельф

4
-1 Просто зробіть все приватним, щоб ніхто навіть і не думав про підкласифікацію ..
M4ks

3
@adelphus Хоча формулювання цієї відповіді тупа, що межує з суворим, це не "неправильна" точка зору. Насправді це та сама точка зору, як і більшість відповідей на це питання поки що лише з менш клінічним тоном.
NemesisX00

+1 за згадування про те, що ви можете видалити "остаточний". Зарозуміло заявляти про попереднє знання всіх можливих застосувань вашого коду. І все-таки скромно пояснити, що ви не можете підтримувати деякі можливі можливості використання, і що для цих застосувань потрібно буде підтримувати вилку.
gmatht

4

Уявімо, що SDK для платформи постачає наступний клас:

class HTTPRequest {
   void get(String url, String method = "GET");
   void post(String url) {
       get(url, "POST");
   }
}

Підкласи додатків цього класу:

class MyHTTPRequest extends HTTPRequest {
    void get(String url, String method = "GET") {
        requestCounter++;
        super.get(url, method);
    }
}

Все добре і добре, але хтось, що працює над SDK, вирішує, що передача методу getє нерозумною, і робить інтерфейс кращим, забезпечуючи забезпечення зворотної сумісності.

class HTTPRequest {
   @Deprecated
   void get(String url, String method) {
        request(url, method);
   }

   void get(String url) {
       request(url, "GET");
   }
   void post(String url) {
       request(url, "POST");
   }

   void request(String url, String method);
}

Все здається нормальним, доки додаток зверху не буде перекомпільовано з новим SDK. Раптом метод переопределення get вже не викликається, а запит не враховується.

Це називається крихкою проблемою базового класу, тому що, здавалося б, невинна зміна призводить до розриву підкласу. Будь-яка зміна методів виклику всередині класу може призвести до розриву підкласу. Це означає, що майже будь-яка зміна може призвести до розриву підкласу.

Фінал не дозволяє комусь підкласифікувати ваш клас. Таким чином, які методи всередині класу можна змінити, не переживаючи, що десь хтось залежить від того, який саме виклик методу робиться.


1

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

Остаточний означає, що ви можете змінювати, як працює ваш клас, без будь-яких ненавмисних змін у поведінці, повзаючи в код інших людей, який покладається на ваш як на базу.

Як приклад, я пишу клас під назвою HobbitKiller, що чудово, адже всі хобіти є хитрощами і, ймовірно, повинні померти. Подрастуйте це, всі вони обов'язково повинні померти.

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

Через три місяці я змінив реалізацію свого методу націлювання. Тепер, у майбутній момент, коли оновлення вашої бібліотеки, невідоме вам, фактична реалізація вашого класу принципово змінилася через зміну методу суперкласу, від якого ви залежите (і взагалі не контролюєте).

Тому для мене, щоб бути сумлінним розробником і забезпечити плавну смерть хобіта в майбутньому, використовуючи свій клас, я повинен бути дуже, дуже обережним з будь-якими змінами, які я вношу до будь-яких класів, які можна продовжити.

Знімаючи здатність до розширення, за винятком випадків, коли я спеціально маю намір розширити клас, я врятую себе (і, сподіваюся, іншим) багато головних болів.


2
Якщо метод націлювання зміниться, чому ви коли-небудь оприлюднювали його? І якщо ви істотно зміните поведінку класу, вам потрібна інша версія, а не надмірно final
захисна

Я поняття не маю, чому це зміниться. Хобіти - це хитрість, і потрібні зміни. Справа в тому, що якщо я будую це як остаточне, я запобігаю спадку, який захищає інших людей від того, щоб мої зміни заразили їх код.
Скотт Тейлор

0

Використання finalжодним чином не є порушенням принципів SOLID. На жаль, надзвичайно часто трактувати відкритий / закритий принцип ("програмні об'єкти повинні бути відкритими для розширення, але закриті для модифікації") як значення ", а не змінювати клас, підклас його та додавати нові функції". Спочатку це не означало це, і зазвичай вважається не найкращим підходом до досягнення своїх цілей.

Найкращий спосіб дотримання OCP - це проектування точок розширення в клас, спеціально надаючи абстрактні форми поведінки, які параметризуються шляхом введення залежності до об'єкта (наприклад, використовуючи шаблон проектування стратегії). Ці поведінки повинні бути розроблені для використання інтерфейсу, щоб нові реалізації не покладалися на спадкування.

Інший підхід - реалізувати свій клас із його публічним API як абстрактний клас (або інтерфейс). Потім ви можете створити абсолютно нову реалізацію, яка може підключатися до тих самих клієнтів. Якщо ваш новий інтерфейс вимагає майже подібної поведінки, як оригінал, ви можете:

  • використовувати шаблон дизайну Decorator, щоб повторно використовувати існуючу поведінку оригіналу, або
  • рефакторинг частин поведінки, які ви хочете зберегти в помічному об’єкті, і використовувати той самий помічник у вашій новій реалізації (рефакторинг не є модифікацією).

0

Для мене це питання дизайну.

Припустимо, у мене є програма, яка розраховує зарплату працівникам. Якщо у мене є клас, який повертає кількість робочих днів між двома датами, виходячи з країни (по одному класу для кожної країни), я поставлю цей підсумковий і запропоную метод для кожного підприємства, щоб забезпечити вільний день лише для своїх календарів.

Чому? Простий. Скажімо, розробник хоче успадкувати базовий клас WorkingDaysUSA у класі WorkingDaysUSAmyCompany і змінити його, щоб відображати, що його підприємство буде закрито для страйку / обслуговування / з будь-якої причини 2-го числа мар.

Розрахунки для замовлень та доставки клієнтів відображатимуть затримку та працюватимуть відповідно, коли під час виконання вони викликають WorkingDaysUSAmyCompany.getWorkingDays (), але що відбувається, коли я підраховую час відпусток? Чи варто додати 2-е марта як свято для всіх? Ні. Але оскільки програміст використовував спадкування, і я не захищав клас, це може призвести до плутанини.

Або скажімо, що вони успадковують та модифікують клас так, щоб відображати, що ця компанія не працює суботами, де в країні вони працюють в половину часу в суботу. Тоді землетрус, електрична криза чи якась обставина змушує президента оголосити три неробочі дні, як це відбулося нещодавно у Венесуелі. Якщо метод успадкованого класу вже віднімається щосуботи, мої модифікації початкового класу можуть призвести до віднімання того ж дня двічі. Мені доведеться перейти до кожного підкласу кожного клієнта і переконатися, що всі зміни сумісні.

Рішення? Зробіть клас остаточним і введіть метод addFreeDay (companyID mycompany, Date freeDay). Таким чином ви впевнені, що при виклику класу WorkingDaysCountry це ваш основний клас, а не підклас

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.