Giao diện riêng cho phương pháp đột biến


11

Tôi đã làm việc để tái cấu trúc một số mã và tôi nghĩ rằng tôi có thể đã thực hiện bước đầu tiên xuống hố thỏ. Tôi đang viết ví dụ bằng Java, nhưng tôi cho rằng nó có thể là thuyết bất khả tri.

Tôi có một giao diện Foođược định nghĩa là

public interface Foo {

    int getX();

    int getY();

    int getZ();
}

Và việc thực hiện như

public final class DefaultFoo implements Foo {

    public DefaultFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getZ() {
        return z;
    }

    private final int x;
    private final int y;
    private final int z;
}

Tôi cũng có một giao diện MutableFoocung cấp các trình biến đổi phù hợp

/**
 * This class extends Foo, because a 'write-only' instance should not
 * be possible and a bit counter-intuitive.
 */
public interface MutableFoo extends Foo {

    void setX(int newX);

    void setY(int newY);

    void setZ(int newZ);
}

Có một vài cách triển khai MutableFoocó thể tồn tại (Tôi chưa thực hiện chúng). Một trong số đó là

public final class DefaultMutableFoo implements MutableFoo {

    /**
     * A DefaultMutableFoo is not conceptually constructed 
     * without all values being set.
     */
    public DefaultMutableFoo(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int getX() {
        return x;
    }

    public void setX(int newX) {
        this.x = newX;
    }

    public int getY() {
        return y;
    }

    public void setY(int newY) {
        this.y = newY;
    }

    public int getZ() {
        return z;
    }

    public void setZ(int newZ) {
        this.z = newZ;
    }

    private int x;
    private int y;
    private int z;
}

Lý do tôi đã chia những thứ này là vì nó có khả năng như nhau cho mỗi cái được sử dụng. Có nghĩa là, nhiều khả năng là ai đó sử dụng các lớp này sẽ muốn một trường hợp bất biến, vì đó là họ sẽ muốn một trường hợp có thể thay đổi.

Trường hợp sử dụng chính mà tôi có là một giao diện được gọi là StatSetđại diện cho các chi tiết chiến đấu nhất định cho một trò chơi (điểm nhấn, tấn công, phòng thủ). Tuy nhiên, số liệu thống kê "hiệu quả" hoặc số liệu thống kê thực tế là kết quả của các số liệu thống kê cơ bản, không bao giờ có thể thay đổi và các số liệu thống kê được đào tạo, có thể tăng lên. Hai cái này có liên quan bởi

/**
 * The EffectiveStats can never be modified independently of either the baseStats
 * or trained stats. As such, this StatSet must never provide mutators.
 */
public StatSet calculateEffectiveStats() {
    int effectiveHitpoints =
        baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    int effectiveAttack = 
        baseStats.getAttack() + (trainedStats.getAttack() / 4);
    int effectiveDefense = 
        baseStats.getDefense() + (trainedStats.getDefense() / 4);

    return StatSetFactory.createImmutableStatSet(effectiveHitpoints, effectiveAttack, effectiveDefense);
}

cácStats được tăng lên sau mỗi trận chiến như

public void grantExperience() {
    int hitpointsReward = 0;
    int attackReward = 0;
    int defenseReward = 0;

    final StatSet enemyStats = enemy.getEffectiveStats();
    final StatSet currentStats = player.getEffectiveStats();
    if (enemyStats.getHitpoints() >= currentStats.getHitpoints()) {
        hitpointsReward++;
    }
    if (enemyStats.getAttack() >= currentStats.getAttack()) {
        attackReward++;
    }
    if (enemyStats.getDefense() >= currentStats.getDefense()) {
        defenseReward++;
    }

    final MutableStatSet trainedStats = player.getTrainedStats();
    trainedStats.increaseHitpoints(hitpointsReward);
    trainedStats.increaseAttack(attackReward);
    trainedStats.increaseDefense(defenseReward);
}

nhưng chúng không tăng ngay sau trận chiến. Sử dụng một số vật phẩm, sử dụng các chiến thuật nhất định, sử dụng chiến trường thông minh đều có thể mang lại trải nghiệm khác nhau.

Bây giờ cho câu hỏi của tôi:

