Mẫu thiết kế cho các giá trị phụ thuộc lẫn nhau


8

Tóm tắt: Có một mẫu thiết kế tốt để giảm trùng lặp thông tin giữa các giá trị phụ thuộc chặt chẽ không?

Trong công việc của tôi, nó khá phổ biến để có mối quan hệ giữa các đại lượng để bạn có thể lấy được một trong các đại lượng nếu bạn biết các đại lượng khác. Một ví dụ có thể là luật khí lý tưởng :

Pv = RT

Bạn có thể tưởng tượng việc tạo ra một lớp để thể hiện trạng thái của một loại khí lý tưởng. Lớp sẽ có 3 thuộc tính, một cách tự nhiên Pressure, TemperatureSpecificVolumemỗi một loại thích hợp.

Đối với người dùng của một đối tượng của lớp này, có vẻ như sẽ tự nhiên mong đợi rằng nếu bạn đặt giá trị cho cả hai PressureTemperature, sau đó bạn có thể đọc ra một giá trị cho SpecificVolumevà mong muốn đối tượng đã tính toán điều đó cho bạn.

Tương tự, nếu bạn đặt giá trị cho cả hai PressureSpecificVolume, sau đó bạn có thể đọc ra Temperature, v.v.

Để thực sự thực hiện lớp này tuy nhiên đòi hỏi một số thông tin trùng lặp. Bạn sẽ cần lập trình rõ ràng tất cả các biến thể của phương trình, coi một biến khác nhau là phụ thuộc trong từng trường hợp:

  1. T = P * v / R

  2. P = R * T / v

  3. v = R * T / P

mà dường như vi phạm nguyên tắc DRY . Mặc dù mỗi biểu hiện mối quan hệ giống nhau, các trường hợp này yêu cầu kiểm tra và mã hóa độc lập.

Trong trường hợp thực, logic tôi nghĩ là phức tạp hơn ví dụ này, nhưng thể hiện cùng một vấn đề cơ bản. Vì vậy, sẽ có giá trị thực sự nếu tôi chỉ có thể diễn đạt logic một lần, hoặc ít nhất là ít lần hơn.

Lưu ý rằng một lớp như thế này có lẽ cũng sẽ phải đối phó với việc đảm bảo rằng nó được khởi tạo đúng trước khi các giá trị được đọc ra, nhưng tôi nghĩ đó là một sự xem xét thứ cấp.


Mặc dù tôi đã đưa ra một ví dụ toán học, câu hỏi không chỉ giới hạn trong mối quan hệ toán học giữa các dữ liệu. Đó dường như chỉ là một ví dụ đơn giản để đưa ra quan điểm.


5
Hãy nhớ rằng ví dụ của bạn không hiển thị bất kỳ hình thức sao chép nào. DRY chỉ áp dụng khi bạn chủ động tạo lại hành vi tương tự . Trong trường hợp ví dụ của bạn, mọi biến thể của phương trình giải quyết một giá trị khác nhau - và do đó, là một hành vi khác nhau. Dạng Pv = RT của phương trình là một khái quát vô dụng của chính nó từ quan điểm OO.
T. Sar

Tôi không thấy bất kỳ sự trùng lặp.
Ngừng làm hại Monica

1
Ngoài ra còn có vấn đề làm cho các quốc gia "bất hợp pháp" là không thể, tức là những người có Pv/T != R. Không chắc chắn nếu ý tưởng đó có thể giúp bạn giải quyết các vấn đề tiềm ẩn, hoặc nếu điều đó thậm chí làm phức tạp nó.
Bernhard Hiller

Như T. Sar đã nói, không có sự lặp lại ở đây. Bất cứ điều gì khác đề nghị Martin Maat chắc chắn sẽ gây ra nhiều rắc rối hơn mà nó sẽ giải quyết. Về cơ bản, việc bạn tìm kiếm công cụ có thể tự động điều chỉnh công thức để tính toán giá trị bị thiếu từ một công cụ khác, trừ khi bạn tìm thấy một thư viện làm điều đó (ai biết ...) chỉ từ bỏ điều đó, nó không đáng để gặp rắc rối.
Walfrat

