Làm enums tạo giao diện giòn?


17

Hãy xem xét ví dụ dưới đây. Mọi thay đổi đối với enum ColorChoice đều ảnh hưởng đến tất cả các lớp con IWindowColor.

Làm enums có xu hướng gây ra giao diện giòn? Có một cái gì đó tốt hơn một enum để cho phép linh hoạt đa hình hơn?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

Chỉnh sửa: xin lỗi vì đã sử dụng màu sắc làm ví dụ của tôi, đó không phải là câu hỏi. Dưới đây là một ví dụ khác để tránh cá trích đỏ và cung cấp thêm thông tin về ý nghĩa của sự linh hoạt.

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

Hơn nữa, hãy tưởng tượng rằng các tay cầm cho lớp con IS SomethingThatNeedToKnowWhatTypeOfCharacter thích hợp được đưa ra bởi một mẫu thiết kế nhà máy. Bây giờ tôi có một API không thể mở rộng trong tương lai cho một ứng dụng khác trong đó các loại ký tự được phép là {human, lùn}.

Chỉnh sửa: Chỉ để cụ thể hơn về những gì tôi đang làm. Tôi đang thiết kế một ràng buộc mạnh mẽ của đặc tả ( MusicXML ) này và tôi đang sử dụng các lớp enum để biểu diễn các loại đó trong đặc tả được khai báo với xs: enumutions. Tôi đang cố gắng nghĩ về những gì sẽ xảy ra khi phiên bản tiếp theo (4.0) ra mắt. Thư viện lớp của tôi có thể làm việc ở chế độ 3.0 và ở chế độ 4.0 không? Nếu phiên bản tiếp theo tương thích ngược 100% thì có thể. Nhưng nếu giá trị liệt kê bị xóa khỏi đặc tả thì tôi đã chết trong nước.


2
Khi bạn nói "tính linh hoạt đa hình", bạn có khả năng gì trong đầu?
Ixrec


3
Sử dụng một enum cho màu sắc tạo ra các giao diện dễ vỡ, không chỉ là "sử dụng một enum".
Doc Brown

3
Thêm một biến thể enum mới phá vỡ mã bằng cách sử dụng enum đó. Mặt khác, việc thêm một thao tác mới vào enum là hoàn toàn khép kín, vì tất cả các trường hợp cần xử lý đều ở ngay đó (tương phản điều này với các giao diện và siêu lớp, trong đó việc thêm một phương thức không mặc định là một thay đổi nghiêm trọng). Nó phụ thuộc vào loại thay đổi thực sự cần thiết.

1
Re MusicXML: Nếu không có cách nào dễ dàng để biết từ các tệp XML mà phiên bản của lược đồ mà mỗi người đang sử dụng, thì đó là một lỗ hổng thiết kế quan trọng trong đặc tả. Nếu bạn phải giải quyết nó bằng cách nào đó, có lẽ chúng tôi không có cách nào để giúp cho đến khi chúng tôi biết chính xác những gì họ sẽ chọn để phá vỡ trong 4.0 và bạn có thể hỏi về một vấn đề cụ thể mà nó gây ra.
Ixrec

Câu trả lời:


25

Khi được sử dụng đúng cách, enums dễ đọc và mạnh mẽ hơn nhiều so với "số ma thuật" mà chúng thay thế. Tôi thường không thấy họ làm cho mã dễ vỡ hơn. Ví dụ:

  • setColor () không phải lãng phí thời gian kiểm tra xem valuegiá trị màu có hợp lệ hay không. Trình biên dịch đã làm điều đó.
  • Bạn có thể viết setColor (Color :: Red) thay vì setColor (0). Tôi tin rằng enum classtính năng trong C ++ hiện đại thậm chí cho phép bạn buộc mọi người luôn viết cái trước thay vì cái sau.
  • Thường không quan trọng, nhưng hầu hết các enum có thể được thực hiện với bất kỳ loại tích phân kích thước nào, vì vậy trình biên dịch có thể chọn bất kỳ kích thước nào thuận tiện nhất mà không buộc bạn phải suy nghĩ về những điều đó.