  1. Có một tên để phân chia giao diện bởi các bộ truy cập và bộ biến đổi thành các giao diện riêng biệt?
  2. Việc chia tách chúng theo cách này là cách tiếp cận 'đúng' nếu chúng có khả năng được sử dụng như nhau, hoặc có một mô hình khác, được chấp nhận hơn mà tôi nên sử dụng thay thế (ví dụ: Foo foo = FooFactory.createImmutableFoo();có thể trả về DefaultFoohoặc DefaultMutableFoobị ẩn vì createImmutableFootrả về Foo)?
  3. Có bất kỳ nhược điểm nào có thể thấy trước ngay khi sử dụng mẫu này, lưu lại để làm phức tạp hệ thống phân cấp giao diện không?

Lý do tôi bắt đầu thiết kế theo cách này là vì tôi nghĩ rằng tất cả những người thực hiện giao diện nên tuân thủ giao diện đơn giản nhất có thể, và không cung cấp gì thêm. Bằng cách thêm setters vào giao diện, bây giờ các số liệu thống kê hiệu quả có thể được sửa đổi độc lập với các phần của nó.

Tạo một lớp mới cho EffectiveStatSetkhông có ý nghĩa nhiều vì chúng tôi sẽ không mở rộng chức năng theo bất kỳ cách nào. Chúng tôi có thể thay đổi việc thực hiện và tạo ra EffectiveStatSetmột hỗn hợp gồm hai loại khác nhau StatSets, nhưng tôi cảm thấy đó không phải là giải pháp phù hợp;

public class EffectiveStatSet implements StatSet {

    public EffectiveStatSet(StatSet baseStats, StatSet trainedStats) {
        // ...
    }