Như câu trả lời đã chỉ ra, ngôn ngữ mệnh lệnh có một số vấn đề. Ngược lại, một ngôn ngữ khai báo / logic như Prolog làm cho nó trở nên tầm thường. Kiểm tra Wikipedia . Nó có thể được thực hiện với một ngôn ngữ bắt buộc, nó "chỉ" đòi hỏi nhiều công việc hơn.
Armaghast

Câu trả lời:


12

Không có vấn đề từ góc độ OO. Bạn có thể có một phương thức cung cấp GasLaw tĩnh

GetVolume(R, T, P) 
GetPressure(R, T, v)

vân vân

và sẽ không có vấn đề trùng lặp. Không có đối tượng, không có thành viên dữ liệu. Chỉ là những hành vi khác nhau.

Bạn có thể coi luật khí là "một điều" nhưng các hoạt động đều khác biệt. Không có gì sai khi có một phương pháp cho từng trường hợp sử dụng của pháp luật.


3
Điều này không thực sự giải quyết câu hỏi làm thế nào bên trong một lớp như vậy để giảm trùng lặp thông tin / logic. Giao diện bên ngoài không thực sự là một vấn đề.
UuDdLrLrSs

Tôi không đồng ý rằng nó không giải quyết câu hỏi. Câu trả lời rất rõ ràng với tôi rằng Martin đang nói đừng làm những gì bạn đang hỏi làm thế nào vì rõ ràng có cách dễ hiểu hơn nhiều để thực hiện chính xác điều tương tự.
Dunk

10

Điều này không thực sự có thể theo cách bạn yêu cầu.

Hãy xem xét: Tôi có một đối tượng khí lý tưởng g. Nếu tôi đặt rõ ràng cả ba nhiệt độ, áp suất và thể tích cụ thể, sau đó lấy lại nhiệt độ, như:

IdealGas g;
g.setTemperature(t1);
g.setPressure(p1);
g.setVolume(v1);
assert(g.getTemperature() == what); // ?

nó nên

  1. cho tôi giá trị t1tôi đặt ban đầu?
  2. tính giá trị sử dụng áp suất và khối lượng riêng?
  3. tính giá trị sử dụng áp suất và khối lượng riêng chỉ khi tôi đặt giá trị sau nhiệt độ?
  4. cho tôi giá trị ban đầu nếu nó đủ gần với giá trị được tính toán (chúng ta nên cho phép làm tròn các lỗi) và tính toán như # 2 hoặc # 3 nếu không?

Nếu bạn phải làm điều này, bạn cần theo dõi rất nhiều trạng thái cho từng thành viên: nó chưa được khởi tạo, được đặt rõ ràng bởi mã máy khách hay lưu vào bộ đệm một giá trị được tính? Giá trị được đặt rõ ràng hoặc được lưu trong bộ nhớ cache đã bị vô hiệu hóa bởi một thành viên khác được thiết lập sau này?

Hành vi rõ ràng là khó dự đoán từ việc đọc mã khách hàng. Đây là một thiết kế kém, bởi vì sẽ luôn khó để hiểu tại sao bạn có câu trả lời bạn đã làm. Sự phức tạp là một dấu hiệu cho thấy vấn đề này không phù hợp với OO, hoặc ít nhất rằng một đối tượng khí lý tưởng trạng thái là một lựa chọn trừu tượng xấu.


8

Đầu tiên tôi không nghĩ bạn vi phạm Nguyên tắc DRY vì cả 3 công thức tính các giá trị khác nhau. Thực tế đó là một sự phụ thuộc giữa cả 3 giá trị là bởi vì bạn thấy nó là một phương trình toán học chứ không phải là một phép gán biến lập trình.

Tôi đề nghị thực hiện trường hợp của bạn với một lớp PerfectGas bất biến như sau

public class IdealGas {
    private static final double R = 8.3145;

    private final Pressure            pressure;
    private final Temperature         temperature;
    private final SpecificVolume      specificVolume;

    public IdealGas(Pressure pressure, Temperature temperature)
    {
        this.pressure = pressure;
        this.temperature = temperature;
        this.specificVolume = calculateSpecificVolume(pressure.getValue(), temperature.getValue());
    }