Tuy nhiên, sử dụng enum cho màu sắc là nghi vấn bởi vì trong nhiều tình huống (hầu hết?), Không có lý do gì để giới hạn người dùng trong một nhóm màu nhỏ như vậy; bạn cũng có thể để chúng vượt qua trong mọi giá trị RGB tùy ý. Trong các dự án tôi làm việc cùng, một danh sách nhỏ các màu như thế này sẽ chỉ xuất hiện như một phần của một "chủ đề" hoặc "phong cách" được cho là hoạt động như một sự trừu tượng mỏng trên các màu cụ thể.

Tôi không chắc câu hỏi "linh hoạt đa hình" của bạn là gì. Enums không có bất kỳ mã thực thi nào, vì vậy không có gì để tạo đa hình. Có lẽ bạn đang tìm kiếm mẫu lệnh ?

Chỉnh sửa: Sau khi chỉnh sửa, tôi vẫn chưa rõ loại khả năng mở rộng nào bạn đang tìm kiếm, nhưng tôi vẫn nghĩ rằng mẫu lệnh là thứ gần nhất bạn sẽ nhận được "enum đa hình".


Tôi có thể tìm hiểu thêm về việc không cho phép 0 vượt qua như một enum ở đâu?
TankorSmash

5
@TankorSmash C ++ 11 đã giới thiệu "lớp enum", còn được gọi là "enum enoped" không thể được chuyển đổi hoàn toàn thành kiểu số cơ bản của chúng. Họ cũng tránh làm ô nhiễm không gian tên như các kiểu "enum" kiểu C cũ đã làm.
Matthew James Briggs

2
Enums thường được hỗ trợ bởi số nguyên. Có rất nhiều điều kỳ lạ có thể xảy ra trong việc tuần tự hóa / giải tuần tự hóa hoặc đúc giữa các số nguyên và enum. Không phải lúc nào cũng an toàn khi cho rằng một enum sẽ luôn có giá trị hợp lệ.
Eric

1
Bạn nói đúng Eric (và đây là vấn đề tôi đã gặp phải một số lần). Tuy nhiên, bạn chỉ phải lo lắng về một giá trị có thể không hợp lệ ở giai đoạn khử lưu huỳnh. Tất cả những lần khác bạn sử dụng enums, bạn có thể giả sử giá trị là hợp lệ (ít nhất là đối với enum trong các ngôn ngữ như Scala - một số ngôn ngữ không có kiểu kiểm tra enum rất mạnh).
Kat

1
@Ixrec "bởi vì trong nhiều tình huống (hầu hết?), Không có lý do gì để giới hạn người dùng trong một nhóm màu nhỏ như vậy" Có những trường hợp hợp pháp. Net Bảng điều khiển giả lập các cửa sổ kiểu cũ console, mà chỉ có thể có văn bản trong một trong 16 màu (16 màu sắc của tiêu chuẩn CGA) msdn.microsoft.com/en-us/library/...
Pharap

15

Mọi thay đổi đối với enum ColorChoice đều ảnh hưởng đến tất cả các lớp con IWindowColor.

Không, nó không có. Có hai trường hợp: người thực hiện sẽ

  • lưu trữ, trả lại và chuyển tiếp các giá trị enum, không bao giờ hoạt động trên chúng, trong trường hợp đó chúng không bị ảnh hưởng bởi các thay đổi trong enum, hoặc

  • hoạt động trên các giá trị enum riêng lẻ, trong trường hợp bất kỳ thay đổi nào trong enum, tất nhiên, tất nhiên, không thể thiếu, nhất thiết , phải được tính bằng một thay đổi tương ứng trong logic của người thực hiện.

Nếu bạn đặt "Hình cầu", "Hình chữ nhật" và "Kim tự tháp" trong enum "Hình dạng" và chuyển một enum như vậy cho một số drawSolid()chức năng mà bạn đã viết để vẽ vật rắn tương ứng, và sau đó vào một buổi sáng, bạn quyết định thêm " Giá trị của hàm số đối với enum, bạn không thể mong đợi drawSolid()hàm vẫn không bị ảnh hưởng; Nếu bạn thực sự mong đợi nó bằng cách nào đó vẽ các icositetrahedron mà không cần bạn phải viết mã thực tế để vẽ các icositetrahedrons, thì đó là lỗi của bạn, không phải lỗi của enum. Vì thế:

Làm enums có xu hướng gây ra giao diện giòn?

Không, họ không. Điều gây ra các giao diện dễ vỡ là các lập trình viên nghĩ mình là ninja, cố gắng biên dịch mã của họ mà không kích hoạt đủ cảnh báo. Sau đó, trình biên dịch không cảnh báo cho họ rằng drawSolid()hàm của chúng chứa một switchcâu lệnh thiếu một casemệnh đề cho giá trị enum "Ikositetrahedron" mới được thêm vào.

Cách thức hoạt động của nó tương tự như việc thêm một phương thức ảo thuần túy mới vào một lớp cơ sở: sau đó bạn phải thực hiện phương thức này trên mỗi người thừa kế duy nhất, nếu không thì dự án không, và không nên, xây dựng.


Bây giờ, sự thật được nói, enums không phải là một cấu trúc hướng đối tượng. Chúng là một sự thỏa hiệp thực dụng giữa mô hình hướng đối tượng và mô hình lập trình có cấu trúc.

Cách làm việc hướng đối tượng thuần túy là hoàn toàn không có enum, và thay vào đó có tất cả các đối tượng.

Vì vậy, cách hoàn toàn hướng đối tượng để thực hiện ví dụ với chất rắn tất nhiên là sử dụng đa hình: thay vì có một phương pháp tập trung quái dị duy nhất biết cách vẽ mọi thứ và phải nói rằng vật rắn sẽ vẽ, bạn tuyên bố " Lớp "rắn" với một draw()phương thức trừu tượng (thuần ảo) , sau đó bạn thêm các lớp con "Hình cầu", "Hình chữ nhật" và "Kim tự tháp", mỗi lớp có cách triển khai riêng draw()để biết cách tự vẽ.

Theo cách này, khi bạn giới thiệu lớp con "Ikositetrahedron", bạn sẽ chỉ phải cung cấp một draw()hàm cho nó, và trình biên dịch sẽ nhắc bạn làm điều đó, bằng cách không cho phép bạn khởi tạo "Icositetrahedron" nếu không.


Bất kỳ lời khuyên về ném một cảnh báo thời gian biên dịch cho các công tắc? Tôi thường chỉ ném ngoại lệ thời gian chạy; nhưng thời gian biên dịch có thể là tuyệt vời! Không hoàn toàn tuyệt vời .. nhưng các bài kiểm tra đơn vị đến với tâm trí.
Vaughan Hilts 17/03/2015

1
Đã được một thời gian kể từ lần cuối tôi sử dụng C ++, vì vậy tôi không chắc chắn, nhưng tôi hy vọng rằng việc cung cấp tham số -Wall cho trình biên dịch và bỏ qua default:mệnh đề sẽ làm điều đó. Tìm kiếm nhanh về chủ đề không mang lại kết quả rõ ràng hơn, vì vậy điều này có thể phù hợp làm chủ đề của programmers SEcâu hỏi khác .
Mike Nakis

Trong C ++ có thể có một kiểm tra thời gian biên dịch nếu bạn kích hoạt nó. Trong C #, mọi kiểm tra sẽ phải có trong thời gian chạy. Bất cứ khi nào tôi lấy enum không gắn cờ làm tham số, tôi đảm bảo xác thực nó với Enum.IsDefined, nhưng điều đó vẫn có nghĩa là bạn phải cập nhật tất cả việc sử dụng enum theo cách thủ công khi bạn thêm giá trị mới. Xem: stackoverflow.com/questions/12531132/ Mạnh
Mike Hỗ trợ Monica

1
Tôi hy vọng rằng một lập trình viên hướng đối tượng sẽ không bao giờ tạo đối tượng truyền dữ liệu của họ, giống như Pyramidthực sự biết cách tạo ra draw()một kim tự tháp. Tốt nhất là nó có thể xuất phát từ Solidvà có một GetTriangles()phương thức và bạn có thể chuyển nó đến một SolidDrawerdịch vụ. Tôi nghĩ rằng chúng ta đang tránh xa các ví dụ về các đối tượng vật lý như các ví dụ về các đối tượng trong OOP.
Scott Whitlock