    public int getHitpoints() {
        return baseStats.getHitpoints() + (trainedStats.getHitpoints() / 4);
    }
}


1
Tôi nghĩ vấn đề là đối tượng của bạn có thể thay đổi được ngay cả khi bạn đang truy cập vào giao diện 'bất biến' của nó. Tốt hơn là có một đối tượng có thể thay đổi với Player.TrainStats ()
Ewan

@gnat bạn có tài liệu tham khảo nào để tôi có thể tìm hiểu thêm về điều đó không? Dựa trên câu hỏi được chỉnh sửa, tôi không chắc mình có thể áp dụng câu hỏi đó như thế nào hoặc ở đâu.
Zymus

@gnat: các liên kết của bạn (và các liên kết cấp hai và cấp ba của nó) rất hữu ích, nhưng các cụm từ thông thái hấp dẫn rất ít giúp ích. Nó chỉ thu hút sự hiểu lầm và coi thường.
rwong

Câu trả lời:


6

Đối với tôi có vẻ như bạn có một giải pháp tìm kiếm một vấn đề.

Có một tên để phân chia giao diện bởi các bộ truy cập và bộ biến đổi thành các giao diện riêng biệt?

Điều này có thể là một chút khiêu khích, nhưng thực sự tôi sẽ gọi nó là "những thứ quá mức" hoặc "những thứ quá phức tạp". Bằng cách cung cấp một biến thể có thể thay đổi và bất biến của cùng một lớp, bạn đưa ra hai giải pháp tương đương về chức năng cho cùng một vấn đề, chỉ khác nhau ở các khía cạnh phi chức năng như hành vi hiệu suất, API và bảo mật chống lại tác dụng phụ. Tôi đoán đó là vì bạn sợ phải đưa ra quyết định nên chọn cái nào hơn hoặc vì bạn cố gắng thực hiện tính năng "const" của C ++ trong C #. Tôi đoán trong 99% tất cả các trường hợp, nó sẽ không tạo ra sự khác biệt lớn nếu người dùng chọn biến thể có thể thay đổi hoặc bất biến, anh ta có thể giải quyết vấn đề của mình bằng cách này hay cách khác. Do đó "khả năng của một lớp được sử dụng"

Ngoại lệ là khi bạn thiết kế một ngôn ngữ lập trình mới hoặc một khung đa mục đích sẽ được sử dụng bởi khoảng mười nghìn lập trình viên trở lên. Sau đó, nó thực sự có thể mở rộng quy mô tốt hơn khi bạn cung cấp các biến thể bất biến và có thể thay đổi của các loại dữ liệu mục đích chung. Nhưng đây là một tình huống mà bạn sẽ có hàng ngàn tình huống sử dụng khác nhau - có lẽ không phải là vấn đề bạn gặp phải, tôi đoán vậy?

Việc chia tách chúng theo cách này là cách tiếp cận 'đúng' nếu chúng có khả năng được sử dụng như nhau hoặc có một mô hình khác, được chấp nhận hơn

"Mẫu được chấp nhận nhiều hơn" được gọi là KISS - giữ cho nó đơn giản và ngu ngốc. Đưa ra quyết định hoặc chống lại khả năng biến đổi cho lớp / giao diện cụ thể trong thư viện của bạn. Ví dụ: nếu "Statset" của bạn có hàng tá thuộc tính hoặc nhiều hơn và chúng hầu như được thay đổi riêng lẻ, tôi thích biến thể có thể thay đổi và chỉ không sửa đổi các số liệu thống kê cơ sở nơi chúng không nên được sửa đổi. Đối với một cái gì đó như một Foolớp có thuộc tính X, Y, Z (một vectơ ba chiều), tôi có lẽ sẽ thích biến thể bất biến.

Có bất kỳ nhược điểm nào có thể thấy trước ngay khi sử dụng mẫu này, lưu lại để làm phức tạp hệ thống phân cấp giao diện không?

Thiết kế quá phức tạp làm cho phần mềm khó kiểm tra hơn, khó bảo trì hơn, khó phát triển hơn.


1
Tôi nghĩ bạn có nghĩa là "HÔN - Giữ nó đơn giản, ngu ngốc"
Epicblood

2

Có một tên để phân chia giao diện bởi các bộ truy cập và bộ biến đổi thành các giao diện riêng biệt?

Có thể có một cái tên cho điều này nếu sự tách biệt này là hữu ích và cung cấp một lợi íchtôi không thấy . Nếu các phân cách không cung cấp một lợi ích, hai câu hỏi khác không có ý nghĩa.

Bạn có thể cho chúng tôi biết bất kỳ trường hợp sử dụng kinh doanh nào trong đó hai Giao diện riêng biệt mang lại lợi ích cho chúng tôi không hoặc câu hỏi này có phải là vấn đề học thuật (YAGNI) không?

Tôi có thể nghĩ về việc mua sắm với một giỏ hàng có thể thay đổi (bạn có thể đặt nhiều bài viết hơn) có thể trở thành một đơn đặt hàng mà các bài báo có thể được thay đổi bởi khách hàng nữa. Tình trạng trật tự vẫn có thể thay đổi.

Các triển khai mà tôi đã thấy không tách rời các giao diện