    public IdealGas(Pressure pressure, SpecificVolume specificVolume)
    {
        this.pressure = pressure;
        this.specificVolume = specificVolume;
        this.temperature = calculateTemperature(pressure.getValue(), specificVolume.getValue());
    }

    public IdealGas(Temperature temperature, SpecificVolume specificVolume)
    {
        this.temperature = temperature;
        this.specificVolume = specificVolume;
        this.pressure = calculatePressure(temperature.getValue(), specificVolume.getValue());
    }

    private SpecificVolume calculateSpecificVolume(double pressure, double temperature)
    {
        return new SpecificVolume(R * temperature / pressure);
    }

    private Temperature calculateTemperature(double pressure, double specificVolume)
    {
        return new Temperature(pressure * specificVolume / R);
    }

    private Pressure calculatePressure(double temperature, double specificVolume)
    {
        return new Pressure(R * temperature / specificVolume);
    }

    public Pressure getPressure()
    {
        return pressure;
    }

    public Temperature getTemperature()
    {
        return temperature;
    }

    public SpecificVolume getSpecificVolume()
    {
        return specificVolume;
    }
}

Hãy để tôi giải thích việc thực hiện. Lớp IdealGas gói gọn 3 thuộc tính Áp suất, Nhiệt độ và Cụ thể là các giá trị cuối cùng cho tính bất biến. Mỗi khi bạn gọi getXXX, sẽ không có tính toán nào được thực hiện. Bí quyết là có 3 hàm tạo cho cả 3 kết hợp của 2 tham số đã cho. Trong mỗi constructor bạn tính toán biến thứ ba còn thiếu. Việc tính toán được thực hiện một lần, tại thời điểm xây dựng và gán cho thuộc tính thứ ba. Bởi vì điều này được thực hiện trong constructor, thuộc tính thứ ba có thể là cuối cùng và bất biến. Đối với tính không thay đổi, tôi giả sử rằng các lớp Áp suất, Nhiệt độ và Cụ thể cũng không thay đổi.

Phần duy nhất còn lại là có getters cho tất cả các thuộc tính. Không có setters tồn tại bởi vì bạn truyền tham số cho các nhà xây dựng. Nếu bạn cần thay đổi một thuộc tính, hãy tạo một cá thể IdealGas mới với các tham số mong muốn.

Trong ví dụ của tôi, các lớp Áp suất, Nhiệt độ và Cụ thể là các hàm đơn giản có giá trị gấp đôi. Mã mẫu là trong java, nhưng có thể được khái quát.

Cách tiếp cận này có thể được khái quát hóa, chuyển tất cả dữ liệu liên quan vào một hàm tạo, tính toán dữ liệu liên quan trong hàm tạo và chỉ có các biểu đồ.


2
Giải quyết điều này với sự bất biến và tiêm xây dựng là câu trả lời đúng ở đây. Theo cách nói của OO, ba biến thể khác nhau của phương trình chuyển thành ba hàm tạo khác nhau. Mỗi hoạt động trên IdealGas tạo ra một đối tượng (giá trị) khác. +1 Đây sẽ là câu trả lời ở đây.
Greg Burghardt

+1 để cung cấp giải pháp
keuleJ

5

Đây là một câu hỏi thực sự thú vị! Bạn đúng về nguyên tắc rằng bạn đang sao chép thông tin vì cùng một phương trình được sử dụng trong cả ba trường hợp, chỉ với những ẩn số khác nhau.

Nhưng trong một ngôn ngữ lập trình chính điển hình, không có hỗ trợ tích hợp để giải phương trình. Các biến trong lập trình luôn là các đại lượng đã biết (tại thời điểm thực hiện), do đó, mặc dù có sự tương đồng bề ngoài, các biểu thức trong ngôn ngữ lập trình không thể so sánh với các phương trình toán học.

Trong một ứng dụng điển hình, một phương trình như bạn mô tả sẽ chỉ được viết thành ba biểu thức riêng biệt và bạn sống với sự trùng lặp. Nhưng các thư viện giải phương trình không tồn tại và có thể được sử dụng trong trường hợp như vậy.