1
Không sao, chỉ là không giống ai làm hỏng việc lập trình hướng đối tượng cũ. :) Đặc biệt là vì hầu hết chúng ta kết hợp lập trình chức năng và OOP nhiều hơn là nghiêm túc OOP nữa.
Scott Whitlock

14

Enums không tạo giao diện giòn. Lạm dụng enum nào.

Enum để làm gì?

Enums được thiết kế để được sử dụng như một tập hợp các hằng số được đặt tên có ý nghĩa. Chúng được sử dụng khi:

  • Bạn biết rằng sẽ không có giá trị nào bị xóa.
  • (Và) Bạn biết rất khó có thể cần một giá trị mới.
  • (Hoặc) Bạn chấp nhận rằng sẽ cần một giá trị mới, nhưng hiếm khi đủ để đảm bảo sửa tất cả các mã bị hỏng vì nó.

Công dụng tốt của enums:

  • Các ngày trong tuần: (theo .Net System.DayOfWeek) Trừ khi bạn đang giao dịch với một số lịch cực kỳ khó hiểu, sẽ chỉ có 7 ngày trong tuần.
  • Màu sắc không thể mở rộng: (theo .Net System.ConsoleColor) Một số có thể không đồng ý với điều này, nhưng .Net đã chọn làm điều này vì một lý do. Trong hệ thống bảng điều khiển của .Net, chỉ có 16 màu có sẵn để sử dụng cho bảng điều khiển. 16 màu này tương ứng với bảng màu kế thừa được gọi là CGA hoặc 'Bộ điều hợp đồ họa màu' . Không có giá trị mới nào sẽ được thêm vào, điều đó có nghĩa là trên thực tế đây là một ứng dụng hợp lý của một enum.
  • Các liệt kê đại diện cho các trạng thái cố định: (theo Java Thread.State) Các nhà thiết kế của Java đã quyết định rằng trong mô hình luồng Java sẽ chỉ có một tập hợp các trạng thái cố định Threadcó thể tồn tại và do đó để đơn giản hóa các vấn đề, các trạng thái khác nhau này được biểu diễn dưới dạng liệt kê . Điều này có nghĩa là nhiều kiểm tra dựa trên trạng thái là các if và switch đơn giản, trong thực tế hoạt động trên các giá trị nguyên, mà không cần lập trình viên phải lo lắng về các giá trị thực sự là gì.
  • Bitflags đại diện cho các tùy chọn không loại trừ lẫn nhau: (theo .Net System.Text.RegularExpressions.RegexOptions) Bitflags là một cách sử dụng enum rất phổ biến. Trên thực tế, phổ biến là trong .Net, tất cả các enum đều có một HasFlag(Enum flag)phương thức được xây dựng. Chúng cũng hỗ trợ các toán tử bitwise và có một FlagsAttributedấu enum được dự định sẽ được sử dụng như một tập hợp các bitflags. Bằng cách sử dụng một enum như một tập hợp các cờ, bạn có thể biểu diễn một nhóm các giá trị boolean trong một giá trị duy nhất, cũng như có các cờ được đặt tên rõ ràng để thuận tiện. Điều này sẽ rất có lợi cho việc thể hiện các cờ của một thanh ghi trạng thái trong trình giả lập hoặc để thể hiện các quyền của tệp (đọc, ghi, thực thi) hoặc bất kỳ trường hợp nào trong đó một tập hợp các tùy chọn liên quan không loại trừ lẫn nhau.