  • không có giao diện ReadOnlyList trong java

Phiên bản ReadOnly sử dụng "WritableInterface" và đưa ra một ngoại lệ nếu phương thức ghi được ghi lại


Tôi đã sửa đổi OP để giải thích trường hợp sử dụng chính.
Zymus

2
"không có giao diện ReadOnlyList trong java" - thật lòng mà nói, nên có. Nếu bạn có một đoạn mã chấp nhận Danh sách <T> làm tham số, bạn không thể dễ dàng biết liệu nó có hoạt động với danh sách không thể thay đổi hay không. Mã trả về danh sách, không có cách nào để biết liệu bạn có thể sửa đổi chúng một cách an toàn hay không. Tùy chọn duy nhất là dựa vào tài liệu có khả năng không đầy đủ hoặc không chính xác, hoặc tạo một bản sao phòng thủ chỉ trong trường hợp. Một loại bộ sưu tập chỉ đọc sẽ làm cho điều này đơn giản hơn nhiều.
Jules

@Jules: thực sự, cách Java dường như là tất cả ở trên: để đảm bảo tài liệu đầy đủ và chính xác, cũng như tạo một bản sao phòng thủ chỉ trong trường hợp. Nó chắc chắn quy mô cho các dự án enterprisey rất lớn.
rwong

1

Sự tách biệt của giao diện bộ sưu tập có thể thay đổi và giao diện bộ sưu tập chỉ đọc hợp đồng là một ví dụ về nguyên tắc phân tách giao diện. Tôi không nghĩ có bất kỳ tên đặc biệt nào được đặt cho mỗi ứng dụng của nguyên tắc.

Lưu ý một vài từ ở đây: "chỉ đọc hợp đồng" và "bộ sưu tập".

Hợp đồng chỉ đọc có nghĩa là lớp Acung cấp cho lớp Bquyền truy cập chỉ đọc, nhưng không ngụ ý rằng đối tượng bộ sưu tập cơ bản là thực sự bất biến. Bất biến có nghĩa là nó sẽ không bao giờ thay đổi trong tương lai, bất kể tác nhân nào. Hợp đồng chỉ đọc chỉ nói rằng người nhận không được phép thay đổi nó; người khác (đặc biệt là lớp A) được phép thay đổi nó.

Để làm cho một đối tượng bất biến, nó phải thực sự bất biến - nó phải từ chối các nỗ lực thay đổi dữ liệu của nó bất kể tác nhân yêu cầu nó.

Mẫu rất có thể được quan sát trên các đối tượng đại diện cho một bộ sưu tập dữ liệu - danh sách, chuỗi, tệp (luồng), v.v.


Từ "bất biến" trở thành mốt, nhưng khái niệm này không mới. Và có nhiều cách sử dụng tính bất biến, cũng như nhiều cách để đạt được thiết kế tốt hơn bằng cách sử dụng một cái gì đó khác (ví dụ như đối thủ cạnh tranh của nó).


Đây là cách tiếp cận của tôi (không dựa trên sự bất biến).