Đây không chỉ là một mô hình, nó là một mô hình lập trình toàn bộ, được gọi là giải quyết ràng buộc và có các ngôn ngữ lập trình chuyên dụng như Prolog cho loại vấn đề này.


3

Đây là một kích thích phổ biến khi mã hóa các mô hình toán học vào phần mềm. Cả hai đều sử dụng một ký hiệu rất giống nhau cho các mô hình đơn giản như luật khí, nhưng ngôn ngữ lập trình không phải là toán học và nhiều thứ bạn có thể bỏ qua khi làm việc trong thế giới toán học phải được thực hiện rõ ràng trong phần mềm.

Nó không lặp lại chính mình. Không có khái niệm về phương trình, biến đổi đại số hoặc "giải cho x" trong hầu hết các ngôn ngữ lập trình. Nếu không có một đại diện phần mềm của tất cả những khái niệm không có cách nào cho phần mềm để đưa ra T = P * v / Rcho các Pv = RTphương trình.

Mặc dù nó có vẻ như là một giới hạn vô lý của các ngôn ngữ lập trình khi nhìn vào các mô hình đơn giản như luật khí, ngay cả các toán học nâng cao hơn một chút cho phép các phương trình không có giải pháp đại số khép kín, hoặc không có phương pháp nào có thể rút ra được giải pháp một cách hiệu quả. Đối với kiến ​​trúc phần mềm, bạn không thể phụ thuộc vào nó trong trường hợp phức tạp hơn.

Và trường hợp đơn giản của chính nó? Tôi không chắc rằng sẽ đáng để đọc thông số kỹ thuật của tính năng trong ngôn ngữ lập trình. Các mô hình có xu hướng được thay thế, không được sửa đổi và thường chỉ có một vài biến để giải quyết.


1

Lớp sẽ có 3 thuộc tính, một cách tự nhiên Pressure, TemperatureSpecificVolume.

Không, bất kỳ hai là đủ. Nhưng làm cho họ tin và phơi bày các phương pháp như pressure(), temperature(), và specificVolume(). Một trong số chúng, không tương ứng với hai thuộc tính riêng tư, nên có logic phù hợp. Do đó chúng ta có thể loại bỏ sự trùng lặp dữ liệu.

Nên có ba nhà xây dựng với các thông số (P, T), (P, v)(T, v). Một trong số chúng, tương ứng với hai thuộc tính riêng, chỉ đơn giản đóng vai trò là setters. Hai cái khác nên có logic thích hợp. Tất nhiên logic được viết ba lần (hai lần ở đây và một lần trong đoạn trước), nhưng chúng không phải là bản sao. Ngay cả khi bạn coi chúng là bản sao thì chúng vẫn cần thiết.

Là ba biểu thức thể hiện cùng một mối quan hệ?

Có về mặt toán học và không có OO-ly. Trong các đối tượng OO là công dân hạng nhất không biểu hiện. Để thể hiện các công dân hạng nhất (hoặc tạo mối quan hệ được thể hiện bằng cùng một thứ), chúng ta cần viết một lớp, Expressionhoặc Equationđó không phải là một nhiệm vụ dễ dàng (có thể có một số thư viện).

Câu hỏi không chỉ giới hạn trong mối quan hệ toán học giữa các dữ liệu.

Mối quan hệ cũng không phải là một công dân hạng nhất. Cho rằng chúng ta có thể cần phải viết một lớp Relationshipcũng không dễ dàng. Nhưng sống với các bản sao dễ dàng hơn.

Nếu bạn quyết định sống với các bản sao và nếu các tham số trong mối quan hệ cao, hãy cân nhắc sử dụng mẫu Builder . Ngay cả khi các tham số ít được xem xét sử dụng các nhà máy tĩnh để tránh các hạn chế của quá tải hàm tạo.


1

Bạn có thể tưởng tượng việc tạo ra một lớp để thể hiện trạng thái của một loại khí lý tưởng.

Một lớp nên đại diện cho hành vi của một loại khí lý tưởng. Một thể hiện lớp - một đối tượng - đại diện cho trạng thái.