Sử dụng xấu của enums:

  • Các lớp / loại nhân vật trong trò chơi: Trừ khi trò chơi là bản demo một lần mà bạn không thể sử dụng lại, thì không nên sử dụng enums cho các lớp nhân vật bởi vì rất có thể bạn sẽ muốn thêm nhiều lớp nữa. Tốt hơn là có một lớp duy nhất đại diện cho nhân vật và có một loại 'nhân vật' trong trò chơi được đại diện theo cách khác. Một cách để xử lý điều này là mẫu TypeObject , các giải pháp khác bao gồm đăng ký các loại ký tự với sổ đăng ký từ điển / loại hoặc hỗn hợp cả hai.
  • Màu sắc có thể mở rộng: Nếu bạn đang sử dụng một enum cho các màu mà sau này có thể được thêm vào thì đó không phải là một ý tưởng tốt để thể hiện điều này ở dạng enum, nếu không bạn sẽ bị bỏ lại mãi mãi thêm màu sắc. Điều này tương tự như vấn đề ở trên, do đó nên sử dụng một giải pháp tương tự (nghĩa là một biến thể của TypeObject).
  • Trạng thái mở rộng: Nếu bạn có một máy trạng thái có thể kết thúc giới thiệu nhiều trạng thái khác, thì không nên đại diện cho các trạng thái này thông qua bảng liệt kê. Phương thức ưa thích là xác định giao diện cho trạng thái của máy, cung cấp một triển khai hoặc lớp bao bọc giao diện và ủy thác các lệnh gọi phương thức của nó (gần giống với mẫu Chiến lược ), sau đó thay đổi trạng thái của nó bằng cách thay đổi triển khai hiện đang cung cấp.
  • Bitflags đại diện cho các tùy chọn loại trừ lẫn nhau: Nếu bạn đang sử dụng enum để đại diện cho các cờ và hai trong số các cờ đó sẽ không bao giờ xuất hiện cùng nhau thì bạn đã tự bắn vào chân mình. Bất cứ điều gì được lập trình để phản ứng với một trong các cờ sẽ đột nhiên phản ứng với bất kỳ cờ nào được lập trình để phản hồi trước - hoặc tệ hơn, nó có thể phản hồi cả hai. Loại tình huống này chỉ là yêu cầu rắc rối. Cách tiếp cận tốt nhất là coi sự vắng mặt của cờ là điều kiện thay thế nếu có thể (nghĩa là sự vắng mặt của Truecờ ngụ ý False). Hành vi này có thể được hỗ trợ thêm thông qua việc sử dụng các chức năng chuyên biệt (tức là IsTrue(flags)IsFalse(flags)).

Tôi sẽ thêm các ví dụ bitflag nếu bất cứ ai cũng có thể tìm thấy một ví dụ hoạt động hoặc nổi tiếng về các enum đang được sử dụng làm bitflags. Tôi biết họ tồn tại nhưng tiếc là tôi không thể nhớ lại bất cứ lúc nào.
Pharap 17/03/2015

1

@BLSully Ví dụ tuyệt vời. Không thể nói tôi đã từng sử dụng chúng, nhưng chúng tồn tại vì một lý do.
Pharap 19/03/2015

4

Enums là một cải tiến tuyệt vời so với số nhận dạng ma thuật cho các bộ giá trị đóng không có nhiều chức năng liên quan đến chúng. Thông thường bạn không quan tâm đến số nào thực sự liên quan đến enum; trong trường hợp này, thật dễ dàng để mở rộng bằng cách thêm các mục mới vào cuối, không có kết quả giòn.

Vấn đề là khi bạn có chức năng quan trọng liên quan đến enum. Đó là, bạn có loại mã này nằm xung quanh:

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

Một hoặc hai trong số này là ok, nhưng ngay khi bạn có loại enum có ý nghĩa này, những switchtuyên bố này có xu hướng nhân lên như những cống phẩm. Bây giờ mỗi khi bạn mở rộng enum, bạn cần phải tìm kiếm tất cả các switches liên quan để đảm bảo bạn đã bao quát mọi thứ. Điều này là dễ vỡ. Các nguồn như Clean Code sẽ gợi ý rằng bạn nên có tối đa một switchenum.

Thay vào đó, những gì bạn nên làm trong trường hợp này là sử dụng các nguyên tắc OO và tạo giao diện cho loại này. Bạn vẫn có thể giữ enum để liên lạc với loại này, nhưng ngay khi bạn cần làm bất cứ điều gì với nó, bạn tạo một đối tượng liên quan đến enum, có thể sử dụng Factory. Điều này dễ bảo trì hơn nhiều, vì bạn chỉ cần tìm một nơi để cập nhật: Nhà máy của bạn và bằng cách thêm một lớp mới.


1