  1. Xác định một DTO (đối tượng truyền dữ liệu), còn được gọi là bộ giá trị.
    • DTO này sẽ chứa ba lĩnh vực: hitpoints, attack, defense.
    • Chỉ cần các lĩnh vực: công khai, bất cứ ai cũng có thể viết, không cần bảo vệ.
    • Tuy nhiên, một DTO phải là một đối tượng vứt đi: nếu lớp Acần truyền DTO cho lớp B, nó sẽ tạo một bản sao của nó và chuyển bản sao thay thế. Vì vậy, Bcó thể sử dụng DTO theo cách nó thích (viết cho nó), mà không ảnh hưởng đến DTO Agiữ.
  2. Các grantExperiencechức năng được chia thành hai:
    • calculateNewStats
    • increaseStats
  3. calculateNewStats sẽ lấy đầu vào từ hai DTO, một đại diện cho các chỉ số của người chơi và một đầu vào đại diện cho các chỉ số của kẻ thù và thực hiện các phép tính.
    • Đối với đầu vào, người gọi nên chọn trong số các số liệu thống kê cơ sở, được đào tạo hoặc hiệu quả, theo nhu cầu của bạn.
    • Kết quả sẽ là một DTO mới, nơi từng lĩnh vực ( hitpoints, attack, defense) cửa hàng số tiền được tăng lên cho khả năng đó.
    • DTO "số tiền tăng thêm" mới không liên quan đến giới hạn trên (giới hạn tối đa áp đặt) cho các giá trị đó.
  4. increaseStats là một phương thức trên trình phát (không phải trên DTO) sử dụng DTO "số tiền để tăng" và áp dụng mức tăng đó trên DTO do người chơi sở hữu và đại diện cho DTO có thể huấn luyện của người chơi.
    • Nếu có các giá trị tối đa áp dụng cho các thống kê này, chúng được thi hành tại đây.

Trong trường hợp calculateNewStatsđược tìm thấy không phụ thuộc vào bất kỳ thông tin người chơi hoặc kẻ thù nào khác (ngoài các giá trị trong hai DTO đầu vào), phương pháp này có thể có khả năng được đặt ở bất kỳ đâu trong dự án.

Nếu calculateNewStatsphát hiện có sự phụ thuộc hoàn toàn vào người chơi và đối tượng kẻ thù (nghĩa là, đối tượng người chơi và kẻ thù trong tương lai có thể có các thuộc tính mới và calculateNewStatsphải được cập nhật để tiêu thụ càng nhiều thuộc tính mới càng tốt), thì calculateNewStatsphải chấp nhận hai thuộc tính đó các đối tượng, không chỉ đơn thuần là DTO. Tuy nhiên, kết quả tính toán của nó vẫn sẽ là DTO tăng hoặc bất kỳ đối tượng truyền dữ liệu đơn giản nào mang thông tin được sử dụng để thực hiện tăng / nâng cấp.


1

Có một vấn đề lớn ở đây: chỉ vì cấu trúc dữ liệu là bất biến không có nghĩa là chúng ta không cần các phiên bản sửa đổi của nó . Các thực phiên bản bất biến của một cấu trúc dữ liệu không cung cấp setX, setYsetZcác phương pháp - họ chỉ trả lại một mới cấu trúc thay vì sửa đổi đối tượng mà bạn đã gọi cho họ.

// Mutates mutableFoo
mutableFoo.setX(...)
// Creates a new updated immutableFoo, existing immutableFoo is unchanged
newFoo = immutableFoo.setX(...)

Vậy làm thế nào để bạn cung cấp cho một phần trong hệ thống của bạn khả năng thay đổi nó trong khi hạn chế các phần khác? Với một đối tượng có thể thay đổi có chứa một tham chiếu đến một thể hiện của cấu trúc bất biến. Về cơ bản, thay vì lớp người chơi của bạn có thể thay đổi đối tượng chỉ số của nó trong khi cung cấp cho mọi lớp khác một cái nhìn bất biến về các chỉ số của nó, các chỉ số của nó bất biến và người chơi có thể thay đổi. Thay vì:

// Stats are mutable, mutates self.stats
self.stats.setX(...)

Bạn sẽ có:

// Stats are immutable, mutates self, setX() returns new updated stats
self.stats = self.stats.setX(...)

Thấy sự khác biệt? Đối tượng thống kê là hoàn toàn bất biến, nhưng các chỉ số hiện tại của người chơi là một tham chiếu có thể thay đổi đối với đối tượng bất biến. Không cần phải thực hiện hai giao diện - chỉ cần làm cho cấu trúc dữ liệu hoàn toàn bất biến và quản lý một tham chiếu có thể thay đổi đến nó nơi bạn tình cờ sử dụng nó để lưu trữ trạng thái.

Điều này có nghĩa là các đối tượng khác trong hệ thống của bạn không thể dựa vào tham chiếu đến đối tượng thống kê - đối tượng thống kê mà họ sẽ không giống với đối tượng thống kê mà người chơi có sau khi người chơi cập nhật chỉ số của họ.

Điều này có ý nghĩa hơn dù sao, vì về mặt khái niệm, nó không thực sự là chỉ số đang thay đổi, đó là số liệu thống kê hiện tại của người chơi . Không phải đối tượng thống kê. Các tài liệu tham khảo cho các số liệu thống kê đối tượng đối tượng người chơi có. Vì vậy, các phần khác trong hệ thống của bạn tùy thuộc vào tham chiếu đó nên được tham chiếu rõ ràng player.currentStats()hoặc một số như vậy thay vì nắm bắt đối tượng thống kê cơ bản của người chơi, lưu trữ ở đâu đó và dựa vào đó để cập nhật thông qua các đột biến.


1

Wow ... điều này thực sự đưa tôi trở lại. Tôi đã thử ý tưởng tương tự một vài lần. Tôi đã quá cứng đầu để từ bỏ nó bởi vì tôi nghĩ rằng sẽ có một cái gì đó tốt để học. Tôi đã thấy những người khác cũng thử mã này. Hầu hết các trường tôi đã thấy được gọi là giao diện Read-Only như của bạn FooViewerhay ReadOnlyFoovà Write-Only giao diện FooEditorhoặc WriteableFoohoặc trong Java Tôi nghĩ rằng tôi thấy FooMutatormột lần. Tôi không chắc có một từ vựng chính thức hoặc thậm chí phổ biến để làm mọi thứ theo cách này.

Điều này không bao giờ làm bất cứ điều gì hữu ích cho tôi ở những nơi tôi đã thử nó. Tôi sẽ tránh nó hoàn toàn. Tôi sẽ làm như những người khác đang đề xuất và lùi lại một bước và xem xét liệu bạn có thực sự cần khái niệm này trong ý tưởng lớn hơn của bạn hay không. Tôi không chắc chắn có một cách đúng đắn để làm điều này vì tôi thực sự không bao giờ giữ bất kỳ mã nào tôi đã tạo trong khi thử điều này. Mỗi lần tôi rút lui sau những nỗ lực đáng kể và những điều đơn giản hóa. Và bằng cách đó, tôi có ý gì đó dọc theo những gì người khác nói về YAGNI và KISS và DRY.

Một nhược điểm có thể có: sự lặp lại. Bạn sẽ phải tạo các giao diện này cho rất nhiều lớp có khả năng và thậm chí chỉ một lớp bạn phải đặt tên cho từng phương thức và mô tả mỗi chữ ký ít nhất hai lần. Trong tất cả những điều làm tôi kiệt sức trong việc mã hóa, việc phải thay đổi nhiều tệp theo cách này khiến tôi gặp nhiều rắc rối nhất. Cuối cùng tôi đã quên mất việc thực hiện các thay đổi ở nơi này hay nơi khác. Khi bạn có một lớp được gọi là Foo và các giao diện được gọi là FooViewer và FooEditor, nếu bạn quyết định sẽ tốt hơn nếu bạn gọi nó là Bar thay vì bạn phải tái cấu trúc-> đổi tên ba lần trừ khi bạn có IDE thông minh thực sự đáng kinh ngạc. Tôi thậm chí đã thấy đây là trường hợp khi tôi có một lượng mã đáng kể đến từ một trình tạo mã.

Cá nhân, tôi không thích tạo giao diện cho những thứ chỉ có một triển khai duy nhất. Điều đó có nghĩa là khi tôi đi vòng quanh trong mã, tôi không thể chuyển sang thực hiện duy nhất trực tiếp, tôi phải chuyển sang giao diện và sau đó đến việc thực hiện giao diện hoặc ít nhất là nhấn một vài phím phụ để đến đó. Những thứ đó cộng lại.

Và sau đó là sự phức tạp mà bạn đề cập. Tôi nghi ngờ tôi sẽ không bao giờ đi theo cách đặc biệt này với mã của tôi nữa. Ngay cả đối với vị trí này phù hợp nhất với kế hoạch tổng thể của tôi cho mã (một ORM tôi đã tạo) tôi đã kéo cái này và thay thế nó bằng thứ gì đó dễ mã hóa hơn.

Tôi thực sự sẽ suy nghĩ nhiều hơn về các thành phần bạn đề cập. Tôi tò mò tại sao bạn cảm thấy đó là một ý tưởng tồi. Tôi đã dự kiến ​​sẽ EffectiveStatssáng tác và bất biến Statsvà một cái gì StatModifiersđó sẽ sáng tác thêm một số bộ StatModifierđại diện cho bất cứ điều gì có thể sửa đổi các chỉ số (hiệu ứng tạm thời, vật phẩm, vị trí trong một số khu vực nâng cao, mệt mỏi) nhưng bạn EffectiveStatssẽ không cần phải hiểu vì StatModifierssẽ không hiểu quản lý những thứ đó là gì và mức độ ảnh hưởng và loại chúng sẽ có trên chỉ số nào. CácStatModifiersẽ là một giao diện cho những thứ khác nhau và có thể biết những thứ như "tôi ở trong khu vực", "khi nào thuốc hết", v.v ... nhưng sẽ không phải cho bất kỳ đối tượng nào khác biết những thứ đó. Nó chỉ cần nói rằng stat và cách sửa đổi nó là ngay bây giờ. Tốt hơn nữa StatModifierchỉ đơn giản là có thể đưa ra một phương pháp để tạo ra một bất biến mới Statsdựa trên một phương pháp khác Statssẽ khác bởi vì nó đã được thay đổi một cách thích hợp. Sau đó bạn có thể làm một cái gì đó như currentStats = statModifier2.modify(statModifier1.modify(baseStats))và tất cả Statscó thể là bất biến. Tôi thậm chí sẽ không mã trực tiếp, tôi có thể sẽ lặp qua tất cả các công cụ sửa đổi và áp dụng từng cái cho kết quả của các công cụ sửa đổi trước đó bắt đầu bằng baseStats.

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.