Đây không phải là chọn nits. Thiết kế lớp nên từ quan điểm hành vi và chức năng. Hành vi được phơi bày công khai sao cho một đối tượng được khởi tạo (vâng, đó là thừa) đạt được trạng thái thông qua việc thực hiện hành vi.

Vì thế:

Gas.HeatToFarenheit( 451 );

Và sau đây chỉ đơn giản là vô nghĩa và không thể trong thế giới thực. Và đừng làm điều đó trong mã:

Gas.MoleculeDistiance = 2.34;  //nanometers

Hành vi đưa ra câu hỏi "thông tin trùng lặp"

Ảnh hưởng đến các thuộc tính trạng thái giống nhau thông qua các phương pháp khác nhau không phải là trùng lặp. Những điều sau đây cũng ảnh hưởng đến nhiệt độ, nhưng nó không phải là một bản sao ở trên:

Gas.PressurizeTo( 34 );  //pascals

0

Có thể bạn có thể sử dụng việc tạo mã để tạo các dạng khác nhau của phương trình từ dạng đơn mà bạn chỉ định.

Tôi có thể hình ảnh nó sẽ khá khó khăn cho các công thức phức tạp hơn. Nhưng đối với đơn giản bạn cung cấp cho bạn chỉ phải

  • di chuyển các biến từ bên này sang bên kia bằng cách chia
  • lật bên trái và bên phải trên phương trình

Tôi có thể tưởng tượng nếu bạn thêm phép nhân, cộng và trừ vào danh sách đó và công thức cơ sở của bạn chỉ có mỗi biến một lần thì bạn có thể sử dụng thao tác chuỗi để tự động tạo tất cả các phiên bản của công thức với mã soạn sẵn thích hợp để sử dụng đúng các biến đã biết .

Wolfram Alpha có các công cụ thao tác phương trình khác nhau chẳng hạn.

Tuy nhiên!! trừ khi bạn có danh sách lớn các lớp này để sản xuất, tôi không thể xem mã đó là sử dụng hiệu quả thời gian của bạn.

Bạn chỉ cần tạo mã này một lần cho mỗi chức năng và các chức năng của bạn có thể quá phức tạp để được giải quyết đơn giản như ví dụ của bạn.

Bằng tay mã hóa mỗi phiên bản có khả năng là phương pháp nhanh nhất và đáng tin cậy nhất của việc tạo các chức năng và mặc dù bạn có thể tưởng tượng một giải pháp mà bạn code để lặp đi lặp lại đoán giá trị và thử nghiệm cho sự thật, tôi nghĩ rằng bạn làm cần thiết để tạo ra và biên dịch các chức năng để tính toán biến không xác định với lượng công suất xử lý hợp lý.


0

Tôi nghĩ rằng bạn đang xem xét lại điều này, hãy xem xét giải pháp sau đây:

double computeIdealGasLaw(double a, double b, double c){
    return a * b / c;
}
// example
//T = P * v/R
double P = ....;
double v = ....;
// R is a constant;
double T = computeIdealGasLaw(P, v, R); 


//P = R * T/v
double T = ....;
double v = ....;
// R is a constant;
double P = computeIdealGasLaw(R, T, v); 

//v = R * T/P
double T = ....;
double P = ....;
// R is a constant;
double v = computeIdealGasLaw(R, T, P); 

Bạn chỉ cần xác định một hàm, không trùng lặp, thậm chí bạn có thể sử dụng hàm này để triển khai lớp bên trong hoặc bạn chỉ có thể sử dụng hàm này mà bạn chỉ cần thay đổi tham số nào bạn sử dụng ở đâu.

Bạn cũng có thể sử dụng mẫu tương tự này cho bất kỳ tình huống nào bạn có hàm trong đó các giá trị được hoán đổi xung quanh.

Nếu bạn đang sử dụng một ngôn ngữ được gõ tĩnh và phải xử lý một hàm trong đó các giá trị có thể là các loại khác nhau, bạn có thể sử dụng lập trình mẫu (lưu ý, sử dụng cú pháp C ++)

template<class A, class B, class C, class D>
D computeIdealGasLaw(A a, B b, C c){
    return D(a * b / c);
}
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.