Nếu bạn cẩn thận trong cách bạn sử dụng chúng, tôi sẽ không coi enums có hại. Nhưng có một vài điều cần xem xét nếu bạn muốn sử dụng chúng trong một số mã thư viện, trái ngược với một ứng dụng duy nhất.

  1. Không bao giờ loại bỏ hoặc sắp xếp lại các giá trị. Nếu bạn tại một thời điểm nào đó có một giá trị enum trong danh sách của bạn, giá trị đó sẽ được liên kết với tên đó mãi mãi. Nếu bạn muốn, đôi khi bạn có thể đổi tên các giá trị thành deprecated_orcbất cứ thứ gì, nhưng bằng cách không loại bỏ chúng, bạn có thể dễ dàng duy trì khả năng tương thích hơn với mã cũ được biên dịch theo bộ enum cũ. Nếu một số mã mới không thể xử lý các hằng số enum cũ, thì sẽ tạo ra một lỗi phù hợp ở đó hoặc đảm bảo không có giá trị nào như vậy đạt đến đoạn mã đó.

  2. Đừng làm số học về chúng. Cụ thể, đừng so sánh thứ tự. Tại sao? Bởi vì sau đó bạn có thể gặp phải tình huống bạn không thể giữ các giá trị hiện có và duy trì trật tự lành mạnh cùng một lúc. Ví dụ như một enum cho các hướng la bàn: N = 0, NE = 1, E = 2, SE = 3, Tấn Bây giờ nếu sau một số cập nhật, bạn bao gồm NNE và cứ thế giữa các hướng hiện có, bạn có thể thêm chúng vào cuối của danh sách và do đó phá vỡ thứ tự, hoặc bạn xen kẽ chúng với các khóa hiện có và do đó phá vỡ các ánh xạ được thiết lập được sử dụng trong mã kế thừa. Hoặc bạn không chấp nhận tất cả các khóa cũ và có một bộ khóa hoàn toàn mới, cùng với một số mã tương thích dịch giữa các khóa cũ và khóa mới vì lợi ích của mã kế thừa.

  3. Chọn một kích thước đủ. Theo mặc định, trình biên dịch sẽ sử dụng số nguyên nhỏ nhất có thể chứa tất cả các giá trị enum. Điều đó có nghĩa là nếu, tại một số cập nhật, tập hợp các enum có thể của bạn mở rộng từ 254 đến 259, bạn đột nhiên cần 2 byte cho mỗi giá trị enum thay vì một. Điều này có thể phá vỡ cấu trúc và bố trí lớp ở khắp mọi nơi, vì vậy hãy cố gắng tránh điều này bằng cách sử dụng một kích thước đủ trong thiết kế đầu tiên. C ++ 11 cung cấp cho bạn rất nhiều quyền kiểm soát ở đây, nhưng nếu không thì chỉ định một mục LAST_SPECIES_VALUE=65535cũng sẽ giúp ích.

  4. Có đăng ký trung tâm. Vì bạn muốn sửa ánh xạ giữa tên và giá trị, thật tệ nếu bạn cho phép người dùng bên thứ ba của mã của bạn thêm các hằng số mới. Không có dự án nào sử dụng mã của bạn nên được phép thay đổi tiêu đề đó để thêm ánh xạ mới. Thay vào đó họ nên lỗi bạn để thêm chúng. Điều này có nghĩa là đối với ví dụ về con người và người lùn mà bạn đã đề cập, enums thực sự là một sự phù hợp kém. Sẽ tốt hơn nếu có một số loại đăng ký vào thời gian chạy, trong đó người dùng mã của bạn có thể chèn một chuỗi và lấy lại một số duy nhất, được bọc độc đáo trong một số loại mờ. Số thứ tự mà thậm chí có thể là một con trỏ tới chuỗi đang đề cập, điều đó không thành vấn đề.

Mặc dù thực tế là điểm cuối cùng của tôi ở trên làm cho enum không phù hợp với tình huống giả định của bạn, nhưng tình huống thực tế của bạn khi một số thông số có thể thay đổi và bạn phải cập nhật một số mã có vẻ phù hợp với đăng ký trung tâm cho thư viện của bạn. Vì vậy, enums nên thích hợp ở đó nếu bạn lấy những gợi ý khác của tôi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.